mirror of
https://github.com/go-i2p/go-sam-go.git
synced 2025-12-01 09:54:58 -05:00
Check in datagram2 and datagram3, update godoc
This commit is contained in:
130
README.md
130
README.md
@@ -5,9 +5,21 @@
|
||||
|
||||
A pure-Go implementation of SAMv3.3 (Simple Anonymous Messaging) for I2P, focused on maintainability and clean architecture. This project is forked from `github.com/go-i2p/sam3` with reorganized code structure.
|
||||
|
||||
**WARNING: This is a new package. Streaming works. Repliable datagrams and Raw datagrams work. Primary Sessions, work but are untested. Authenticated Repliable Datagrams(Datagram2), and Unauthenticated Repliable Datagrams(Datagram3) are NOT YET IMPLEMENTED.**
|
||||
**The API should not change much.**
|
||||
**It needs more people looking at it.**
|
||||
## ⚠️ Implementation Status
|
||||
|
||||
**Stable & Production-Ready:**
|
||||
- ✅ **Stream** - TCP-like reliable connections (fully tested)
|
||||
- ✅ **Datagram** - Legacy authenticated repliable datagrams (fully tested)
|
||||
- ✅ **Raw** - Encrypted unauthenticated datagrams (fully tested)
|
||||
|
||||
**Implemented & Documented (Awaiting I2P Router Support):**
|
||||
- ⚠️ **Datagram2** - Authenticated repliable datagrams with replay protection (spec finalized early 2025, no router implementations yet)
|
||||
- ⚠️ **Datagram3** - Unauthenticated repliable datagrams with hash-based sources (spec finalized early 2025, no router implementations yet)
|
||||
|
||||
**Partially Implemented:**
|
||||
- 🔶 **Primary Sessions** - Multi-session management (works but needs more testing)
|
||||
|
||||
**Note:** DATAGRAM2 and DATAGRAM3 are fully implemented in this library but require I2P router support (Java I2P or i2pd) to function. Check your router's release notes for SAMv3 DATAGRAM2/3 support.
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
@@ -102,22 +114,67 @@ raw, err := sam.NewRawSession("raw", keys, options, 0) // 0 = auto-assign UDP po
|
||||
n, err := raw.WriteTo(data, dest)
|
||||
```
|
||||
|
||||
#### `datagram2` Package (Planned - Not Implemented)
|
||||
Authenticated repliable datagrams:
|
||||
#### `datagram2` Package
|
||||
Authenticated repliable datagrams with replay protection:
|
||||
```go
|
||||
// Will be available in future release - currently not implemented
|
||||
// dgram2, err := sam.NewDatagram2Session("udp", keys, options, 0)
|
||||
// n, err := dgram2.WriteTo(data, dest)
|
||||
// DATAGRAM2 - Authenticated with replay protection (requires router support)
|
||||
// Specification finalized early 2025, awaiting I2P router implementation
|
||||
import "github.com/go-i2p/go-sam-go/datagram2"
|
||||
|
||||
session, err := datagram2.NewDatagram2Session(sam, "session-id", keys, options)
|
||||
if err != nil {
|
||||
// Handle error - may fail if router doesn't support DATAGRAM2 yet
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
// Send authenticated datagram
|
||||
err = session.SendDatagram(data, destination)
|
||||
|
||||
// Receive with full authentication
|
||||
conn := session.PacketConn()
|
||||
n, addr, err := conn.ReadFrom(buffer)
|
||||
```
|
||||
|
||||
#### `datagram3` Package (Planned - Not Implemented)
|
||||
Unauthenticated repliable datagrams:
|
||||
**Security:** Provides cryptographic authentication and replay protection. Recommended for new applications requiring source verification.
|
||||
|
||||
**Status:** Implementation complete. Waiting for I2P router support (Java I2P 0.9.x+ or i2pd 2.x+).
|
||||
|
||||
#### `datagram3` Package
|
||||
⚠️ **SECURITY WARNING:** Unauthenticated repliable datagrams with hash-based sources:
|
||||
```go
|
||||
// Will be available in future release - currently not implemented
|
||||
// dgram3, err := sam.NewDatagram3Session("udp", keys, options, 0)
|
||||
// n, err := dgram3.WriteTo(data, dest)
|
||||
// DATAGRAM3 - UNAUTHENTICATED sources (requires router support + app-layer auth)
|
||||
// Sources are 32-byte hashes that can be spoofed - implement your own authentication!
|
||||
import "github.com/go-i2p/go-sam-go/datagram3"
|
||||
|
||||
session, err := datagram3.NewDatagram3Session(sam, "session-id", keys, options)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
// Receive datagram with UNAUTHENTICATED source hash
|
||||
reader := session.NewReader()
|
||||
datagram, err := reader.ReceiveDatagram()
|
||||
|
||||
// ⚠️ Source hash is NOT authenticated - verify at application layer!
|
||||
// Resolve hash to destination for reply (requires NAMING LOOKUP)
|
||||
err = datagram.ResolveSource(session)
|
||||
|
||||
// Send reply
|
||||
writer := session.NewWriter()
|
||||
err = writer.SendDatagram(replyData, datagram.Source)
|
||||
```
|
||||
|
||||
**Security:** ⚠️ **Sources are NOT authenticated and can be spoofed!** Only use when:
|
||||
- Application implements message-level authentication (signatures, HMAC, etc.)
|
||||
- Source identity is not security-critical
|
||||
- Lower overhead is essential
|
||||
|
||||
**Use DATAGRAM2 instead if you need authenticated sources.**
|
||||
|
||||
**Status:** Implementation complete with comprehensive security documentation. Waiting for I2P router support.
|
||||
|
||||
### Configuration
|
||||
|
||||
Built-in configuration profiles:
|
||||
@@ -139,7 +196,8 @@ export DEBUG_I2P=error # Error level
|
||||
## 🔧 Requirements
|
||||
|
||||
- Go 1.24.2 or later (toolchain go1.24.4)
|
||||
- Running I2P router with SAM enabled (default port: 7656)
|
||||
- Running I2P router with SAM bridge enabled (default port: 7656)
|
||||
- For DATAGRAM2/DATAGRAM3: I2P router with SAMv3 DATAGRAM2/3 support (check router release notes)
|
||||
|
||||
## 📝 Development
|
||||
|
||||
@@ -147,10 +205,52 @@ export DEBUG_I2P=error # Error level
|
||||
# Format code
|
||||
make fmt
|
||||
|
||||
# Run tests
|
||||
# Run tests (short mode, no I2P required)
|
||||
go test -short ./...
|
||||
|
||||
# Run full integration tests (requires running I2P router)
|
||||
# Note: I2P tests can take 30-150 seconds due to tunnel establishment
|
||||
go test ./...
|
||||
|
||||
# Run with race detection
|
||||
go test -race -short ./...
|
||||
```
|
||||
|
||||
## 📖 Package Documentation
|
||||
|
||||
Each sub-package has comprehensive documentation:
|
||||
|
||||
- **[datagram2/](datagram2/README.md)** - DATAGRAM2 authenticated datagrams with replay protection
|
||||
- **[datagram3/](datagram3/README.md)** - ⚠️ DATAGRAM3 unauthenticated datagrams (security warnings!)
|
||||
- **[stream/](stream/)** - TCP-like reliable connections
|
||||
- **[datagram/](datagram/)** - Legacy authenticated datagrams
|
||||
- **[raw/](raw/)** - Encrypted unauthenticated datagrams
|
||||
- **[primary/](primary/)** - PRIMARY session management
|
||||
|
||||
## 🔒 Security Notes
|
||||
|
||||
### DATAGRAM3 Security Warning
|
||||
|
||||
⚠️ **CRITICAL:** DATAGRAM3 sources are **NOT authenticated**. Any attacker can claim to be any sender by providing a fake hash. Only use DATAGRAM3 when:
|
||||
|
||||
1. You implement application-layer authentication (Ed25519 signatures, HMAC, etc.)
|
||||
2. Source identity is not security-critical
|
||||
3. You understand the security implications
|
||||
|
||||
**For authenticated sources, use DATAGRAM2 instead.**
|
||||
|
||||
See [datagram3/AUDIT.md](datagram3/AUDIT.md) for comprehensive security analysis including attack scenarios and mitigations.
|
||||
|
||||
### I2P Timing Considerations
|
||||
|
||||
I2P operations have significant latency due to tunnel-based architecture:
|
||||
|
||||
- **Session creation:** 2-5 minutes on initial connection
|
||||
- **Message delivery:** Variable (network-dependent)
|
||||
- **Best practice:** Use generous timeouts (5+ minutes) and exponential backoff retry logic
|
||||
|
||||
All tests accommodate I2P timing requirements.
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT License
|
||||
|
||||
27
common/doc.go
Normal file
27
common/doc.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Package common provides core SAM protocol implementation and shared utilities for I2P.
|
||||
//
|
||||
// This package implements the foundational SAMv3.3 protocol communication layer, including
|
||||
// session management, configuration options, I2P name resolution, and shared abstractions
|
||||
// used by all session types (stream, datagram, raw).
|
||||
//
|
||||
// Core types:
|
||||
// - SAM: Base connection to I2P SAM bridge (default port 7656)
|
||||
// - Session: Base session interface with lifecycle management
|
||||
// - I2PConfig: Configuration builder for tunnel parameters
|
||||
// - SAMEmit: SAM protocol command formatter
|
||||
//
|
||||
// Session creation requires 2-5 minutes for I2P tunnel establishment; use generous timeouts
|
||||
// and exponential backoff retry logic. All network operations should use context.Context
|
||||
// for cancellation and timeout control.
|
||||
//
|
||||
// Basic usage:
|
||||
//
|
||||
// sam, err := common.NewSAM("127.0.0.1:7656")
|
||||
// if err != nil { log.Fatal(err) }
|
||||
// defer sam.Close()
|
||||
// session, err := sam.NewGenericSession("STREAM", "my-session", keys, []string{"inbound.length=1"})
|
||||
// defer session.Close()
|
||||
//
|
||||
// This package is primarily used as a foundation by higher-level packages (stream, datagram,
|
||||
// raw, primary) rather than being used directly by applications.
|
||||
package common
|
||||
@@ -260,25 +260,29 @@ datagrams over I2P. This session type provides UDP-like messaging capabilities
|
||||
through the I2P network, allowing applications to send and receive datagrams
|
||||
with message reliability and ordering guarantees. The session manages the
|
||||
underlying I2P connection and provides methods for creating readers and writers.
|
||||
Example usage: session, err := NewDatagramSession(sam, "my-session", keys,
|
||||
options)
|
||||
For PRIMARY subsessions, it can use UDP forwarding mode where datagrams are
|
||||
received via UDP socket. Example usage: session, err := NewDatagramSession(sam,
|
||||
"my-session", keys, options)
|
||||
|
||||
#### func NewDatagramSession
|
||||
|
||||
```go
|
||||
func NewDatagramSession(sam *common.SAM, id string, keys i2pkeys.I2PKeys, options []string) (*DatagramSession, error)
|
||||
```
|
||||
NewDatagramSession creates a new datagram session for UDP-like I2P messaging.
|
||||
This function establishes a new datagram session with the provided SAM
|
||||
connection, session ID, cryptographic keys, and configuration options. It
|
||||
returns a DatagramSession instance that can be used for sending and receiving
|
||||
datagrams over the I2P network. Example usage: session, err :=
|
||||
NewDatagramSession creates a new datagram session for UDP-like I2P messaging
|
||||
using SAMv3 UDP forwarding. This function establishes a new datagram session
|
||||
with the provided SAM connection, session ID, cryptographic keys, and
|
||||
configuration options. It automatically creates a UDP listener for receiving
|
||||
forwarded datagrams (SAMv3 requirement) and configures the session with
|
||||
PORT/HOST parameters. V1/V2 compatibility (reading from TCP control socket) is
|
||||
no longer supported. Returns a DatagramSession instance that uses UDP forwarding
|
||||
for all datagram reception. Example usage: session, err :=
|
||||
NewDatagramSession(sam, "my-session", keys, []string{"inbound.length=1"})
|
||||
|
||||
#### func NewDatagramSessionFromSubsession
|
||||
|
||||
```go
|
||||
func NewDatagramSessionFromSubsession(sam *common.SAM, id string, keys i2pkeys.I2PKeys, options []string) (*DatagramSession, error)
|
||||
func NewDatagramSessionFromSubsession(sam *common.SAM, id string, keys i2pkeys.I2PKeys, options []string, udpConn *net.UDPConn) (*DatagramSession, error)
|
||||
```
|
||||
NewDatagramSessionFromSubsession creates a DatagramSession for a subsession that
|
||||
has already been registered with a PRIMARY session using SESSION ADD. This
|
||||
@@ -289,12 +293,17 @@ This function is specifically designed for use with SAMv3.3 PRIMARY sessions
|
||||
where subsessions are created using SESSION ADD rather than SESSION CREATE
|
||||
commands.
|
||||
|
||||
For PRIMARY datagram subsessions, UDP forwarding is mandatory (SAMv3
|
||||
requirement). The UDP connection must be provided for proper datagram reception
|
||||
via UDP forwarding.
|
||||
|
||||
Parameters:
|
||||
|
||||
- sam: SAM connection for data operations (separate from the primary session's control connection)
|
||||
- id: The subsession ID that was already registered with SESSION ADD
|
||||
- keys: The I2P keys from the primary session (shared across all subsessions)
|
||||
- options: Configuration options for the subsession
|
||||
- udpConn: UDP connection for receiving forwarded datagrams (required, not nil)
|
||||
|
||||
Returns a DatagramSession ready for use without attempting to create a new SAM
|
||||
session.
|
||||
@@ -316,9 +325,9 @@ address:", myAddr.Base32())
|
||||
func (s *DatagramSession) Close() error
|
||||
```
|
||||
Close closes the datagram session and all associated resources. This method
|
||||
safely terminates the session, closes the underlying connection, and cleans up
|
||||
any background goroutines. It's safe to call multiple times. Example usage:
|
||||
defer session.Close()
|
||||
safely terminates the session, closes the UDP listener and underlying
|
||||
connection, and cleans up any background goroutines. It's safe to call multiple
|
||||
times. Example usage: defer session.Close()
|
||||
|
||||
#### func (*DatagramSession) Dial
|
||||
|
||||
@@ -581,9 +590,10 @@ NewDatagramSessionWithPorts creates a new datagram session with port
|
||||
specifications. This method allows configuring specific port ranges for the
|
||||
session, enabling fine-grained control over network communication ports for
|
||||
advanced routing scenarios. Port configuration is useful for applications
|
||||
requiring specific port mappings or firewall compatibility. Example usage:
|
||||
session, err := sam.NewDatagramSessionWithPorts(id, "8080", "8081", keys,
|
||||
options)
|
||||
requiring specific port mappings or firewall compatibility. This function
|
||||
creates a UDP listener for SAMv3 UDP forwarding (required for v3-only mode).
|
||||
Example usage: session, err := sam.NewDatagramSessionWithPorts(id, "8080",
|
||||
"8081", keys, options)
|
||||
|
||||
#### func (*SAM) NewDatagramSessionWithSignature
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package datagram
|
||||
# go-sam-go/datagram
|
||||
|
||||
High-level datagram library for UDP-like message delivery over I2P using the SAMv3 protocol.
|
||||
Datagram library for authenticated UDP-like message delivery over I2P using the SAMv3 protocol.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -8,16 +8,13 @@ Install using Go modules with the package path `github.com/go-i2p/go-sam-go/data
|
||||
|
||||
## Usage
|
||||
|
||||
The package provides UDP-like datagram messaging over I2P networks. [`DatagramSession`](datagram/types.go) manages the session lifecycle, [`DatagramReader`](datagram/types.go) handles incoming datagrams, [`DatagramWriter`](datagram/types.go) sends outgoing datagrams, and [`DatagramConn`](datagram/types.go) implements the standard `net.PacketConn` interface for seamless integration with existing Go networking code.
|
||||
The package provides authenticated datagram messaging over I2P networks. DatagramSession manages the session lifecycle, DatagramReader handles incoming datagrams, DatagramWriter sends outgoing datagrams, and DatagramConn implements the standard net.PacketConn interface.
|
||||
|
||||
Create sessions using [`NewDatagramSession`](datagram/session.go), send messages with [`SendDatagram()`](datagram/session.go), and receive messages using [`ReceiveDatagram()`](datagram/session.go). The implementation supports I2P address resolution, configurable tunnel parameters, and comprehensive error handling with proper resource cleanup.
|
||||
|
||||
Key features include full `net.PacketConn` and `net.Conn` compatibility, I2P destination management, base64 payload encoding, and concurrent datagram processing with proper synchronization.
|
||||
Create sessions using NewDatagramSession(), send messages with SendDatagram(), and receive messages using ReceiveDatagram(). Supports I2P address resolution, configurable tunnel parameters, and proper resource cleanup.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- github.com/go-i2p/go-sam-go/common - Core SAM protocol implementation
|
||||
- github.com/go-i2p/i2pkeys - I2P cryptographic key handling
|
||||
- github.com/go-i2p/logger - Logging functionality
|
||||
- github.com/go-i2p/i2pkeys - I2P cryptographic key handling
|
||||
- github.com/sirupsen/logrus - Structured logging
|
||||
- github.com/samber/oops - Enhanced error handling
|
||||
- github.com/samber/oops - Enhanced error handling
|
||||
|
||||
29
datagram/doc.go
Normal file
29
datagram/doc.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Package datagram provides legacy authenticated datagram sessions for I2P using SAMv3 DATAGRAM.
|
||||
//
|
||||
// DATAGRAM sessions provide authenticated, repliable UDP-like messaging over I2P tunnels.
|
||||
// This is the legacy format without replay protection. For new applications requiring replay
|
||||
// protection, use package datagram2 instead.
|
||||
//
|
||||
// Key features:
|
||||
// - Authenticated datagrams with signature verification
|
||||
// - Repliable (can send replies to sender)
|
||||
// - No replay protection (use datagram2 if needed)
|
||||
// - UDP-like messaging (unreliable, unordered)
|
||||
// - Maximum 31744 bytes per datagram (11 KB recommended)
|
||||
// - Implements net.PacketConn interface
|
||||
//
|
||||
// Session creation requires 2-5 minutes for I2P tunnel establishment. Use generous timeouts
|
||||
// and exponential backoff retry logic.
|
||||
//
|
||||
// Basic usage:
|
||||
//
|
||||
// sam, err := common.NewSAM("127.0.0.1:7656")
|
||||
// session, err := datagram.NewDatagramSession(sam, "my-session", keys, []string{"inbound.length=1"})
|
||||
// defer session.Close()
|
||||
// conn := session.PacketConn()
|
||||
// n, err := conn.WriteTo(data, destination)
|
||||
// n, addr, err := conn.ReadFrom(buffer)
|
||||
//
|
||||
// See also: Package datagram2 (with replay protection), datagram3 (unauthenticated),
|
||||
// stream (TCP-like), raw (non-repliable), primary (multi-session management).
|
||||
package datagram
|
||||
690
datagram2/DOC.md
690
datagram2/DOC.md
@@ -2,5 +2,695 @@
|
||||
--
|
||||
import "github.com/go-i2p/go-sam-go/datagram2"
|
||||
|
||||
Package datagram2 provides authenticated datagram2 sessions with replay
|
||||
protection for I2P.
|
||||
|
||||
DATAGRAM2 is a new format specified in early 2025 that replaces legacy DATAGRAM
|
||||
sessions for applications requiring replay protection. It uses the SAMv3
|
||||
protocol with STYLE=DATAGRAM2.
|
||||
|
||||
# Key Features
|
||||
|
||||
- Authenticated datagrams with signature verification
|
||||
- Replay protection (not available in legacy DATAGRAM)
|
||||
- Offline signature support
|
||||
- UDP-like messaging (unreliable, unordered)
|
||||
- Maximum 31744 bytes (11 KB recommended for reliability)
|
||||
- Compatible with SAMv3.3 PRIMARY sessions
|
||||
|
||||
# Basic Usage
|
||||
|
||||
Create a session:
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
sam, err := common.NewSAM("127.0.0.1:7656")
|
||||
session, err := datagram2.NewDatagram2Session(sam, "my-session", keys, []string{"inbound.length=1"})
|
||||
defer session.Close()
|
||||
|
||||
Send and receive using PacketConn:
|
||||
|
||||
conn := session.PacketConn()
|
||||
defer conn.Close()
|
||||
n, err := conn.WriteTo(data, destination)
|
||||
n, addr, err := conn.ReadFrom(buffer)
|
||||
|
||||
# I2P Timing Considerations
|
||||
|
||||
Session creation: 2-5 minutes for I2P tunnel establishment
|
||||
|
||||
Message delivery: Variable latency (network-dependent)
|
||||
|
||||
Recommended: Use generous timeouts (5+ minutes for session creation) and retry
|
||||
logic with exponential backoff.
|
||||
|
||||
# Implementation Status
|
||||
|
||||
DATAGRAM2 specification finalized early 2025. This is one of the first
|
||||
implementations. Check I2P router documentation for SAMv3 DATAGRAM2 support
|
||||
(Java I2P 0.9.x+, i2pd 2.x+).
|
||||
|
||||
Current implementation status:
|
||||
|
||||
- Core session management: ✅ Implemented
|
||||
- UDP forwarding: ✅ Implemented
|
||||
- Send/receive operations: ✅ Implemented
|
||||
- PacketConn interface: ✅ Implemented
|
||||
- Replay protection: ✅ Implemented (handled by I2P router)
|
||||
- PRIMARY session integration: ⏸️ Deferred (low priority)
|
||||
|
||||
# See Also
|
||||
|
||||
Package datagram: Legacy DATAGRAM sessions (authenticated, no replay protection)
|
||||
|
||||
Package datagram3: DATAGRAM3 sessions (unauthenticated, hash-based sources)
|
||||
|
||||
Package stream: TCP-like reliable connections
|
||||
|
||||
Package raw: Encrypted but unauthenticated datagrams (non-repliable)
|
||||
|
||||
Package primary: PRIMARY session management for multiple subsessions
|
||||
|
||||
## Usage
|
||||
|
||||
#### type Datagram2
|
||||
|
||||
```go
|
||||
type Datagram2 struct {
|
||||
Data []byte // Raw datagram payload (up to ~31KB)
|
||||
Source i2pkeys.I2PAddr // Authenticated source destination
|
||||
Local i2pkeys.I2PAddr // Local destination (this session)
|
||||
}
|
||||
```
|
||||
|
||||
Datagram2 represents an authenticated I2P datagram2 message with replay
|
||||
protection. It encapsulates the payload data along with source and destination
|
||||
addressing details, providing all necessary information for processing received
|
||||
datagrams. The structure includes both the raw data bytes and I2P address
|
||||
information for routing.
|
||||
|
||||
All DATAGRAM2 messages are authenticated by the I2P router, with signature
|
||||
verification performed internally. Replay protection prevents replay attacks
|
||||
that affect legacy DATAGRAM.
|
||||
|
||||
Example usage:
|
||||
|
||||
if datagram.Source.Base32() == expectedSender {
|
||||
processData(datagram.Data)
|
||||
}
|
||||
|
||||
#### type Datagram2Addr
|
||||
|
||||
```go
|
||||
type Datagram2Addr struct {
|
||||
}
|
||||
```
|
||||
|
||||
Datagram2Addr implements net.Addr interface for I2P datagram2 addresses. This
|
||||
type provides standard Go networking address representation for I2P
|
||||
destinations, allowing seamless integration with existing Go networking code
|
||||
that expects net.Addr. The address wraps an I2P address and provides string
|
||||
representation and network type identification.
|
||||
|
||||
Example usage:
|
||||
|
||||
addr := &Datagram2Addr{addr: destination}
|
||||
fmt.Println(addr.Network(), addr.String())
|
||||
|
||||
#### func (*Datagram2Addr) Network
|
||||
|
||||
```go
|
||||
func (a *Datagram2Addr) Network() string
|
||||
```
|
||||
Network returns the network type for I2P datagram2 addresses. This implements
|
||||
the net.Addr interface by returning "datagram2" as the network type.
|
||||
|
||||
#### func (*Datagram2Addr) String
|
||||
|
||||
```go
|
||||
func (a *Datagram2Addr) String() string
|
||||
```
|
||||
String returns the string representation of the I2P address. This implements the
|
||||
net.Addr interface by returning the base32 address representation, which is
|
||||
suitable for display and logging purposes.
|
||||
|
||||
#### type Datagram2Conn
|
||||
|
||||
```go
|
||||
type Datagram2Conn struct {
|
||||
}
|
||||
```
|
||||
|
||||
Datagram2Conn implements net.PacketConn interface for I2P datagram2
|
||||
communication. This type provides compatibility with standard Go networking
|
||||
patterns by wrapping datagram2 session functionality in a familiar PacketConn
|
||||
interface. It manages internal readers and writers while providing standard
|
||||
connection operations.
|
||||
|
||||
The connection provides thread-safe concurrent access to I2P datagram2
|
||||
operations and properly handles cleanup on close. All datagrams are
|
||||
authenticated with replay protection provided by the DATAGRAM2 format.
|
||||
|
||||
Example usage:
|
||||
|
||||
conn := session.PacketConn()
|
||||
n, addr, err := conn.ReadFrom(buffer)
|
||||
n, err = conn.WriteTo(data, destination)
|
||||
|
||||
#### func (*Datagram2Conn) Close
|
||||
|
||||
```go
|
||||
func (c *Datagram2Conn) Close() error
|
||||
```
|
||||
Close closes the datagram2 connection and releases associated resources. This
|
||||
method implements the net.Conn interface. It closes the reader and writer but
|
||||
does not close the underlying session, which may be shared by other connections.
|
||||
Multiple calls to Close are safe and will return nil after the first call.
|
||||
|
||||
#### func (*Datagram2Conn) LocalAddr
|
||||
|
||||
```go
|
||||
func (c *Datagram2Conn) LocalAddr() net.Addr
|
||||
```
|
||||
LocalAddr returns the local network address as a Datagram2Addr containing the
|
||||
I2P destination address of this connection's session. This method implements the
|
||||
net.Conn interface and provides access to the local I2P destination.
|
||||
|
||||
#### func (*Datagram2Conn) Read
|
||||
|
||||
```go
|
||||
func (c *Datagram2Conn) Read(b []byte) (n int, err error)
|
||||
```
|
||||
Read implements net.Conn by wrapping ReadFrom for stream-like usage. It reads
|
||||
data into the provided byte slice and returns the number of bytes read. When
|
||||
reading, it also updates the remote address of the connection for subsequent
|
||||
Write calls. Note: This is not typical for datagrams which are connectionless,
|
||||
but provides compatibility with the net.Conn interface.
|
||||
|
||||
#### func (*Datagram2Conn) ReadFrom
|
||||
|
||||
```go
|
||||
func (c *Datagram2Conn) ReadFrom(p []byte) (n int, addr net.Addr, err error)
|
||||
```
|
||||
ReadFrom reads an authenticated datagram with replay protection from the
|
||||
connection. This method implements the net.PacketConn interface. It starts the
|
||||
receive loop if not already started and blocks until a datagram is received. The
|
||||
data is copied to the provided buffer p, and the authenticated source address is
|
||||
returned as a Datagram2Addr.
|
||||
|
||||
All datagrams are authenticated by the I2P router with DATAGRAM2 replay
|
||||
protection.
|
||||
|
||||
#### func (*Datagram2Conn) RemoteAddr
|
||||
|
||||
```go
|
||||
func (c *Datagram2Conn) RemoteAddr() net.Addr
|
||||
```
|
||||
RemoteAddr returns the remote network address of the connection. This method
|
||||
implements the net.Conn interface. For datagram2 connections, this returns the
|
||||
authenticated address of the last peer that sent data (set by Read), or nil if
|
||||
no data has been received yet.
|
||||
|
||||
#### func (*Datagram2Conn) SetDeadline
|
||||
|
||||
```go
|
||||
func (c *Datagram2Conn) SetDeadline(t time.Time) error
|
||||
```
|
||||
SetDeadline sets both read and write deadlines for the connection. This method
|
||||
implements the net.Conn interface by calling both SetReadDeadline and
|
||||
SetWriteDeadline with the same time value. If either deadline cannot be set, the
|
||||
first error encountered is returned.
|
||||
|
||||
#### func (*Datagram2Conn) SetReadDeadline
|
||||
|
||||
```go
|
||||
func (c *Datagram2Conn) SetReadDeadline(t time.Time) error
|
||||
```
|
||||
SetReadDeadline sets the deadline for future ReadFrom calls. This method
|
||||
implements the net.Conn interface. For datagram2 connections, this is currently
|
||||
a placeholder implementation that always returns nil. Timeout handling is
|
||||
managed differently for datagram operations.
|
||||
|
||||
#### func (*Datagram2Conn) SetWriteDeadline
|
||||
|
||||
```go
|
||||
func (c *Datagram2Conn) SetWriteDeadline(t time.Time) error
|
||||
```
|
||||
SetWriteDeadline sets the deadline for future WriteTo calls. This method
|
||||
implements the net.Conn interface. If the deadline is not zero, it calculates
|
||||
the timeout duration and sets it on the writer for subsequent write operations.
|
||||
|
||||
#### func (*Datagram2Conn) Write
|
||||
|
||||
```go
|
||||
func (c *Datagram2Conn) Write(b []byte) (n int, err error)
|
||||
```
|
||||
Write implements net.Conn by wrapping WriteTo for stream-like usage. It writes
|
||||
data to the remote address set by the last Read operation and returns the number
|
||||
of bytes written. If no remote address has been set, it returns an error. Note:
|
||||
This is not typical for datagrams which are connectionless, but provides
|
||||
compatibility with the net.Conn interface.
|
||||
|
||||
#### func (*Datagram2Conn) WriteTo
|
||||
|
||||
```go
|
||||
func (c *Datagram2Conn) WriteTo(p []byte, addr net.Addr) (n int, err error)
|
||||
```
|
||||
WriteTo writes an authenticated datagram with replay protection to the specified
|
||||
address. This method implements the net.PacketConn interface. The address must
|
||||
be a Datagram2Addr containing a valid I2P destination. The entire byte slice p
|
||||
is sent as a single authenticated datagram message with replay protection.
|
||||
|
||||
#### type Datagram2Reader
|
||||
|
||||
```go
|
||||
type Datagram2Reader struct {
|
||||
}
|
||||
```
|
||||
|
||||
Datagram2Reader handles incoming authenticated datagram2 reception from the I2P
|
||||
network. It provides asynchronous datagram reception through buffered channels,
|
||||
allowing applications to receive datagrams without blocking. The reader manages
|
||||
its own goroutine for continuous message processing and provides thread-safe
|
||||
access to received datagrams.
|
||||
|
||||
All datagrams received are authenticated by the I2P router with signature
|
||||
verification performed internally. DATAGRAM2 provides replay protection,
|
||||
preventing replay attacks that are possible with legacy DATAGRAM sessions.
|
||||
|
||||
Example usage:
|
||||
|
||||
reader := session.NewReader()
|
||||
for {
|
||||
datagram, err := reader.ReceiveDatagram()
|
||||
if err != nil {
|
||||
// Handle error
|
||||
}
|
||||
// Process datagram.Data from datagram.Source
|
||||
}
|
||||
|
||||
#### func (*Datagram2Reader) Close
|
||||
|
||||
```go
|
||||
func (r *Datagram2Reader) Close() error
|
||||
```
|
||||
Close closes the Datagram2Reader and stops its receive loop. This method safely
|
||||
terminates the reader, cleans up all associated resources, and signals any
|
||||
waiting goroutines to stop. It's safe to call multiple times and will not block
|
||||
if the reader is already closed.
|
||||
|
||||
Example usage:
|
||||
|
||||
defer reader.Close()
|
||||
|
||||
#### func (*Datagram2Reader) ReceiveDatagram
|
||||
|
||||
```go
|
||||
func (r *Datagram2Reader) ReceiveDatagram() (*Datagram2, error)
|
||||
```
|
||||
ReceiveDatagram receives a single authenticated datagram with replay protection
|
||||
from the I2P network. This method blocks until a datagram is received or an
|
||||
error occurs, returning the received datagram with its data and authenticated
|
||||
addressing information. It handles concurrent access safely and provides proper
|
||||
error handling for network issues.
|
||||
|
||||
All datagrams are authenticated by the I2P router with DATAGRAM2 replay
|
||||
protection.
|
||||
|
||||
Example usage:
|
||||
|
||||
datagram, err := reader.ReceiveDatagram()
|
||||
if err != nil {
|
||||
// Handle error
|
||||
}
|
||||
// Process datagram.Data from authenticated datagram.Source
|
||||
|
||||
#### type Datagram2Session
|
||||
|
||||
```go
|
||||
type Datagram2Session struct {
|
||||
*common.BaseSession
|
||||
}
|
||||
```
|
||||
|
||||
Datagram2Session represents an authenticated datagram2 session with replay
|
||||
protection. This session type provides UDP-like messaging capabilities through
|
||||
the I2P network with enhanced security features compared to legacy DATAGRAM
|
||||
sessions. DATAGRAM2 provides replay protection and offline signature support,
|
||||
making it the recommended format for new applications that don't require
|
||||
backward compatibility.
|
||||
|
||||
The session manages the underlying I2P connection and provides methods for
|
||||
creating readers and writers. For SAMv3 mode, it uses UDP forwarding where
|
||||
datagrams are received via a local UDP socket that the SAM bridge forwards to.
|
||||
|
||||
I2P Timing Considerations:
|
||||
|
||||
- Session creation: 2-5 minutes for tunnel establishment
|
||||
- Message delivery: Variable latency (network-dependent)
|
||||
- Use generous timeouts and retry logic with exponential backoff
|
||||
|
||||
Example usage:
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
session, err := NewDatagram2Session(sam, "my-session", keys, options)
|
||||
|
||||
#### func NewDatagram2Session
|
||||
|
||||
```go
|
||||
func NewDatagram2Session(sam *common.SAM, id string, keys i2pkeys.I2PKeys, options []string) (*Datagram2Session, error)
|
||||
```
|
||||
NewDatagram2Session creates a new datagram2 session with replay protection for
|
||||
UDP-like I2P messaging. This function establishes a new DATAGRAM2 session with
|
||||
the provided SAM connection, session ID, cryptographic keys, and configuration
|
||||
options. It automatically creates a UDP listener for receiving forwarded
|
||||
datagrams (SAMv3 requirement) and configures the session with PORT/HOST
|
||||
parameters.
|
||||
|
||||
DATAGRAM2 provides enhanced security compared to legacy DATAGRAM:
|
||||
|
||||
- Replay protection prevents replay attacks (not available in DATAGRAM)
|
||||
- Offline signature support for advanced key management
|
||||
- Identical SAM API for easy migration from DATAGRAM
|
||||
|
||||
I2P Timing Considerations:
|
||||
|
||||
- Session creation can take 2-5 minutes for I2P tunnel establishment
|
||||
- Use context.WithTimeout with generous timeouts (5+ minutes recommended)
|
||||
- Implement exponential backoff retry logic for connection attempts
|
||||
- Distinguish between I2P timing delays and actual failures
|
||||
|
||||
Example usage:
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
session, err := NewDatagram2Session(sam, "my-session", keys, []string{"inbound.length=1"})
|
||||
|
||||
#### func NewDatagram2SessionFromSubsession
|
||||
|
||||
```go
|
||||
func NewDatagram2SessionFromSubsession(sam *common.SAM, id string, keys i2pkeys.I2PKeys, options []string, udpConn *net.UDPConn) (*Datagram2Session, error)
|
||||
```
|
||||
NewDatagram2SessionFromSubsession creates a Datagram2Session for a subsession
|
||||
that has already been registered with a PRIMARY session using SESSION ADD. This
|
||||
constructor skips the session creation step since the subsession is already
|
||||
registered with the SAM bridge.
|
||||
|
||||
This function is specifically designed for use with SAMv3.3 PRIMARY sessions
|
||||
where subsessions are created using SESSION ADD rather than SESSION CREATE
|
||||
commands.
|
||||
|
||||
For PRIMARY datagram2 subsessions, UDP forwarding is mandatory (SAMv3
|
||||
requirement). The UDP connection must be provided for proper datagram reception
|
||||
via UDP forwarding.
|
||||
|
||||
DATAGRAM2 subsessions share the same cryptographic keys and I2P tunnels as the
|
||||
PRIMARY session, but can be differentiated using LISTEN_PORT for incoming
|
||||
datagram routing.
|
||||
|
||||
Parameters:
|
||||
|
||||
- sam: SAM connection for data operations (separate from the primary session's control connection)
|
||||
- id: The subsession ID that was already registered with SESSION ADD
|
||||
- keys: The I2P keys from the primary session (shared across all subsessions)
|
||||
- options: Configuration options for the subsession
|
||||
- udpConn: UDP connection for receiving forwarded datagrams (required, not nil)
|
||||
|
||||
Returns a Datagram2Session ready for use without attempting to create a new SAM
|
||||
session.
|
||||
|
||||
Example usage with PRIMARY session:
|
||||
|
||||
primary, err := sam.NewPrimarySession("main", keys, options)
|
||||
udpConn, _ := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0})
|
||||
sub, err := NewDatagram2SessionFromSubsession(sam, "sub1", keys, options, udpConn)
|
||||
|
||||
#### func (*Datagram2Session) Addr
|
||||
|
||||
```go
|
||||
func (s *Datagram2Session) Addr() i2pkeys.I2PAddr
|
||||
```
|
||||
Addr returns the I2P address of this datagram2 session. This address represents
|
||||
the session's identity on the I2P network and can be used by other nodes to send
|
||||
authenticated datagrams with replay protection to this session. The address is
|
||||
derived from the session's cryptographic keys.
|
||||
|
||||
Example usage:
|
||||
|
||||
myAddr := session.Addr()
|
||||
fmt.Println("My I2P address:", myAddr.Base32())
|
||||
|
||||
#### func (*Datagram2Session) Close
|
||||
|
||||
```go
|
||||
func (s *Datagram2Session) Close() error
|
||||
```
|
||||
Close closes the datagram2 session and all associated resources. This method
|
||||
safely terminates the session, closes the UDP listener and underlying
|
||||
connection, and cleans up any background goroutines. It's safe to call multiple
|
||||
times.
|
||||
|
||||
Example usage:
|
||||
|
||||
defer session.Close()
|
||||
|
||||
#### func (*Datagram2Session) NewReader
|
||||
|
||||
```go
|
||||
func (s *Datagram2Session) NewReader() *Datagram2Reader
|
||||
```
|
||||
NewReader creates a Datagram2Reader for receiving authenticated datagrams with
|
||||
replay protection. This method initializes a new reader with buffered channels
|
||||
for asynchronous datagram reception. The reader must be started manually with
|
||||
receiveLoop() for continuous operation.
|
||||
|
||||
All datagrams received through this reader are authenticated by the I2P router
|
||||
with signature verification performed internally. DATAGRAM2 provides replay
|
||||
protection, preventing replay attacks that are possible with legacy DATAGRAM
|
||||
sessions.
|
||||
|
||||
Example usage:
|
||||
|
||||
reader := session.NewReader()
|
||||
go reader.receiveLoop()
|
||||
for {
|
||||
datagram, err := reader.ReceiveDatagram()
|
||||
if err != nil {
|
||||
// Handle error
|
||||
}
|
||||
// Process datagram.Data from datagram.Source
|
||||
}
|
||||
|
||||
#### func (*Datagram2Session) NewWriter
|
||||
|
||||
```go
|
||||
func (s *Datagram2Session) NewWriter() *Datagram2Writer
|
||||
```
|
||||
NewWriter creates a Datagram2Writer for sending authenticated datagrams with
|
||||
replay protection. This method initializes a new writer with a default timeout
|
||||
of 30 seconds for send operations. The timeout can be customized using the
|
||||
SetTimeout method on the returned writer.
|
||||
|
||||
All datagrams sent through this writer are authenticated and provide replay
|
||||
protection. Maximum datagram size is 31744 bytes total (including headers), with
|
||||
11 KB recommended for best reliability across the I2P network.
|
||||
|
||||
Example usage:
|
||||
|
||||
writer := session.NewWriter().SetTimeout(60*time.Second)
|
||||
err := writer.SendDatagram(data, destination)
|
||||
|
||||
#### func (*Datagram2Session) PacketConn
|
||||
|
||||
```go
|
||||
func (s *Datagram2Session) PacketConn() net.PacketConn
|
||||
```
|
||||
PacketConn returns a net.PacketConn interface for this datagram2 session. This
|
||||
method provides compatibility with standard Go networking code by wrapping the
|
||||
datagram2 session in a connection that implements the PacketConn interface. The
|
||||
connection provides authenticated datagrams with replay protection.
|
||||
|
||||
Example usage:
|
||||
|
||||
conn := session.PacketConn()
|
||||
n, addr, err := conn.ReadFrom(buffer)
|
||||
n, err = conn.WriteTo(data, destination)
|
||||
|
||||
#### func (*Datagram2Session) ReceiveDatagram
|
||||
|
||||
```go
|
||||
func (s *Datagram2Session) ReceiveDatagram() (*Datagram2, error)
|
||||
```
|
||||
ReceiveDatagram receives a single authenticated datagram from the I2P network.
|
||||
This method is a convenience wrapper that performs a direct single read
|
||||
operation without starting a continuous receive loop. For continuous reception,
|
||||
use NewReader() and manage the reader lifecycle manually.
|
||||
|
||||
All datagrams are authenticated by the I2P router with signature verification
|
||||
performed internally. DATAGRAM2 provides replay protection.
|
||||
|
||||
Example usage:
|
||||
|
||||
datagram, err := session.ReceiveDatagram()
|
||||
if err != nil {
|
||||
// Handle error
|
||||
}
|
||||
// Process datagram.Data from datagram.Source
|
||||
|
||||
#### func (*Datagram2Session) SendDatagram
|
||||
|
||||
```go
|
||||
func (s *Datagram2Session) SendDatagram(data []byte, dest i2pkeys.I2PAddr) error
|
||||
```
|
||||
SendDatagram sends an authenticated datagram with replay protection to the
|
||||
specified destination. This is a convenience method that creates a temporary
|
||||
writer and sends the datagram immediately. For multiple sends, it's more
|
||||
efficient to create a writer once and reuse it.
|
||||
|
||||
Maximum datagram size is 31744 bytes total (including headers), with 11 KB
|
||||
recommended for best reliability across the I2P network. The datagram is
|
||||
authenticated and provides replay protection.
|
||||
|
||||
Example usage:
|
||||
|
||||
err := session.SendDatagram([]byte("hello"), destinationAddr)
|
||||
|
||||
#### type Datagram2Writer
|
||||
|
||||
```go
|
||||
type Datagram2Writer struct {
|
||||
}
|
||||
```
|
||||
|
||||
Datagram2Writer handles outgoing authenticated datagram2 transmission to I2P
|
||||
destinations. It provides methods for sending datagrams with configurable
|
||||
timeouts and handles the underlying SAM protocol communication for message
|
||||
delivery. The writer supports method chaining for configuration and provides
|
||||
error handling for send operations.
|
||||
|
||||
All datagrams are authenticated and provide replay protection. Maximum datagram
|
||||
size is 31744 bytes total (including headers), with 11 KB recommended for best
|
||||
reliability.
|
||||
|
||||
Example usage:
|
||||
|
||||
writer := session.NewWriter().SetTimeout(30*time.Second)
|
||||
err := writer.SendDatagram(data, destination)
|
||||
|
||||
#### func (*Datagram2Writer) SendDatagram
|
||||
|
||||
```go
|
||||
func (w *Datagram2Writer) SendDatagram(data []byte, dest i2pkeys.I2PAddr) error
|
||||
```
|
||||
SendDatagram sends an authenticated datagram with replay protection to the
|
||||
specified I2P destination. This method uses the SAMv3 UDP approach: sending via
|
||||
UDP socket to port 7655 with DATAGRAM2 format. The datagram is authenticated by
|
||||
the I2P router and provides replay protection not available in legacy DATAGRAM
|
||||
sessions.
|
||||
|
||||
Maximum datagram size is 31744 bytes total (including headers), with 11 KB
|
||||
recommended for best reliability across the I2P network. It blocks until the
|
||||
datagram is sent or an error occurs, respecting the configured timeout.
|
||||
|
||||
Example usage:
|
||||
|
||||
err := writer.SendDatagram([]byte("hello world"), destinationAddr)
|
||||
|
||||
#### func (*Datagram2Writer) SetTimeout
|
||||
|
||||
```go
|
||||
func (w *Datagram2Writer) SetTimeout(timeout time.Duration) *Datagram2Writer
|
||||
```
|
||||
SetTimeout sets the timeout for datagram2 write operations. This method
|
||||
configures the maximum time to wait for authenticated datagram send operations
|
||||
to complete. The timeout prevents indefinite blocking during network congestion
|
||||
or connection issues. Returns the writer instance for method chaining
|
||||
convenience.
|
||||
|
||||
Example usage:
|
||||
|
||||
writer.SetTimeout(30*time.Second).SendDatagram(data, destination)
|
||||
|
||||
#### type SAM
|
||||
|
||||
```go
|
||||
type SAM struct {
|
||||
*common.SAM
|
||||
}
|
||||
```
|
||||
|
||||
SAM wraps common.SAM to provide datagram2-specific functionality for I2P
|
||||
messaging. This type extends the base SAM functionality with methods
|
||||
specifically designed for DATAGRAM2 communication, providing authenticated
|
||||
datagrams with replay protection. DATAGRAM2 is the recommended format for new
|
||||
applications, offering enhanced security over legacy DATAGRAM sessions through
|
||||
replay attack prevention and offline signature support. Example usage: sam :=
|
||||
&SAM{SAM: baseSAM}; session, err := sam.NewDatagram2Session(id, keys, options)
|
||||
|
||||
#### func (*SAM) NewDatagram2Session
|
||||
|
||||
```go
|
||||
func (s *SAM) NewDatagram2Session(id string, keys i2pkeys.I2PKeys, options []string) (*Datagram2Session, error)
|
||||
```
|
||||
NewDatagram2Session creates a new datagram2 session with the SAM bridge using
|
||||
default settings. This method establishes a new DATAGRAM2 session for
|
||||
authenticated UDP-like messaging over I2P with replay protection. It uses
|
||||
default signature settings (Ed25519) and automatically configures UDP forwarding
|
||||
for SAMv3 compatibility. Session creation can take 2-5 minutes due to I2P tunnel
|
||||
establishment, so generous timeouts are recommended.
|
||||
|
||||
DATAGRAM2 provides enhanced security compared to legacy DATAGRAM:
|
||||
|
||||
- Replay protection prevents replay attacks
|
||||
- Offline signature support for advanced key management
|
||||
- Identical SAM API for easy migration
|
||||
|
||||
Example usage:
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
session, err := sam.NewDatagram2Session("my-session", keys, []string{"inbound.length=1"})
|
||||
|
||||
#### func (*SAM) NewDatagram2SessionWithPorts
|
||||
|
||||
```go
|
||||
func (s *SAM) NewDatagram2SessionWithPorts(id, fromPort, toPort string, keys i2pkeys.I2PKeys, options []string) (*Datagram2Session, error)
|
||||
```
|
||||
NewDatagram2SessionWithPorts creates a new datagram2 session with port
|
||||
specifications. This method allows configuring specific I2CP port ranges for the
|
||||
session, enabling fine-grained control over network communication ports for
|
||||
advanced routing scenarios. Port configuration is useful for applications
|
||||
requiring specific port mappings or PRIMARY session subsessions. This function
|
||||
automatically creates a UDP listener for SAMv3 UDP forwarding (required for v3
|
||||
mode).
|
||||
|
||||
The FROM_PORT and TO_PORT parameters specify I2CP ports for protocol-level
|
||||
communication, distinct from the UDP forwarding port which is auto-assigned by
|
||||
the OS.
|
||||
|
||||
Example usage:
|
||||
|
||||
session, err := sam.NewDatagram2SessionWithPorts(id, "8080", "8081", keys, options)
|
||||
|
||||
#### func (*SAM) NewDatagram2SessionWithSignature
|
||||
|
||||
```go
|
||||
func (s *SAM) NewDatagram2SessionWithSignature(id string, keys i2pkeys.I2PKeys, options []string, sigType string) (*Datagram2Session, error)
|
||||
```
|
||||
NewDatagram2SessionWithSignature creates a new datagram2 session with custom
|
||||
signature type. This method allows specifying a custom cryptographic signature
|
||||
type for the session, enabling advanced security configurations beyond the
|
||||
default Ed25519 algorithm. DATAGRAM2 supports offline signatures, allowing
|
||||
pre-signed destinations for enhanced privacy and key management flexibility.
|
||||
|
||||
Different signature types provide various security levels and compatibility
|
||||
options:
|
||||
|
||||
- Ed25519 (type 7) - Recommended for most applications
|
||||
- ECDSA (types 1-3) - Legacy compatibility
|
||||
- RedDSA (type 11) - Advanced privacy features
|
||||
|
||||
Example usage:
|
||||
|
||||
session, err := sam.NewDatagram2SessionWithSignature(id, keys, options, "EdDSA_SHA512_Ed25519")
|
||||
|
||||
20
datagram2/README.md
Normal file
20
datagram2/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# go-sam-go/datagram2
|
||||
|
||||
Datagram2 library for authenticated UDP-like messaging with replay protection over I2P using the SAMv3 protocol.
|
||||
|
||||
## Installation
|
||||
|
||||
Install using Go modules with the package path `github.com/go-i2p/go-sam-go/datagram2`.
|
||||
|
||||
## Usage
|
||||
|
||||
The package provides authenticated datagram messaging with replay protection over I2P networks. Datagram2Session manages the session lifecycle, Datagram2Reader handles incoming datagrams, Datagram2Writer sends outgoing datagrams, and Datagram2Conn implements the standard net.PacketConn interface.
|
||||
|
||||
Create sessions using NewDatagram2Session(), send messages with SendDatagram2(), and receive messages using ReceiveDatagram2(). Requires I2P router with DATAGRAM2 support. Check router release notes for compatibility.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- github.com/go-i2p/go-sam-go/common - Core SAM protocol implementation
|
||||
- github.com/go-i2p/i2pkeys - I2P cryptographic key handling
|
||||
- github.com/sirupsen/logrus - Structured logging
|
||||
- github.com/samber/oops - Enhanced error handling
|
||||
167
datagram2/SAM.go
Normal file
167
datagram2/SAM.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package datagram2
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/go-i2p/go-sam-go/common"
|
||||
"github.com/go-i2p/i2pkeys"
|
||||
"github.com/samber/oops"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// SAM wraps common.SAM to provide datagram2-specific functionality for I2P messaging.
|
||||
// This type extends the base SAM functionality with methods specifically designed for
|
||||
// DATAGRAM2 communication, providing authenticated datagrams with replay protection.
|
||||
// DATAGRAM2 is the recommended format for new applications, offering enhanced security
|
||||
// over legacy DATAGRAM sessions through replay attack prevention and offline signature support.
|
||||
// Example usage: sam := &SAM{SAM: baseSAM}; session, err := sam.NewDatagram2Session(id, keys, options)
|
||||
type SAM struct {
|
||||
*common.SAM
|
||||
}
|
||||
|
||||
// NewDatagram2Session creates a new datagram2 session with the SAM bridge using default settings.
|
||||
// This method establishes a new DATAGRAM2 session for authenticated UDP-like messaging over I2P
|
||||
// with replay protection. It uses default signature settings (Ed25519) and automatically configures
|
||||
// UDP forwarding for SAMv3 compatibility. Session creation can take 2-5 minutes due to I2P tunnel
|
||||
// establishment, so generous timeouts are recommended.
|
||||
//
|
||||
// DATAGRAM2 provides enhanced security compared to legacy DATAGRAM:
|
||||
// - Replay protection prevents replay attacks
|
||||
// - Offline signature support for advanced key management
|
||||
// - Identical SAM API for easy migration
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
// defer cancel()
|
||||
// session, err := sam.NewDatagram2Session("my-session", keys, []string{"inbound.length=1"})
|
||||
func (s *SAM) NewDatagram2Session(id string, keys i2pkeys.I2PKeys, options []string) (*Datagram2Session, error) {
|
||||
// Delegate to the package-level function for session creation
|
||||
// This provides consistency with the package API design
|
||||
return NewDatagram2Session(s.SAM, id, keys, options)
|
||||
}
|
||||
|
||||
// NewDatagram2SessionWithSignature creates a new datagram2 session with custom signature type.
|
||||
// This method allows specifying a custom cryptographic signature type for the session,
|
||||
// enabling advanced security configurations beyond the default Ed25519 algorithm.
|
||||
// DATAGRAM2 supports offline signatures, allowing pre-signed destinations for enhanced
|
||||
// privacy and key management flexibility.
|
||||
//
|
||||
// Different signature types provide various security levels and compatibility options:
|
||||
// - Ed25519 (type 7) - Recommended for most applications
|
||||
// - ECDSA (types 1-3) - Legacy compatibility
|
||||
// - RedDSA (type 11) - Advanced privacy features
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// session, err := sam.NewDatagram2SessionWithSignature(id, keys, options, "EdDSA_SHA512_Ed25519")
|
||||
func (s *SAM) NewDatagram2SessionWithSignature(id string, keys i2pkeys.I2PKeys, options []string, sigType string) (*Datagram2Session, error) {
|
||||
// Log session creation with signature type for debugging
|
||||
logger := log.WithFields(logrus.Fields{
|
||||
"id": id,
|
||||
"options": options,
|
||||
"sigType": sigType,
|
||||
})
|
||||
logger.Debug("Creating new Datagram2Session with signature")
|
||||
|
||||
// Create the base session using the common package with custom signature
|
||||
// CRITICAL: Use STYLE=DATAGRAM2 (not DATAGRAM) for replay protection
|
||||
session, err := s.SAM.NewGenericSessionWithSignature("DATAGRAM2", id, keys, sigType, options)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to create generic session with signature")
|
||||
return nil, oops.Errorf("failed to create datagram2 session: %w", err)
|
||||
}
|
||||
|
||||
// Ensure the session is of the correct type for datagram2 operations
|
||||
baseSession, ok := session.(*common.BaseSession)
|
||||
if !ok {
|
||||
logger.Error("Session is not a BaseSession")
|
||||
session.Close()
|
||||
return nil, oops.Errorf("invalid session type")
|
||||
}
|
||||
|
||||
// Initialize the datagram2 session with the base session and configuration
|
||||
ds := &Datagram2Session{
|
||||
BaseSession: baseSession,
|
||||
sam: s.SAM,
|
||||
options: options,
|
||||
}
|
||||
|
||||
logger.Debug("Successfully created Datagram2Session with signature")
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
// NewDatagram2SessionWithPorts creates a new datagram2 session with port specifications.
|
||||
// This method allows configuring specific I2CP port ranges for the session, enabling fine-grained
|
||||
// control over network communication ports for advanced routing scenarios. Port configuration
|
||||
// is useful for applications requiring specific port mappings or PRIMARY session subsessions.
|
||||
// This function automatically creates a UDP listener for SAMv3 UDP forwarding (required for v3 mode).
|
||||
//
|
||||
// The FROM_PORT and TO_PORT parameters specify I2CP ports for protocol-level communication,
|
||||
// distinct from the UDP forwarding port which is auto-assigned by the OS.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// session, err := sam.NewDatagram2SessionWithPorts(id, "8080", "8081", keys, options)
|
||||
func (s *SAM) NewDatagram2SessionWithPorts(id, fromPort, toPort string, keys i2pkeys.I2PKeys, options []string) (*Datagram2Session, error) {
|
||||
// Log session creation with port configuration for debugging
|
||||
logger := log.WithFields(logrus.Fields{
|
||||
"id": id,
|
||||
"fromPort": fromPort,
|
||||
"toPort": toPort,
|
||||
"options": options,
|
||||
})
|
||||
logger.Debug("Creating new Datagram2Session with ports")
|
||||
|
||||
// Create UDP listener for receiving forwarded datagrams (SAMv3 requirement)
|
||||
// The SAM bridge will forward incoming DATAGRAM2 messages to this local UDP port
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to resolve UDP address")
|
||||
return nil, oops.Errorf("failed to resolve UDP address: %w", err)
|
||||
}
|
||||
|
||||
udpConn, err := net.ListenUDP("udp", udpAddr)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to create UDP listener")
|
||||
return nil, oops.Errorf("failed to create UDP listener: %w", err)
|
||||
}
|
||||
|
||||
// Get the actual port assigned by the OS
|
||||
udpPort := udpConn.LocalAddr().(*net.UDPAddr).Port
|
||||
logger.WithField("udp_port", udpPort).Debug("Created UDP listener for datagram2 forwarding")
|
||||
|
||||
// Inject UDP forwarding parameters into session options (SAMv3 requirement)
|
||||
// HOST and PORT tell the SAM bridge where to forward received datagrams
|
||||
options = ensureUDPForwardingParameters(options, udpPort)
|
||||
|
||||
// Create the base session using the common package with port configuration
|
||||
// CRITICAL: Use STYLE=DATAGRAM2 (not DATAGRAM) for replay protection
|
||||
session, err := s.SAM.NewGenericSessionWithSignatureAndPorts("DATAGRAM2", id, fromPort, toPort, keys, common.SIG_EdDSA_SHA512_Ed25519, options)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to create generic session with ports")
|
||||
udpConn.Close() // Clean up UDP listener on error
|
||||
return nil, oops.Errorf("failed to create datagram2 session: %w", err)
|
||||
}
|
||||
|
||||
// Ensure the session is of the correct type for datagram2 operations
|
||||
baseSession, ok := session.(*common.BaseSession)
|
||||
if !ok {
|
||||
logger.Error("Session is not a BaseSession")
|
||||
session.Close()
|
||||
udpConn.Close() // Clean up UDP listener on error
|
||||
return nil, oops.Errorf("invalid session type")
|
||||
}
|
||||
|
||||
// Initialize the datagram2 session with UDP forwarding enabled
|
||||
ds := &Datagram2Session{
|
||||
BaseSession: baseSession,
|
||||
sam: s.SAM,
|
||||
options: options,
|
||||
udpConn: udpConn,
|
||||
udpEnabled: true,
|
||||
}
|
||||
|
||||
logger.Debug("Successfully created Datagram2Session with ports and UDP forwarding")
|
||||
return ds, nil
|
||||
}
|
||||
@@ -1,8 +1,29 @@
|
||||
// Package datagram2 provides authenticated datagram sessions with replay protection for I2P.
|
||||
//
|
||||
// DATAGRAM2 sessions provide authenticated, repliable UDP-like messaging over I2P tunnels
|
||||
// with replay attack protection. This is the recommended datagram format for applications
|
||||
// requiring both source authentication and replay protection.
|
||||
//
|
||||
// Key features:
|
||||
// - Authenticated datagrams with signature verification
|
||||
// - Replay protection (not available in legacy DATAGRAM)
|
||||
// - Repliable (can send replies to sender)
|
||||
// - UDP-like messaging (unreliable, unordered)
|
||||
// - Maximum 31744 bytes per datagram (11 KB recommended)
|
||||
// - Implements net.PacketConn interface
|
||||
//
|
||||
// Session creation requires 2-5 minutes for I2P tunnel establishment. Use generous timeouts
|
||||
// and exponential backoff retry logic.
|
||||
//
|
||||
// Basic usage:
|
||||
//
|
||||
// sam, err := common.NewSAM("127.0.0.1:7656")
|
||||
// session, err := datagram2.NewDatagram2Session(sam, "my-session", keys, []string{"inbound.length=1"})
|
||||
// defer session.Close()
|
||||
// conn := session.PacketConn()
|
||||
// n, err := conn.WriteTo(data, destination)
|
||||
// n, addr, err := conn.ReadFrom(buffer)
|
||||
//
|
||||
// See also: Package datagram (legacy, no replay protection), datagram3 (unauthenticated),
|
||||
// stream (TCP-like), raw (non-repliable), primary (multi-session management).
|
||||
package datagram2
|
||||
|
||||
/*
|
||||
* TODO: implement the Datagram2Session type for SAMv2 Datagram Sessions
|
||||
* This package provides the implementation for datagram sessions
|
||||
* using the SAMv2 protocol. It includes session management, datagram reading and writing,
|
||||
* and integration with the SAMv2 protocol for secure communication.
|
||||
*/
|
||||
|
||||
7
datagram2/log.go
Normal file
7
datagram2/log.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package datagram2
|
||||
|
||||
import (
|
||||
"github.com/go-i2p/logger"
|
||||
)
|
||||
|
||||
var log = logger.GetGoI2PLogger()
|
||||
212
datagram2/packetconn.go
Normal file
212
datagram2/packetconn.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package datagram2
|
||||
|
||||
import (
|
||||
"net"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/go-i2p/i2pkeys"
|
||||
"github.com/samber/oops"
|
||||
)
|
||||
|
||||
// ReadFrom reads an authenticated datagram with replay protection from the connection.
|
||||
// This method implements the net.PacketConn interface. It starts the receive loop if not
|
||||
// already started and blocks until a datagram is received. The data is copied to the provided
|
||||
// buffer p, and the authenticated source address is returned as a Datagram2Addr.
|
||||
//
|
||||
// All datagrams are authenticated by the I2P router with DATAGRAM2 replay protection.
|
||||
func (c *Datagram2Conn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
c.mu.RLock()
|
||||
if c.closed {
|
||||
c.mu.RUnlock()
|
||||
return 0, nil, oops.Errorf("connection is closed")
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
// Start receive loop if not already started
|
||||
go c.reader.receiveLoop()
|
||||
|
||||
datagram, err := c.reader.ReceiveDatagram()
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
// Copy data to the provided buffer
|
||||
n = copy(p, datagram.Data)
|
||||
addr = &Datagram2Addr{addr: datagram.Source}
|
||||
|
||||
return n, addr, nil
|
||||
}
|
||||
|
||||
// WriteTo writes an authenticated datagram with replay protection to the specified address.
|
||||
// This method implements the net.PacketConn interface. The address must be a Datagram2Addr
|
||||
// containing a valid I2P destination. The entire byte slice p is sent as a single authenticated
|
||||
// datagram message with replay protection.
|
||||
func (c *Datagram2Conn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||
c.mu.RLock()
|
||||
if c.closed {
|
||||
c.mu.RUnlock()
|
||||
return 0, oops.Errorf("connection is closed")
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
// Convert address to I2P address
|
||||
i2pAddr, ok := addr.(*Datagram2Addr)
|
||||
if !ok {
|
||||
return 0, oops.Errorf("address must be a Datagram2Addr")
|
||||
}
|
||||
|
||||
err = c.writer.SendDatagram(p, i2pAddr.addr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Close closes the datagram2 connection and releases associated resources.
|
||||
// This method implements the net.Conn interface. It closes the reader and writer
|
||||
// but does not close the underlying session, which may be shared by other connections.
|
||||
// Multiple calls to Close are safe and will return nil after the first call.
|
||||
func (c *Datagram2Conn) Close() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.closed {
|
||||
return nil
|
||||
}
|
||||
|
||||
var sessionID string
|
||||
if c.session != nil {
|
||||
sessionID = c.session.ID()
|
||||
} else {
|
||||
sessionID = "unknown"
|
||||
}
|
||||
logger := log.WithFields(map[string]interface{}{
|
||||
"session_id": sessionID,
|
||||
"style": "DATAGRAM2",
|
||||
})
|
||||
logger.Debug("Closing Datagram2Conn")
|
||||
|
||||
c.closed = true
|
||||
|
||||
// Clear the finalizer since we're cleaning up explicitly
|
||||
c.clearCleanup()
|
||||
|
||||
// Close reader and writer - these are owned by this connection
|
||||
if c.reader != nil {
|
||||
c.reader.Close()
|
||||
}
|
||||
|
||||
// DO NOT close the session - it's a shared resource that may be used by other connections
|
||||
// The session should be closed by the code that created it, not by individual connections
|
||||
// that use it. This follows the principle that the creator owns the resource.
|
||||
|
||||
logger.Debug("Successfully closed Datagram2Conn")
|
||||
return nil
|
||||
}
|
||||
|
||||
// LocalAddr returns the local network address as a Datagram2Addr containing
|
||||
// the I2P destination address of this connection's session. This method implements
|
||||
// the net.Conn interface and provides access to the local I2P destination.
|
||||
func (c *Datagram2Conn) LocalAddr() net.Addr {
|
||||
return &Datagram2Addr{addr: c.session.Addr()}
|
||||
}
|
||||
|
||||
// SetDeadline sets both read and write deadlines for the connection.
|
||||
// This method implements the net.Conn interface by calling both SetReadDeadline
|
||||
// and SetWriteDeadline with the same time value. If either deadline cannot be set,
|
||||
// the first error encountered is returned.
|
||||
func (c *Datagram2Conn) SetDeadline(t time.Time) error {
|
||||
if err := c.SetReadDeadline(t); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
// SetReadDeadline sets the deadline for future ReadFrom calls.
|
||||
// This method implements the net.Conn interface. For datagram2 connections,
|
||||
// this is currently a placeholder implementation that always returns nil.
|
||||
// Timeout handling is managed differently for datagram operations.
|
||||
func (c *Datagram2Conn) SetReadDeadline(t time.Time) error {
|
||||
// For datagrams, we handle timeouts differently
|
||||
// This is a placeholder implementation
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetWriteDeadline sets the deadline for future WriteTo calls.
|
||||
// This method implements the net.Conn interface. If the deadline is not zero,
|
||||
// it calculates the timeout duration and sets it on the writer for subsequent
|
||||
// write operations.
|
||||
func (c *Datagram2Conn) SetWriteDeadline(t time.Time) error {
|
||||
// Calculate timeout duration
|
||||
if !t.IsZero() {
|
||||
timeout := time.Until(t)
|
||||
c.writer.SetTimeout(timeout)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read implements net.Conn by wrapping ReadFrom for stream-like usage.
|
||||
// It reads data into the provided byte slice and returns the number of bytes read.
|
||||
// When reading, it also updates the remote address of the connection for subsequent
|
||||
// Write calls. Note: This is not typical for datagrams which are connectionless,
|
||||
// but provides compatibility with the net.Conn interface.
|
||||
func (c *Datagram2Conn) Read(b []byte) (n int, err error) {
|
||||
n, addr, err := c.ReadFrom(b)
|
||||
c.remoteAddr = addr.(*i2pkeys.I2PAddr)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// RemoteAddr returns the remote network address of the connection.
|
||||
// This method implements the net.Conn interface. For datagram2 connections,
|
||||
// this returns the authenticated address of the last peer that sent data (set by Read),
|
||||
// or nil if no data has been received yet.
|
||||
func (c *Datagram2Conn) RemoteAddr() net.Addr {
|
||||
if c.remoteAddr != nil {
|
||||
return &Datagram2Addr{addr: *c.remoteAddr}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write implements net.Conn by wrapping WriteTo for stream-like usage.
|
||||
// It writes data to the remote address set by the last Read operation and
|
||||
// returns the number of bytes written. If no remote address has been set,
|
||||
// it returns an error. Note: This is not typical for datagrams which are
|
||||
// connectionless, but provides compatibility with the net.Conn interface.
|
||||
func (c *Datagram2Conn) Write(b []byte) (n int, err error) {
|
||||
if c.remoteAddr == nil {
|
||||
return 0, oops.Errorf("no remote address set, use WriteTo or Read first")
|
||||
}
|
||||
|
||||
addr := &Datagram2Addr{addr: *c.remoteAddr}
|
||||
return c.WriteTo(b, addr)
|
||||
}
|
||||
|
||||
// cleanupDatagram2Conn is called by AddCleanup to ensure resources are cleaned up
|
||||
// even if the user forgets to call Close(). This prevents goroutine leaks.
|
||||
func cleanupDatagram2Conn(c *Datagram2Conn) {
|
||||
c.mu.Lock()
|
||||
if !c.closed {
|
||||
log.Warn("Datagram2Conn was garbage collected without being closed - cleaning up resources")
|
||||
c.closed = true
|
||||
if c.reader != nil {
|
||||
c.reader.Close()
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// addCleanup sets up automatic cleanup for the connection to prevent resource leaks
|
||||
func (c *Datagram2Conn) addCleanup() {
|
||||
c.cleanup = runtime.AddCleanup(&c.cleanup, cleanupDatagram2Conn, c)
|
||||
}
|
||||
|
||||
// clearCleanup removes the cleanup when Close() is called explicitly
|
||||
func (c *Datagram2Conn) clearCleanup() {
|
||||
var zero runtime.Cleanup
|
||||
if c.cleanup != zero {
|
||||
c.cleanup.Stop()
|
||||
c.cleanup = zero
|
||||
}
|
||||
}
|
||||
300
datagram2/read.go
Normal file
300
datagram2/read.go
Normal file
@@ -0,0 +1,300 @@
|
||||
package datagram2
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-i2p/logger"
|
||||
"github.com/samber/oops"
|
||||
)
|
||||
|
||||
// ReceiveDatagram receives a single authenticated datagram with replay protection from the I2P network.
|
||||
// This method blocks until a datagram is received or an error occurs, returning
|
||||
// the received datagram with its data and authenticated addressing information. It handles
|
||||
// concurrent access safely and provides proper error handling for network issues.
|
||||
//
|
||||
// All datagrams are authenticated by the I2P router with DATAGRAM2 replay protection.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// datagram, err := reader.ReceiveDatagram()
|
||||
// if err != nil {
|
||||
// // Handle error
|
||||
// }
|
||||
// // Process datagram.Data from authenticated datagram.Source
|
||||
func (r *Datagram2Reader) ReceiveDatagram() (*Datagram2, error) {
|
||||
// Hold read lock for the entire operation to prevent race with Close()
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
if r.closed {
|
||||
return nil, oops.Errorf("reader is closed")
|
||||
}
|
||||
|
||||
// Use select to handle multiple channel operations atomically
|
||||
// The lock ensures that channels won't be closed while we're selecting on them
|
||||
select {
|
||||
case datagram := <-r.recvChan:
|
||||
// Successfully received an authenticated datagram from the network
|
||||
return datagram, nil
|
||||
case err := <-r.errorChan:
|
||||
// An error occurred during datagram reception
|
||||
return nil, err
|
||||
case <-r.closeChan:
|
||||
// The reader has been closed while waiting for a datagram
|
||||
return nil, oops.Errorf("reader is closed")
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the Datagram2Reader and stops its receive loop.
|
||||
// This method safely terminates the reader, cleans up all associated resources,
|
||||
// and signals any waiting goroutines to stop. It's safe to call multiple times
|
||||
// and will not block if the reader is already closed.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// defer reader.Close()
|
||||
func (r *Datagram2Reader) Close() error {
|
||||
// Use sync.Once to ensure cleanup only happens once
|
||||
// This prevents double-close panics and ensures thread safety
|
||||
r.closeOnce.Do(func() {
|
||||
r.performCloseOperation()
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// performCloseOperation executes the complete close sequence with proper synchronization.
|
||||
func (r *Datagram2Reader) performCloseOperation() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.closed {
|
||||
return
|
||||
}
|
||||
|
||||
logger := r.initializeCloseLogger()
|
||||
r.signalReaderClosure(logger)
|
||||
r.waitForReceiveLoopTermination(logger)
|
||||
r.finalizeReaderClosure(logger)
|
||||
}
|
||||
|
||||
// initializeCloseLogger sets up logging context for the close operation.
|
||||
func (r *Datagram2Reader) initializeCloseLogger() *logger.Entry {
|
||||
sessionID := "unknown"
|
||||
if r.session != nil && r.session.BaseSession != nil {
|
||||
sessionID = r.session.ID()
|
||||
}
|
||||
logger := log.WithField("session_id", sessionID)
|
||||
logger.Debug("Closing Datagram2Reader")
|
||||
return logger
|
||||
}
|
||||
|
||||
// signalReaderClosure marks the reader as closed and signals termination.
|
||||
func (r *Datagram2Reader) signalReaderClosure(logger *logger.Entry) {
|
||||
r.closed = true
|
||||
// Signal the receive loop to terminate
|
||||
// This prevents the background goroutine from continuing to run
|
||||
close(r.closeChan)
|
||||
}
|
||||
|
||||
// waitForReceiveLoopTermination waits for the receive loop to stop with timeout protection.
|
||||
func (r *Datagram2Reader) waitForReceiveLoopTermination(logger *logger.Entry) {
|
||||
// Only wait for the receive loop if it was actually started
|
||||
if r.loopStarted {
|
||||
r.waitForLoopWithTimeout(logger)
|
||||
} else {
|
||||
logger.Debug("Receive loop was never started, skipping wait")
|
||||
}
|
||||
}
|
||||
|
||||
// waitForLoopWithTimeout waits for receive loop termination with timeout protection.
|
||||
func (r *Datagram2Reader) waitForLoopWithTimeout(logger *logger.Entry) {
|
||||
// Wait for the receive loop to confirm termination
|
||||
// This ensures proper cleanup before returning
|
||||
select {
|
||||
case <-r.doneChan:
|
||||
// Receive loop has confirmed it stopped
|
||||
logger.Debug("Receive loop stopped")
|
||||
case <-time.After(5 * time.Second):
|
||||
// Timeout protection to prevent indefinite blocking
|
||||
logger.Warn("Timeout waiting for receive loop to stop")
|
||||
}
|
||||
}
|
||||
|
||||
// finalizeReaderClosure performs final cleanup and logging.
|
||||
func (r *Datagram2Reader) finalizeReaderClosure(logger *logger.Entry) {
|
||||
// Clean up channels to prevent resource leaks
|
||||
// Note: We don't close r.recvChan and r.errorChan here because the receive loop
|
||||
// might still be sending on them. These channels will be garbage collected when
|
||||
// all references are dropped. Only the receive loop should close send-channels.
|
||||
|
||||
logger.Debug("Successfully closed Datagram2Reader")
|
||||
}
|
||||
|
||||
// receiveLoop continuously receives incoming authenticated datagrams in a separate goroutine.
|
||||
// This method handles the SAM protocol communication for datagram2 reception, parsing
|
||||
// UDP forwarded messages and forwarding authenticated datagrams to the appropriate channels.
|
||||
// It runs until the reader is closed and provides error handling for network issues.
|
||||
//
|
||||
// All datagrams received provide authentication and replay protection via DATAGRAM2 format.
|
||||
func (r *Datagram2Reader) receiveLoop() {
|
||||
// Initialize receive loop state in a separate function to handle locking properly
|
||||
if !r.initializeReceiveLoopState() {
|
||||
return
|
||||
}
|
||||
|
||||
logger := r.initializeReceiveLoop()
|
||||
defer r.signalReceiveLoopCompletion()
|
||||
|
||||
if err := r.validateSessionState(logger); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
r.runReceiveLoop(logger)
|
||||
}
|
||||
|
||||
// initializeReceiveLoopState safely initializes the receive loop state with proper locking.
|
||||
// Returns false if initialization failed, true if successful.
|
||||
func (r *Datagram2Reader) initializeReceiveLoopState() bool {
|
||||
// CRITICAL FIX: Check if we can acquire the lock without blocking
|
||||
// Use TryLock equivalent by checking state first
|
||||
r.mu.RLock()
|
||||
if r.closed {
|
||||
r.mu.RUnlock()
|
||||
return false
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
|
||||
// Now safely acquire write lock to set loopStarted
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
// Double-check closed state after acquiring write lock
|
||||
if r.closed {
|
||||
return false
|
||||
}
|
||||
|
||||
r.loopStarted = true
|
||||
return true
|
||||
}
|
||||
|
||||
// initializeReceiveLoop sets up logging context and returns a logger for the receive loop.
|
||||
func (r *Datagram2Reader) initializeReceiveLoop() *logger.Entry {
|
||||
sessionID := "unknown"
|
||||
if r.session != nil && r.session.BaseSession != nil {
|
||||
sessionID = r.session.ID()
|
||||
}
|
||||
logger := log.WithField("session_id", sessionID)
|
||||
logger.Debug("Starting datagram2 receive loop")
|
||||
return logger
|
||||
}
|
||||
|
||||
// signalReceiveLoopCompletion signals that the receive loop has completed execution.
|
||||
func (r *Datagram2Reader) signalReceiveLoopCompletion() {
|
||||
// Close doneChan to signal completion - channels should be closed by sender
|
||||
close(r.doneChan)
|
||||
}
|
||||
|
||||
// validateSessionState checks if the session is valid before starting the receive loop.
|
||||
func (r *Datagram2Reader) validateSessionState(logger *logger.Entry) error {
|
||||
if r.session == nil || r.session.BaseSession == nil {
|
||||
logger.Error("Invalid session state")
|
||||
select {
|
||||
case r.errorChan <- oops.Errorf("invalid session state"):
|
||||
case <-r.closeChan:
|
||||
}
|
||||
return oops.Errorf("invalid session state")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runReceiveLoop executes the main receive loop until the reader is closed.
|
||||
func (r *Datagram2Reader) runReceiveLoop(logger *logger.Entry) {
|
||||
for {
|
||||
select {
|
||||
case <-r.closeChan:
|
||||
logger.Debug("Receive loop terminated")
|
||||
return
|
||||
default:
|
||||
if !r.processIncomingDatagram(logger) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processIncomingDatagram receives and forwards a single authenticated datagram, returning false if the loop should terminate.
|
||||
func (r *Datagram2Reader) processIncomingDatagram(logger *logger.Entry) bool {
|
||||
if !r.checkReaderActiveState() {
|
||||
return false
|
||||
}
|
||||
|
||||
datagram, err := r.receiveDatagram()
|
||||
if err != nil {
|
||||
return r.handleDatagramError(err, logger)
|
||||
}
|
||||
|
||||
return r.forwardDatagramToChannel(datagram)
|
||||
}
|
||||
|
||||
// checkReaderActiveState verifies the reader is not closed before processing.
|
||||
func (r *Datagram2Reader) checkReaderActiveState() bool {
|
||||
r.mu.RLock()
|
||||
isClosed := r.closed
|
||||
r.mu.RUnlock()
|
||||
return !isClosed
|
||||
}
|
||||
|
||||
// handleDatagramError processes errors during datagram reception and reports them.
|
||||
func (r *Datagram2Reader) handleDatagramError(err error, logger *logger.Entry) bool {
|
||||
logger.WithError(err).Debug("Error receiving datagram2 message")
|
||||
select {
|
||||
case r.errorChan <- err:
|
||||
return true
|
||||
case <-r.closeChan:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// forwardDatagramToChannel sends the received authenticated datagram to the receive channel atomically.
|
||||
func (r *Datagram2Reader) forwardDatagramToChannel(datagram *Datagram2) bool {
|
||||
select {
|
||||
case r.recvChan <- datagram:
|
||||
return true
|
||||
case <-r.closeChan:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// receiveDatagram performs the actual datagram2 reception from the UDP connection.
|
||||
// This method handles UDP datagram2 reception forwarded by the SAM bridge (SAMv3).
|
||||
// V1/V2 TCP control socket reading is no longer supported.
|
||||
// All datagrams are authenticated with DATAGRAM2 replay protection.
|
||||
func (r *Datagram2Reader) receiveDatagram() (*Datagram2, error) {
|
||||
if err := r.validateReaderState(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// V3-only: Read from UDP connection
|
||||
r.session.mu.RLock()
|
||||
udpConn := r.session.udpConn
|
||||
r.session.mu.RUnlock()
|
||||
|
||||
if udpConn == nil {
|
||||
return nil, oops.Errorf("UDP connection not available (v3 UDP forwarding required)")
|
||||
}
|
||||
|
||||
return r.session.readDatagramFromUDP(udpConn)
|
||||
}
|
||||
|
||||
// validateReaderState checks if reader is closed before attempting expensive I/O operation.
|
||||
func (r *Datagram2Reader) validateReaderState() error {
|
||||
r.mu.RLock()
|
||||
isClosed := r.closed
|
||||
r.mu.RUnlock()
|
||||
|
||||
if isClosed {
|
||||
return oops.Errorf("reader is closing")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
366
datagram2/session.go
Normal file
366
datagram2/session.go
Normal file
@@ -0,0 +1,366 @@
|
||||
package datagram2
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-i2p/go-sam-go/common"
|
||||
"github.com/go-i2p/i2pkeys"
|
||||
"github.com/samber/oops"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// NewDatagram2Session creates a new datagram2 session with replay protection for UDP-like I2P messaging.
|
||||
// It initializes the session with the provided SAM connection, session ID, cryptographic keys,
|
||||
// and configuration options. The session automatically creates a UDP listener for receiving
|
||||
// forwarded datagrams per SAMv3 requirements.
|
||||
// Example usage: session, err := NewDatagram2Session(sam, "my-session", keys, []string{"inbound.length=1"})
|
||||
func NewDatagram2Session(sam *common.SAM, id string, keys i2pkeys.I2PKeys, options []string) (*Datagram2Session, error) {
|
||||
// Log session creation with detailed parameters for debugging
|
||||
logger := log.WithFields(logrus.Fields{
|
||||
"id": id,
|
||||
"style": "DATAGRAM2",
|
||||
"options": options,
|
||||
})
|
||||
logger.Debug("Creating new Datagram2Session with SAMv3 UDP forwarding")
|
||||
|
||||
// Create UDP listener for receiving forwarded datagrams (SAMv3 requirement)
|
||||
// The SAM bridge will forward incoming DATAGRAM2 messages to this local UDP port
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to resolve UDP address")
|
||||
return nil, oops.Errorf("failed to resolve UDP address: %w", err)
|
||||
}
|
||||
|
||||
udpConn, err := net.ListenUDP("udp", udpAddr)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to create UDP listener")
|
||||
return nil, oops.Errorf("failed to create UDP listener: %w", err)
|
||||
}
|
||||
|
||||
// Get the actual port assigned by the OS
|
||||
udpPort := udpConn.LocalAddr().(*net.UDPAddr).Port
|
||||
logger.WithField("udp_port", udpPort).Debug("Created UDP listener for datagram2 forwarding")
|
||||
|
||||
// Inject UDP forwarding parameters into session options (SAMv3 requirement)
|
||||
// PORT/HOST tell the SAM bridge where to forward received datagrams
|
||||
options = ensureUDPForwardingParameters(options, udpPort)
|
||||
|
||||
// CRITICAL: Use STYLE=DATAGRAM2 (not DATAGRAM) for replay protection
|
||||
// Create the base session using the common package for session management
|
||||
// This handles the underlying SAM protocol communication and session establishment
|
||||
session, err := sam.NewGenericSession("DATAGRAM2", id, keys, options)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to create generic session")
|
||||
udpConn.Close() // Clean up UDP listener on error
|
||||
return nil, oops.Errorf("failed to create datagram2 session: %w", err)
|
||||
}
|
||||
|
||||
// Ensure the session is of the correct type for datagram2 operations
|
||||
baseSession, ok := session.(*common.BaseSession)
|
||||
if !ok {
|
||||
logger.Error("Session is not a BaseSession")
|
||||
session.Close()
|
||||
udpConn.Close() // Clean up UDP listener on error
|
||||
return nil, oops.Errorf("invalid session type")
|
||||
}
|
||||
|
||||
// Initialize the datagram2 session with UDP forwarding enabled
|
||||
ds := &Datagram2Session{
|
||||
BaseSession: baseSession,
|
||||
sam: sam,
|
||||
options: options,
|
||||
udpConn: udpConn,
|
||||
udpEnabled: true, // Always true for SAMv3
|
||||
}
|
||||
|
||||
logger.Debug("Successfully created Datagram2Session with UDP forwarding and replay protection")
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
// ensureUDPForwardingParameters injects UDP forwarding parameters into session options if not already present.
|
||||
// This ensures SAMv3 UDP forwarding is configured with PORT and HOST parameters.
|
||||
// PORT/HOST specify where the SAM bridge should forward datagrams TO (the client's UDP listener).
|
||||
// sam.udp.port/sam.udp.host are NOT set here - they configure the SAM bridge's own UDP port (default 7655).
|
||||
// This is required for all datagram2 sessions in v3-only mode.
|
||||
func ensureUDPForwardingParameters(options []string, udpPort int) []string {
|
||||
updatedOptions := make([]string, 0, len(options)+2)
|
||||
|
||||
hasPort := false
|
||||
hasHost := false
|
||||
|
||||
// Check existing options
|
||||
for _, opt := range options {
|
||||
if strings.HasPrefix(opt, "PORT=") {
|
||||
hasPort = true
|
||||
} else if strings.HasPrefix(opt, "HOST=") {
|
||||
hasHost = true
|
||||
}
|
||||
updatedOptions = append(updatedOptions, opt)
|
||||
}
|
||||
|
||||
// Inject missing UDP forwarding parameters
|
||||
// PORT/HOST tell SAM bridge where to forward datagrams TO (our UDP listener)
|
||||
if !hasHost {
|
||||
updatedOptions = append(updatedOptions, "HOST=127.0.0.1")
|
||||
}
|
||||
if !hasPort {
|
||||
updatedOptions = append(updatedOptions, "PORT="+strconv.Itoa(udpPort))
|
||||
}
|
||||
|
||||
return updatedOptions
|
||||
}
|
||||
|
||||
// NewDatagram2SessionFromSubsession creates a Datagram2Session for a subsession that has already been
|
||||
// registered with a PRIMARY session using SESSION ADD. This constructor skips the session
|
||||
// creation step since the subsession is already registered with the SAM bridge.
|
||||
//
|
||||
// For PRIMARY datagram2 subsessions, UDP forwarding is mandatory (SAMv3 requirement).
|
||||
// The UDP connection must be provided for proper datagram reception.
|
||||
//
|
||||
// Example usage: session, err := NewDatagram2SessionFromSubsession(sam, "sub1", keys, options, udpConn)
|
||||
func NewDatagram2SessionFromSubsession(sam *common.SAM, id string, keys i2pkeys.I2PKeys, options []string, udpConn *net.UDPConn) (*Datagram2Session, error) {
|
||||
logger := log.WithFields(logrus.Fields{
|
||||
"id": id,
|
||||
"style": "DATAGRAM2",
|
||||
"options": options,
|
||||
"udp_enabled": udpConn != nil,
|
||||
})
|
||||
logger.Debug("Creating Datagram2Session from existing subsession with SAMv3 UDP forwarding")
|
||||
|
||||
// Validate UDP connection is provided (mandatory for SAMv3 datagram2 subsessions)
|
||||
if udpConn == nil {
|
||||
logger.Error("UDP connection is required for SAMv3 datagram2 subsessions")
|
||||
return nil, oops.Errorf("udp connection is required for datagram2 subsessions (v3 only)")
|
||||
}
|
||||
|
||||
// Create a BaseSession manually since the subsession is already registered via SESSION ADD
|
||||
// This bypasses SESSION CREATE and uses the existing registration
|
||||
baseSession, err := common.NewBaseSessionFromSubsession(sam, id, keys)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to create base session from subsession")
|
||||
return nil, oops.Errorf("failed to create datagram2 session from subsession: %w", err)
|
||||
}
|
||||
|
||||
ds := &Datagram2Session{
|
||||
BaseSession: baseSession,
|
||||
sam: sam,
|
||||
options: options,
|
||||
udpConn: udpConn,
|
||||
udpEnabled: true, // Always true for SAMv3
|
||||
}
|
||||
|
||||
logger.Debug("Successfully created Datagram2Session from subsession with UDP forwarding and replay protection")
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
// NewReader creates a Datagram2Reader for receiving authenticated datagrams with replay protection.
|
||||
// This method initializes a new reader with buffered channels for asynchronous datagram
|
||||
// reception. The reader must be started manually with receiveLoop() for continuous operation.
|
||||
// Example usage: reader := session.NewReader(); go reader.receiveLoop(); datagram, err := reader.ReceiveDatagram()
|
||||
func (s *Datagram2Session) NewReader() *Datagram2Reader {
|
||||
// Create reader with buffered channels for non-blocking operation
|
||||
// The buffer size of 10 prevents blocking when multiple datagrams arrive rapidly
|
||||
return &Datagram2Reader{
|
||||
session: s,
|
||||
recvChan: make(chan *Datagram2, 10), // Buffer for incoming datagrams
|
||||
errorChan: make(chan error, 1),
|
||||
closeChan: make(chan struct{}),
|
||||
doneChan: make(chan struct{}, 1),
|
||||
closed: false,
|
||||
loopStarted: false,
|
||||
mu: sync.RWMutex{},
|
||||
closeOnce: sync.Once{},
|
||||
}
|
||||
}
|
||||
|
||||
// NewWriter creates a Datagram2Writer for sending authenticated datagrams with replay protection.
|
||||
// This method initializes a new writer with a default timeout of 30 seconds for send operations.
|
||||
// The timeout can be customized using the SetTimeout method on the returned writer.
|
||||
// Example usage: writer := session.NewWriter().SetTimeout(60*time.Second); err := writer.SendDatagram(data, dest)
|
||||
func (s *Datagram2Session) NewWriter() *Datagram2Writer {
|
||||
// Initialize writer with default timeout for send operations
|
||||
// The timeout prevents indefinite blocking on send operations
|
||||
return &Datagram2Writer{
|
||||
session: s,
|
||||
timeout: 30, // Default timeout in seconds
|
||||
}
|
||||
}
|
||||
|
||||
// PacketConn returns a net.PacketConn interface for this datagram2 session.
|
||||
// This method provides compatibility with standard Go networking code by wrapping
|
||||
// the datagram2 session in a connection that implements the PacketConn interface.
|
||||
// The connection provides authenticated datagrams with replay protection.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// conn := session.PacketConn()
|
||||
// n, addr, err := conn.ReadFrom(buffer)
|
||||
// n, err = conn.WriteTo(data, destination)
|
||||
func (s *Datagram2Session) PacketConn() net.PacketConn {
|
||||
// Create a PacketConn wrapper with integrated reader and writer
|
||||
// This provides standard Go networking interface compliance
|
||||
return &Datagram2Conn{
|
||||
session: s,
|
||||
reader: s.NewReader(),
|
||||
writer: s.NewWriter(),
|
||||
}
|
||||
}
|
||||
|
||||
// SendDatagram sends an authenticated datagram with replay protection to the specified destination.
|
||||
// This is a convenience method that creates a temporary writer and sends the datagram
|
||||
// immediately. For multiple sends, it's more efficient to create a writer once and reuse it.
|
||||
// Example usage: err := session.SendDatagram([]byte("hello"), destinationAddr)
|
||||
func (s *Datagram2Session) SendDatagram(data []byte, dest i2pkeys.I2PAddr) error {
|
||||
// Use a temporary writer for one-time send operations
|
||||
// This simplifies the API for simple send operations
|
||||
return s.NewWriter().SendDatagram(data, dest)
|
||||
}
|
||||
|
||||
// ReceiveDatagram receives a single authenticated datagram from the I2P network.
|
||||
// This method is a convenience wrapper that performs a direct single read operation
|
||||
// without starting a continuous receive loop. For continuous reception,
|
||||
// use NewReader() and manage the reader lifecycle manually.
|
||||
// Example usage: datagram, err := session.ReceiveDatagram()
|
||||
func (s *Datagram2Session) ReceiveDatagram() (*Datagram2, error) {
|
||||
// ARCHITECTURAL FIX: Perform direct read for one-shot operations instead of
|
||||
// creating a reader with receive loop, which causes deadlocks due to RWMutex contention
|
||||
return s.readSingleDatagram()
|
||||
}
|
||||
|
||||
// readSingleDatagram performs a direct read from the UDP connection for one-shot datagram operations.
|
||||
// This method bypasses the reader infrastructure to avoid deadlocks when only one datagram is needed.
|
||||
// SAMv3 UDP forwarding mode only - reads from UDP connection where SAM bridge forwards datagrams.
|
||||
// V1/V2 TCP control socket reading is no longer supported.
|
||||
func (s *Datagram2Session) readSingleDatagram() (*Datagram2, error) {
|
||||
s.mu.RLock()
|
||||
udpConn := s.udpConn
|
||||
s.mu.RUnlock()
|
||||
|
||||
// V3-only: Always read from UDP connection
|
||||
if udpConn == nil {
|
||||
return nil, oops.Errorf("UDP connection not available (v3 UDP forwarding required)")
|
||||
}
|
||||
|
||||
return s.readDatagramFromUDP(udpConn)
|
||||
}
|
||||
|
||||
// readDatagramFromUDP reads a forwarded datagram2 message from the UDP connection.
|
||||
// Format per SAMv3.md: destination line, port lines, empty line, payload.
|
||||
func (s *Datagram2Session) readDatagramFromUDP(udpConn *net.UDPConn) (*Datagram2, error) {
|
||||
buffer := make([]byte, 65536) // Large buffer for UDP datagrams (I2P maximum)
|
||||
n, _, err := udpConn.ReadFromUDP(buffer)
|
||||
if err != nil {
|
||||
return nil, oops.Errorf("failed to read from UDP connection: %w", err)
|
||||
}
|
||||
|
||||
log.WithFields(logrus.Fields{
|
||||
"bytes_read": n,
|
||||
"style": "DATAGRAM2",
|
||||
}).Debug("Received UDP datagram2 message")
|
||||
|
||||
// Parse the UDP datagram format per SAMv3.md
|
||||
response := string(buffer[:n])
|
||||
|
||||
// Find the first newline - that's the end of the header line
|
||||
firstNewline := strings.Index(response, "\n")
|
||||
if firstNewline == -1 {
|
||||
return nil, oops.Errorf("invalid UDP datagram2 format: no newline found")
|
||||
}
|
||||
|
||||
// Line 1: Source destination (base64, authenticated) followed by optional FROM_PORT=nnn TO_PORT=nnn
|
||||
headerLine := strings.TrimSpace(response[:firstNewline])
|
||||
|
||||
if headerLine == "" {
|
||||
return nil, oops.Errorf("empty header line in UDP datagram2")
|
||||
}
|
||||
|
||||
// Parse the header line to extract the authenticated source destination
|
||||
// Format: "$destination FROM_PORT=nnn TO_PORT=nnn"
|
||||
// We need to split on space and take the first part as the destination
|
||||
parts := strings.Fields(headerLine)
|
||||
if len(parts) == 0 {
|
||||
return nil, oops.Errorf("empty header line in UDP datagram2")
|
||||
}
|
||||
|
||||
source := parts[0] // First field is the authenticated source destination
|
||||
// Remaining parts are FROM_PORT and TO_PORT which we ignore for now
|
||||
|
||||
// Everything after the first newline is the payload
|
||||
data := response[firstNewline+1:]
|
||||
|
||||
if data == "" {
|
||||
return nil, oops.Errorf("no data in UDP datagram2")
|
||||
}
|
||||
|
||||
return s.createDatagram(source, data)
|
||||
}
|
||||
|
||||
// createDatagram constructs the final Datagram2 from parsed authenticated source and data.
|
||||
func (s *Datagram2Session) createDatagram(source, data string) (*Datagram2, error) {
|
||||
sourceAddr, err := i2pkeys.NewI2PAddrFromString(source)
|
||||
if err != nil {
|
||||
return nil, oops.Errorf("failed to parse authenticated source address: %w", err)
|
||||
}
|
||||
|
||||
// Data is already raw bytes, not base64 encoded
|
||||
datagram := &Datagram2{
|
||||
Data: []byte(data),
|
||||
Source: sourceAddr, // Authenticated by I2P router with replay protection
|
||||
Local: s.Addr(),
|
||||
}
|
||||
|
||||
return datagram, nil
|
||||
}
|
||||
|
||||
// Close closes the datagram2 session and all associated resources.
|
||||
// This method safely terminates the session, closes the UDP listener and underlying connection,
|
||||
// and cleans up any background goroutines. It's safe to call multiple times.
|
||||
// Example usage: defer session.Close()
|
||||
func (s *Datagram2Session) Close() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.closed {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Log session closure for debugging and monitoring
|
||||
logger := log.WithFields(logrus.Fields{
|
||||
"id": s.ID(),
|
||||
"style": "DATAGRAM2",
|
||||
})
|
||||
logger.Debug("Closing Datagram2Session")
|
||||
|
||||
s.closed = true
|
||||
|
||||
// Close the UDP listener for v3 forwarding
|
||||
if s.udpConn != nil {
|
||||
if err := s.udpConn.Close(); err != nil {
|
||||
logger.WithError(err).Warn("Failed to close UDP listener")
|
||||
// Continue with base session closure even if UDP close fails
|
||||
}
|
||||
}
|
||||
|
||||
// Close the underlying base session to terminate SAM communication
|
||||
// This ensures proper cleanup of the I2P connection
|
||||
if err := s.BaseSession.Close(); err != nil {
|
||||
logger.WithError(err).Error("Failed to close base session")
|
||||
return oops.Errorf("failed to close datagram2 session: %w", err)
|
||||
}
|
||||
|
||||
logger.Debug("Successfully closed Datagram2Session")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Addr returns the I2P address of this datagram2 session.
|
||||
// This address represents the session's identity on the I2P network and can be
|
||||
// used by other nodes to send authenticated datagrams with replay protection to this session.
|
||||
// The address is derived from the session's cryptographic keys.
|
||||
// Example usage: myAddr := session.Addr(); fmt.Println("My I2P address:", myAddr.Base32())
|
||||
func (s *Datagram2Session) Addr() i2pkeys.I2PAddr {
|
||||
// Return the I2P address derived from the session's cryptographic keys
|
||||
return s.Keys().Addr()
|
||||
}
|
||||
563
datagram2/session_test.go
Normal file
563
datagram2/session_test.go
Normal file
@@ -0,0 +1,563 @@
|
||||
package datagram2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-i2p/go-sam-go/common"
|
||||
"github.com/go-i2p/i2pkeys"
|
||||
)
|
||||
|
||||
const testSAMAddr = "127.0.0.1:7656"
|
||||
|
||||
func setupTestSAM(t *testing.T) (*common.SAM, i2pkeys.I2PKeys) {
|
||||
t.Helper()
|
||||
|
||||
sam, err := common.NewSAM(testSAMAddr)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create SAM connection: %v", err)
|
||||
}
|
||||
|
||||
keys, err := sam.NewKeys()
|
||||
if err != nil {
|
||||
sam.Close()
|
||||
t.Fatalf("Failed to generate keys: %v", err)
|
||||
}
|
||||
|
||||
return sam, keys
|
||||
}
|
||||
|
||||
// generateUniqueSessionID creates a unique session ID to prevent conflicts during concurrent test execution.
|
||||
// This ensures test isolation when multiple tests run simultaneously (e.g., during race detection).
|
||||
// DATAGRAM2 sessions use a unique prefix to distinguish from legacy DATAGRAM sessions.
|
||||
func generateUniqueSessionID(testName string) string {
|
||||
// Use timestamp (nanoseconds) and random number to ensure uniqueness across concurrent executions
|
||||
timestamp := time.Now().UnixNano()
|
||||
random := rand.Intn(99999)
|
||||
return fmt.Sprintf("dg2_%s_%d_%05d", testName, timestamp, random)
|
||||
}
|
||||
|
||||
func TestNewDatagram2Session(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping I2P integration test in short mode")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
idBase string
|
||||
options []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "basic datagram2 session creation",
|
||||
idBase: "test_session",
|
||||
options: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "session with custom options",
|
||||
idBase: "test_with_opts",
|
||||
options: []string{"inbound.length=1", "outbound.length=1"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "session with small tunnel config",
|
||||
idBase: "test_small",
|
||||
options: []string{
|
||||
"inbound.length=1",
|
||||
"outbound.length=1",
|
||||
"inbound.lengthVariance=0",
|
||||
"outbound.lengthVariance=0",
|
||||
"inbound.quantity=1",
|
||||
"outbound.quantity=1",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "session with Ed25519 signature",
|
||||
idBase: "test_ed25519",
|
||||
options: []string{
|
||||
"inbound.length=0",
|
||||
"outbound.length=0",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sam, keys := setupTestSAM(t)
|
||||
defer sam.Close()
|
||||
|
||||
// Generate unique session ID to prevent conflicts during concurrent test execution
|
||||
sessionID := generateUniqueSessionID(tt.idBase)
|
||||
|
||||
t.Logf("Creating DATAGRAM2 session with ID: %s", sessionID)
|
||||
t.Logf("Note: I2P tunnel establishment can take 2-5 minutes")
|
||||
|
||||
session, err := NewDatagram2Session(sam, sessionID, keys, tt.options)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NewDatagram2Session() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
// Verify session properties
|
||||
if session.ID() != sessionID {
|
||||
t.Errorf("Session ID = %v, want %v", session.ID(), sessionID)
|
||||
}
|
||||
|
||||
if session.Keys().Addr().Base32() != keys.Addr().Base32() {
|
||||
t.Error("Session keys don't match provided keys")
|
||||
}
|
||||
|
||||
addr := session.Addr()
|
||||
if addr.Base32() == "" {
|
||||
t.Error("Session address is empty")
|
||||
}
|
||||
|
||||
// Verify UDP forwarding is enabled (DATAGRAM2 requires this)
|
||||
if !session.udpEnabled {
|
||||
t.Error("UDP forwarding should be enabled for DATAGRAM2")
|
||||
}
|
||||
|
||||
if session.udpConn == nil {
|
||||
t.Error("UDP connection should be initialized for DATAGRAM2")
|
||||
}
|
||||
|
||||
t.Logf("Session created successfully: %s", addr.Base32())
|
||||
|
||||
// Clean up
|
||||
if err := session.Close(); err != nil {
|
||||
t.Errorf("Failed to close session: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatagram2Session_Close(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping I2P integration test in short mode")
|
||||
}
|
||||
|
||||
sam, keys := setupTestSAM(t)
|
||||
defer sam.Close()
|
||||
|
||||
// Generate unique session ID to prevent conflicts during concurrent test execution
|
||||
sessionID := generateUniqueSessionID("test_close")
|
||||
|
||||
t.Logf("Creating DATAGRAM2 session for close test")
|
||||
session, err := NewDatagram2Session(sam, sessionID, keys, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session: %v", err)
|
||||
}
|
||||
|
||||
// Close the session
|
||||
err = session.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Close() error = %v", err)
|
||||
}
|
||||
|
||||
// Closing again should not error (idempotent)
|
||||
err = session.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Second Close() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify session is marked as closed
|
||||
if !session.closed {
|
||||
t.Error("Session should be marked as closed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatagram2Session_Addr(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping I2P integration test in short mode")
|
||||
}
|
||||
|
||||
sam, keys := setupTestSAM(t)
|
||||
defer sam.Close()
|
||||
|
||||
// Generate unique session ID to prevent conflicts during concurrent test execution
|
||||
sessionID := generateUniqueSessionID("test_addr")
|
||||
|
||||
session, err := NewDatagram2Session(sam, sessionID, keys, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session: %v", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
addr := session.Addr()
|
||||
expectedAddr := keys.Addr()
|
||||
|
||||
if addr.Base32() != expectedAddr.Base32() {
|
||||
t.Errorf("Addr() = %v, want %v", addr.Base32(), expectedAddr.Base32())
|
||||
}
|
||||
|
||||
if addr.Base64() != expectedAddr.Base64() {
|
||||
t.Error("Base64 addresses don't match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatagram2Session_NewReader(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping I2P integration test in short mode")
|
||||
}
|
||||
|
||||
sam, keys := setupTestSAM(t)
|
||||
defer sam.Close()
|
||||
|
||||
// Generate unique session ID to prevent conflicts during concurrent test execution
|
||||
sessionID := generateUniqueSessionID("test_reader")
|
||||
|
||||
session, err := NewDatagram2Session(sam, sessionID, keys, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session: %v", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
reader := session.NewReader()
|
||||
if reader == nil {
|
||||
t.Error("NewReader() returned nil")
|
||||
}
|
||||
|
||||
if reader.session != session {
|
||||
t.Error("Reader session reference is incorrect")
|
||||
}
|
||||
|
||||
// Verify channels are initialized
|
||||
if reader.recvChan == nil {
|
||||
t.Error("Reader recvChan is nil")
|
||||
}
|
||||
if reader.errorChan == nil {
|
||||
t.Error("Reader errorChan is nil")
|
||||
}
|
||||
if reader.closeChan == nil {
|
||||
t.Error("Reader closeChan is nil")
|
||||
}
|
||||
if reader.doneChan == nil {
|
||||
t.Error("Reader doneChan is nil")
|
||||
}
|
||||
|
||||
// Clean up reader
|
||||
if err := reader.Close(); err != nil {
|
||||
t.Errorf("Failed to close reader: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatagram2Session_NewWriter(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping I2P integration test in short mode")
|
||||
}
|
||||
|
||||
sam, keys := setupTestSAM(t)
|
||||
defer sam.Close()
|
||||
|
||||
// Generate unique session ID to prevent conflicts during concurrent test execution
|
||||
sessionID := generateUniqueSessionID("test_writer")
|
||||
|
||||
session, err := NewDatagram2Session(sam, sessionID, keys, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session: %v", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
writer := session.NewWriter()
|
||||
if writer == nil {
|
||||
t.Error("NewWriter() returned nil")
|
||||
}
|
||||
|
||||
if writer.session != session {
|
||||
t.Error("Writer session reference is incorrect")
|
||||
}
|
||||
|
||||
if writer.timeout != 30 {
|
||||
t.Errorf("Writer timeout = %v, want 30", writer.timeout)
|
||||
}
|
||||
|
||||
// Test method chaining with SetTimeout
|
||||
writer2 := writer.SetTimeout(60 * time.Second)
|
||||
if writer2 != writer {
|
||||
t.Error("SetTimeout should return the same writer instance")
|
||||
}
|
||||
if writer.timeout != 60*time.Second {
|
||||
t.Errorf("Writer timeout = %v, want 60s", writer.timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatagram2Session_PacketConn(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping I2P integration test in short mode")
|
||||
}
|
||||
|
||||
sam, keys := setupTestSAM(t)
|
||||
defer sam.Close()
|
||||
|
||||
// Generate unique session ID to prevent conflicts during concurrent test execution
|
||||
sessionID := generateUniqueSessionID("test_packetconn")
|
||||
|
||||
session, err := NewDatagram2Session(sam, sessionID, keys, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session: %v", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
conn := session.PacketConn()
|
||||
if conn == nil {
|
||||
t.Error("PacketConn() returned nil")
|
||||
}
|
||||
|
||||
datagram2Conn, ok := conn.(*Datagram2Conn)
|
||||
if !ok {
|
||||
t.Error("PacketConn() did not return a Datagram2Conn")
|
||||
}
|
||||
|
||||
if datagram2Conn.session != session {
|
||||
t.Error("Datagram2Conn session reference is incorrect")
|
||||
}
|
||||
|
||||
if datagram2Conn.reader == nil {
|
||||
t.Error("Datagram2Conn reader is nil")
|
||||
}
|
||||
|
||||
if datagram2Conn.writer == nil {
|
||||
t.Error("Datagram2Conn writer is nil")
|
||||
}
|
||||
|
||||
// Test LocalAddr
|
||||
localAddr := conn.LocalAddr()
|
||||
if localAddr == nil {
|
||||
t.Error("LocalAddr() returned nil")
|
||||
}
|
||||
|
||||
if localAddr.Network() != "datagram2" {
|
||||
t.Errorf("LocalAddr().Network() = %v, want datagram2", localAddr.Network())
|
||||
}
|
||||
|
||||
// Clean up
|
||||
if err := conn.Close(); err != nil {
|
||||
t.Errorf("Failed to close PacketConn: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatagram2Addr_Network(t *testing.T) {
|
||||
addr := &Datagram2Addr{}
|
||||
if addr.Network() != "datagram2" {
|
||||
t.Errorf("Network() = %v, want datagram2", addr.Network())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatagram2Addr_String(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping I2P integration test in short mode")
|
||||
}
|
||||
|
||||
sam, keys := setupTestSAM(t)
|
||||
defer sam.Close()
|
||||
|
||||
addr := &Datagram2Addr{addr: keys.Addr()}
|
||||
expected := keys.Addr().Base32()
|
||||
|
||||
if addr.String() != expected {
|
||||
t.Errorf("String() = %v, want %v", addr.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureUDPForwardingParameters(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options []string
|
||||
udpPort int
|
||||
wantHost bool
|
||||
wantPort bool
|
||||
}{
|
||||
{
|
||||
name: "empty options",
|
||||
options: []string{},
|
||||
udpPort: 12345,
|
||||
wantHost: true,
|
||||
wantPort: true,
|
||||
},
|
||||
{
|
||||
name: "existing HOST",
|
||||
options: []string{"HOST=192.168.1.1"},
|
||||
udpPort: 12345,
|
||||
wantHost: true,
|
||||
wantPort: true,
|
||||
},
|
||||
{
|
||||
name: "existing PORT",
|
||||
options: []string{"PORT=9999"},
|
||||
udpPort: 12345,
|
||||
wantHost: true,
|
||||
wantPort: true,
|
||||
},
|
||||
{
|
||||
name: "both existing",
|
||||
options: []string{"HOST=192.168.1.1", "PORT=9999"},
|
||||
udpPort: 12345,
|
||||
wantHost: true,
|
||||
wantPort: true,
|
||||
},
|
||||
{
|
||||
name: "with other options",
|
||||
options: []string{"inbound.length=3", "outbound.length=3"},
|
||||
udpPort: 54321,
|
||||
wantHost: true,
|
||||
wantPort: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ensureUDPForwardingParameters(tt.options, tt.udpPort)
|
||||
|
||||
hasHost := false
|
||||
hasPort := false
|
||||
|
||||
for _, opt := range result {
|
||||
if len(opt) >= 5 && opt[:5] == "HOST=" {
|
||||
hasHost = true
|
||||
}
|
||||
if len(opt) >= 5 && opt[:5] == "PORT=" {
|
||||
hasPort = true
|
||||
}
|
||||
}
|
||||
|
||||
if hasHost != tt.wantHost {
|
||||
t.Errorf("Has HOST = %v, want %v", hasHost, tt.wantHost)
|
||||
}
|
||||
|
||||
if hasPort != tt.wantPort {
|
||||
t.Errorf("Has PORT = %v, want %v", hasPort, tt.wantPort)
|
||||
}
|
||||
|
||||
// Verify original options are preserved
|
||||
for _, orig := range tt.options {
|
||||
found := false
|
||||
for _, res := range result {
|
||||
if res == orig {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// Only check non-HOST/PORT options
|
||||
if len(orig) < 5 || (orig[:5] != "HOST=" && orig[:5] != "PORT=") {
|
||||
if !found {
|
||||
t.Errorf("Original option %q not found in result", orig)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatagram2Reader_Close(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping I2P integration test in short mode")
|
||||
}
|
||||
|
||||
sam, keys := setupTestSAM(t)
|
||||
defer sam.Close()
|
||||
|
||||
sessionID := generateUniqueSessionID("test_reader_close")
|
||||
|
||||
session, err := NewDatagram2Session(sam, sessionID, keys, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session: %v", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
reader := session.NewReader()
|
||||
|
||||
// Close the reader
|
||||
err = reader.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Close() error = %v", err)
|
||||
}
|
||||
|
||||
// Closing again should not error (idempotent)
|
||||
err = reader.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Second Close() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify reader is marked as closed
|
||||
reader.mu.RLock()
|
||||
closed := reader.closed
|
||||
reader.mu.RUnlock()
|
||||
|
||||
if !closed {
|
||||
t.Error("Reader should be marked as closed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatagram2Writer_SetTimeout(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping I2P integration test in short mode")
|
||||
}
|
||||
|
||||
sam, keys := setupTestSAM(t)
|
||||
defer sam.Close()
|
||||
|
||||
sessionID := generateUniqueSessionID("test_writer_timeout")
|
||||
|
||||
session, err := NewDatagram2Session(sam, sessionID, keys, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session: %v", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
writer := session.NewWriter()
|
||||
|
||||
// Test various timeout values
|
||||
timeouts := []time.Duration{
|
||||
10 * time.Second,
|
||||
30 * time.Second,
|
||||
60 * time.Second,
|
||||
2 * time.Minute,
|
||||
}
|
||||
|
||||
for _, timeout := range timeouts {
|
||||
returned := writer.SetTimeout(timeout)
|
||||
if returned != writer {
|
||||
t.Error("SetTimeout should return the same writer for chaining")
|
||||
}
|
||||
if writer.timeout != timeout {
|
||||
t.Errorf("SetTimeout(%v): timeout = %v, want %v", timeout, writer.timeout, timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkNewDatagram2Session measures session creation performance
|
||||
// Note: This requires an I2P router and can be slow (2-5 minutes per iteration)
|
||||
func BenchmarkNewDatagram2Session(b *testing.B) {
|
||||
if testing.Short() {
|
||||
b.Skip("Skipping I2P integration benchmark in short mode")
|
||||
}
|
||||
|
||||
sam, err := common.NewSAM(testSAMAddr)
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to create SAM connection: %v", err)
|
||||
}
|
||||
defer sam.Close()
|
||||
|
||||
keys, err := sam.NewKeys()
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to generate keys: %v", err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
sessionID := generateUniqueSessionID(fmt.Sprintf("bench_%d", i))
|
||||
session, err := NewDatagram2Session(sam, sessionID, keys, nil)
|
||||
if err != nil {
|
||||
b.Errorf("Failed to create session: %v", err)
|
||||
continue
|
||||
}
|
||||
session.Close()
|
||||
}
|
||||
}
|
||||
158
datagram2/types.go
Normal file
158
datagram2/types.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package datagram2
|
||||
|
||||
import (
|
||||
"net"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-i2p/go-sam-go/common"
|
||||
"github.com/go-i2p/i2pkeys"
|
||||
)
|
||||
|
||||
// Datagram2Session represents an authenticated datagram2 session with replay protection.
|
||||
// This session type provides UDP-like messaging capabilities through the I2P network with
|
||||
// enhanced security features compared to legacy DATAGRAM sessions. DATAGRAM2 provides
|
||||
// replay protection and offline signature support, making it the recommended format for
|
||||
// new applications that don't require backward compatibility.
|
||||
//
|
||||
// The session manages the underlying I2P connection and provides methods for creating readers
|
||||
// and writers. For SAMv3 mode, it uses UDP forwarding where datagrams are received via a
|
||||
// local UDP socket that the SAM bridge forwards to.
|
||||
//
|
||||
// I2P Timing Considerations:
|
||||
// - Session creation: 2-5 minutes for tunnel establishment
|
||||
// - Message delivery: Variable latency (network-dependent)
|
||||
// - Use generous timeouts and retry logic with exponential backoff
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
// defer cancel()
|
||||
// session, err := NewDatagram2Session(sam, "my-session", keys, options)
|
||||
type Datagram2Session struct {
|
||||
*common.BaseSession
|
||||
sam *common.SAM
|
||||
options []string
|
||||
mu sync.RWMutex
|
||||
closed bool
|
||||
udpConn *net.UDPConn // UDP connection for receiving forwarded datagrams (SAMv3 mode)
|
||||
udpEnabled bool // Whether UDP forwarding is enabled (always true for SAMv3)
|
||||
}
|
||||
|
||||
// Datagram2Reader handles incoming authenticated datagram2 reception from the I2P network.
|
||||
// It provides asynchronous datagram reception through buffered channels, allowing applications
|
||||
// to receive datagrams without blocking. The reader manages its own goroutine for continuous
|
||||
// message processing and provides thread-safe access to received datagrams.
|
||||
//
|
||||
// All datagrams received are authenticated by the I2P router with signature verification
|
||||
// performed internally. DATAGRAM2 provides replay protection, preventing replay attacks
|
||||
// that are possible with legacy DATAGRAM sessions.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// reader := session.NewReader()
|
||||
// for {
|
||||
// datagram, err := reader.ReceiveDatagram()
|
||||
// if err != nil {
|
||||
// // Handle error
|
||||
// }
|
||||
// // Process datagram.Data from datagram.Source
|
||||
// }
|
||||
type Datagram2Reader struct {
|
||||
session *Datagram2Session
|
||||
recvChan chan *Datagram2
|
||||
errorChan chan error
|
||||
closeChan chan struct{}
|
||||
doneChan chan struct{}
|
||||
closed bool
|
||||
loopStarted bool
|
||||
mu sync.RWMutex
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
// Datagram2Writer handles outgoing authenticated datagram2 transmission to I2P destinations.
|
||||
// It provides methods for sending datagrams with configurable timeouts and handles
|
||||
// the underlying SAM protocol communication for message delivery. The writer supports
|
||||
// method chaining for configuration and provides error handling for send operations.
|
||||
//
|
||||
// All datagrams are authenticated and provide replay protection. Maximum datagram size
|
||||
// is 31744 bytes total (including headers), with 11 KB recommended for best reliability.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// writer := session.NewWriter().SetTimeout(30*time.Second)
|
||||
// err := writer.SendDatagram(data, destination)
|
||||
type Datagram2Writer struct {
|
||||
session *Datagram2Session
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// Datagram2 represents an authenticated I2P datagram2 message with replay protection.
|
||||
// It encapsulates the payload data along with source and destination addressing details,
|
||||
// providing all necessary information for processing received datagrams. The structure
|
||||
// includes both the raw data bytes and I2P address information for routing.
|
||||
//
|
||||
// All DATAGRAM2 messages are authenticated by the I2P router, with signature verification
|
||||
// performed internally. Replay protection prevents replay attacks that affect legacy DATAGRAM.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// if datagram.Source.Base32() == expectedSender {
|
||||
// processData(datagram.Data)
|
||||
// }
|
||||
type Datagram2 struct {
|
||||
Data []byte // Raw datagram payload (up to ~31KB)
|
||||
Source i2pkeys.I2PAddr // Authenticated source destination
|
||||
Local i2pkeys.I2PAddr // Local destination (this session)
|
||||
}
|
||||
|
||||
// Datagram2Addr implements net.Addr interface for I2P datagram2 addresses.
|
||||
// This type provides standard Go networking address representation for I2P destinations,
|
||||
// allowing seamless integration with existing Go networking code that expects net.Addr.
|
||||
// The address wraps an I2P address and provides string representation and network type identification.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// addr := &Datagram2Addr{addr: destination}
|
||||
// fmt.Println(addr.Network(), addr.String())
|
||||
type Datagram2Addr struct {
|
||||
addr i2pkeys.I2PAddr
|
||||
}
|
||||
|
||||
// Network returns the network type for I2P datagram2 addresses.
|
||||
// This implements the net.Addr interface by returning "datagram2" as the network type.
|
||||
func (a *Datagram2Addr) Network() string {
|
||||
return "datagram2"
|
||||
}
|
||||
|
||||
// String returns the string representation of the I2P address.
|
||||
// This implements the net.Addr interface by returning the base32 address representation,
|
||||
// which is suitable for display and logging purposes.
|
||||
func (a *Datagram2Addr) String() string {
|
||||
return a.addr.Base32()
|
||||
}
|
||||
|
||||
// Datagram2Conn implements net.PacketConn interface for I2P datagram2 communication.
|
||||
// This type provides compatibility with standard Go networking patterns by wrapping
|
||||
// datagram2 session functionality in a familiar PacketConn interface. It manages
|
||||
// internal readers and writers while providing standard connection operations.
|
||||
//
|
||||
// The connection provides thread-safe concurrent access to I2P datagram2 operations
|
||||
// and properly handles cleanup on close. All datagrams are authenticated with replay
|
||||
// protection provided by the DATAGRAM2 format.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// conn := session.PacketConn()
|
||||
// n, addr, err := conn.ReadFrom(buffer)
|
||||
// n, err = conn.WriteTo(data, destination)
|
||||
type Datagram2Conn struct {
|
||||
session *Datagram2Session
|
||||
reader *Datagram2Reader
|
||||
writer *Datagram2Writer
|
||||
remoteAddr *i2pkeys.I2PAddr
|
||||
mu sync.RWMutex
|
||||
closed bool
|
||||
cleanup runtime.Cleanup
|
||||
}
|
||||
15
datagram2/types_test.go
Normal file
15
datagram2/types_test.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package datagram2
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/go-i2p/go-sam-go/common"
|
||||
)
|
||||
|
||||
// Compile-time interface checks to ensure types implement expected interfaces
|
||||
var (
|
||||
ds common.Session = &Datagram2Session{}
|
||||
dc net.PacketConn = &Datagram2Conn{}
|
||||
dcc net.Conn = &Datagram2Conn{}
|
||||
da net.Addr = &Datagram2Addr{}
|
||||
)
|
||||
94
datagram2/write.go
Normal file
94
datagram2/write.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package datagram2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/go-i2p/i2pkeys"
|
||||
"github.com/samber/oops"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// SetTimeout sets the timeout for datagram2 write operations.
|
||||
// This method configures the maximum time to wait for authenticated datagram send operations
|
||||
// to complete. Returns the writer instance for method chaining convenience.
|
||||
// Example usage: writer.SetTimeout(30*time.Second).SendDatagram(data, destination)
|
||||
func (w *Datagram2Writer) SetTimeout(timeout time.Duration) *Datagram2Writer {
|
||||
// Configure the timeout for send operations to prevent indefinite blocking
|
||||
w.timeout = timeout
|
||||
return w
|
||||
}
|
||||
|
||||
// SendDatagram sends an authenticated datagram with replay protection to the specified I2P destination.
|
||||
// It uses the SAMv3 UDP approach by sending to port 7655 with DATAGRAM2 format.
|
||||
// Maximum datagram size is 31744 bytes (11 KB recommended for reliability).
|
||||
// Example usage: err := writer.SendDatagram([]byte("hello world"), destinationAddr)
|
||||
func (w *Datagram2Writer) SendDatagram(data []byte, dest i2pkeys.I2PAddr) error {
|
||||
// Check if the session is closed before attempting to send
|
||||
w.session.mu.RLock()
|
||||
if w.session.closed {
|
||||
w.session.mu.RUnlock()
|
||||
return oops.Errorf("session is closed")
|
||||
}
|
||||
w.session.mu.RUnlock()
|
||||
|
||||
// Create detailed logging context for debugging send operations
|
||||
logger := log.WithFields(logrus.Fields{
|
||||
"session_id": w.session.ID(),
|
||||
"destination": dest.Base32(),
|
||||
"size": len(data),
|
||||
"style": "DATAGRAM2",
|
||||
})
|
||||
logger.Debug("Sending datagram2 message via UDP socket")
|
||||
|
||||
// Use UDP socket approach (SAMv3 method) to send DATAGRAM2 messages
|
||||
// Connect to SAM's UDP port (default 7655) for datagram2 transmission
|
||||
samHost := w.session.sam.SAMEmit.I2PConfig.SamHost
|
||||
if samHost == "" {
|
||||
samHost = "127.0.0.1" // Default SAM host
|
||||
}
|
||||
samUDPPort := "7655" // Default SAM UDP port for datagram2 transmission
|
||||
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(samHost, samUDPPort))
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to resolve SAM UDP address")
|
||||
return oops.Errorf("failed to resolve SAM UDP address: %w", err)
|
||||
}
|
||||
|
||||
udpConn, err := net.DialUDP("udp", nil, udpAddr)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to connect to SAM UDP port")
|
||||
return oops.Errorf("failed to connect to SAM UDP port: %w", err)
|
||||
}
|
||||
defer udpConn.Close()
|
||||
|
||||
// Construct the SAMv3 UDP datagram2 format:
|
||||
// First line: "3.3 <session_id> <destination> [options]\n"
|
||||
// Remaining data: the actual message payload (authenticated with replay protection)
|
||||
sessionID := w.session.ID()
|
||||
destination := dest.Base64()
|
||||
|
||||
// Create the header line according to SAMv3 specification
|
||||
// The SAM bridge handles DATAGRAM2 authentication and replay protection internally
|
||||
headerLine := fmt.Sprintf("3.3 %s %s\n", sessionID, destination)
|
||||
|
||||
// Combine header and data into final UDP packet
|
||||
udpMessage := append([]byte(headerLine), data...)
|
||||
|
||||
logger.WithFields(logrus.Fields{
|
||||
"header": headerLine,
|
||||
"total_size": len(udpMessage),
|
||||
}).Debug("Sending UDP datagram2 to SAM")
|
||||
|
||||
// Send the datagram2 message via UDP to SAM bridge
|
||||
// The SAM bridge will add authentication and replay protection
|
||||
_, err = udpConn.Write(udpMessage)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to send UDP datagram2 to SAM")
|
||||
return oops.Errorf("failed to send UDP datagram2 to SAM: %w", err)
|
||||
}
|
||||
|
||||
logger.Debug("Successfully sent datagram2 message via UDP")
|
||||
return nil
|
||||
}
|
||||
1204
datagram3/DOC.md
1204
datagram3/DOC.md
File diff suppressed because it is too large
Load Diff
20
datagram3/README.md
Normal file
20
datagram3/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# go-sam-go/datagram3
|
||||
|
||||
Datagram3 library for unauthenticated UDP-like messaging with hash-based sources over I2P using the SAMv3 protocol.
|
||||
|
||||
## Installation
|
||||
|
||||
Install using Go modules with the package path `github.com/go-i2p/go-sam-go/datagram3`.
|
||||
|
||||
## Usage
|
||||
|
||||
The package provides unauthenticated datagram messaging over I2P networks. Datagram3Session manages the session lifecycle, Datagram3Reader handles incoming datagrams, Datagram3Writer sends outgoing datagrams, and Datagram3Conn implements the standard net.PacketConn interface.
|
||||
|
||||
Create sessions using NewDatagram3Session(), send messages with SendDatagram3(), and receive messages using ReceiveDatagram3(). Sources use 32-byte hashes that are not cryptographically authenticated. Implement application-layer authentication for security. Requires I2P router with DATAGRAM3 support.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- github.com/go-i2p/go-sam-go/common - Core SAM protocol implementation
|
||||
- github.com/go-i2p/i2pkeys - I2P cryptographic key handling
|
||||
- github.com/sirupsen/logrus - Structured logging
|
||||
- github.com/samber/oops - Enhanced error handling
|
||||
194
datagram3/SAM.go
Normal file
194
datagram3/SAM.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package datagram3
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/go-i2p/go-sam-go/common"
|
||||
"github.com/go-i2p/i2pkeys"
|
||||
"github.com/samber/oops"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// SAM wraps common.SAM to provide datagram3-specific functionality for I2P messaging.
|
||||
// This type extends the base SAM functionality with methods specifically designed for
|
||||
// DATAGRAM3 communication, providing repliable but UNAUTHENTICATED datagrams with hash-based
|
||||
// source identification.
|
||||
//
|
||||
// ⚠️ SECURITY WARNING: DATAGRAM3 sources are NOT authenticated and can be spoofed!
|
||||
// ⚠️ Do not trust source addresses without additional application-level authentication.
|
||||
// ⚠️ If you need authenticated sources, use DATAGRAM2 instead.
|
||||
//
|
||||
// DATAGRAM3 uses 32-byte hashes instead of full destinations for source identification,
|
||||
// reducing overhead at the cost of source verification. Applications requiring source
|
||||
// authentication MUST implement their own authentication layer.
|
||||
//
|
||||
// Example usage: sam := &SAM{SAM: baseSAM}; session, err := sam.NewDatagram3Session(id, keys, options)
|
||||
type SAM struct {
|
||||
*common.SAM
|
||||
}
|
||||
|
||||
// NewDatagram3Session creates a new repliable but UNAUTHENTICATED datagram3 session.
|
||||
// This method establishes a new DATAGRAM3 session for UDP-like messaging over I2P with
|
||||
// hash-based source identification. Session creation can take 2-5 minutes due to I2P tunnel
|
||||
// establishment, so generous timeouts are recommended.
|
||||
//
|
||||
// ⚠️ SECURITY WARNING: DATAGRAM3 sources are NOT authenticated and can be spoofed!
|
||||
// ⚠️ Applications requiring source authentication should use DATAGRAM2 instead.
|
||||
//
|
||||
// DATAGRAM3 provides repliable datagrams with minimal overhead by using hash-based source
|
||||
// identification instead of full authenticated destinations. Received datagrams contain a
|
||||
// 32-byte hash that must be resolved via NAMING LOOKUP to reply. The session maintains
|
||||
// a cache to avoid repeated lookups.
|
||||
//
|
||||
// Key differences from DATAGRAM and DATAGRAM2:
|
||||
// - Repliable: Can reply to sender (like DATAGRAM/DATAGRAM2)
|
||||
// - Unauthenticated: Source is NOT verified (unlike DATAGRAM/DATAGRAM2)
|
||||
// - Hash-based: Source is 32-byte hash, NOT full destination
|
||||
// - Lower overhead: No signature verification required
|
||||
// - Reply requires NAMING LOOKUP: Hash must be resolved to full destination
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
// defer cancel()
|
||||
// session, err := sam.NewDatagram3Session("my-session", keys, []string{"inbound.length=1"})
|
||||
func (s *SAM) NewDatagram3Session(id string, keys i2pkeys.I2PKeys, options []string) (*Datagram3Session, error) {
|
||||
// Delegate to the package-level function for session creation
|
||||
// This provides consistency with the package API design
|
||||
return NewDatagram3Session(s.SAM, id, keys, options)
|
||||
}
|
||||
|
||||
// NewDatagram3SessionWithSignature creates a new datagram3 session with custom signature type.
|
||||
// This method allows specifying a custom cryptographic signature type for the session,
|
||||
// enabling advanced security configurations beyond the default Ed25519 algorithm.
|
||||
// DATAGRAM3 supports offline signatures, allowing pre-signed destinations for enhanced
|
||||
// privacy and key management flexibility.
|
||||
//
|
||||
// ⚠️ SECURITY WARNING: Custom signature types do NOT add source authentication to DATAGRAM3!
|
||||
// ⚠️ Sources remain unauthenticated regardless of signature configuration.
|
||||
//
|
||||
// Different signature types provide various security levels for the local destination:
|
||||
// - Ed25519 (type 7) - Recommended for most applications
|
||||
// - ECDSA (types 1-3) - Legacy compatibility
|
||||
// - RedDSA (type 11) - Advanced privacy features
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// session, err := sam.NewDatagram3SessionWithSignature(id, keys, options, "EdDSA_SHA512_Ed25519")
|
||||
func (s *SAM) NewDatagram3SessionWithSignature(id string, keys i2pkeys.I2PKeys, options []string, sigType string) (*Datagram3Session, error) {
|
||||
// Log session creation with security warning
|
||||
logger := log.WithFields(logrus.Fields{
|
||||
"id": id,
|
||||
"options": options,
|
||||
"sigType": sigType,
|
||||
})
|
||||
logger.Warn("Creating DATAGRAM3 session: sources are UNAUTHENTICATED and can be spoofed")
|
||||
logger.Debug("Creating new Datagram3Session with signature")
|
||||
|
||||
// Create the base session using the common package with custom signature
|
||||
// CRITICAL: Use STYLE=DATAGRAM3 (not DATAGRAM or DATAGRAM2)
|
||||
session, err := s.SAM.NewGenericSessionWithSignature("DATAGRAM3", id, keys, sigType, options)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to create generic session with signature")
|
||||
return nil, oops.Errorf("failed to create datagram3 session: %w", err)
|
||||
}
|
||||
|
||||
// Ensure the session is of the correct type for datagram3 operations
|
||||
baseSession, ok := session.(*common.BaseSession)
|
||||
if !ok {
|
||||
logger.Error("Session is not a BaseSession")
|
||||
session.Close()
|
||||
return nil, oops.Errorf("invalid session type")
|
||||
}
|
||||
|
||||
// Initialize the datagram3 session with the base session and hash resolver
|
||||
ds := &Datagram3Session{
|
||||
BaseSession: baseSession,
|
||||
sam: s.SAM,
|
||||
options: options,
|
||||
resolver: NewHashResolver(s.SAM),
|
||||
}
|
||||
|
||||
logger.Debug("Successfully created Datagram3Session with signature")
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
// NewDatagram3SessionWithPorts creates a new datagram3 session with port specifications.
|
||||
// This method allows configuring specific I2CP port ranges for the session, enabling fine-grained
|
||||
// control over network communication ports for advanced routing scenarios. Port configuration
|
||||
// is useful for applications requiring specific port mappings or PRIMARY session subsessions.
|
||||
// This function automatically creates a UDP listener for SAMv3 UDP forwarding (required for v3 mode).
|
||||
//
|
||||
// ⚠️ SECURITY WARNING: Port configuration does NOT add source authentication to DATAGRAM3!
|
||||
// ⚠️ Sources remain unauthenticated regardless of port settings.
|
||||
//
|
||||
// The FROM_PORT and TO_PORT parameters specify I2CP ports for protocol-level communication,
|
||||
// distinct from the UDP forwarding port which is auto-assigned by the OS.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// session, err := sam.NewDatagram3SessionWithPorts(id, "8080", "8081", keys, options)
|
||||
func (s *SAM) NewDatagram3SessionWithPorts(id, fromPort, toPort string, keys i2pkeys.I2PKeys, options []string) (*Datagram3Session, error) {
|
||||
// Log session creation with security warning
|
||||
logger := log.WithFields(logrus.Fields{
|
||||
"id": id,
|
||||
"fromPort": fromPort,
|
||||
"toPort": toPort,
|
||||
"options": options,
|
||||
})
|
||||
logger.Warn("Creating DATAGRAM3 session with ports: sources are UNAUTHENTICATED")
|
||||
logger.Debug("Creating new Datagram3Session with ports")
|
||||
|
||||
// Create UDP listener for receiving forwarded datagrams (SAMv3 requirement)
|
||||
// The SAM bridge will forward incoming DATAGRAM3 messages to this local UDP port
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to resolve UDP address")
|
||||
return nil, oops.Errorf("failed to resolve UDP address: %w", err)
|
||||
}
|
||||
|
||||
udpConn, err := net.ListenUDP("udp", udpAddr)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to create UDP listener")
|
||||
return nil, oops.Errorf("failed to create UDP listener: %w", err)
|
||||
}
|
||||
|
||||
// Get the actual port assigned by the OS
|
||||
udpPort := udpConn.LocalAddr().(*net.UDPAddr).Port
|
||||
logger.WithField("udp_port", udpPort).Debug("Created UDP listener for datagram3 forwarding")
|
||||
|
||||
// Inject UDP forwarding parameters into session options (SAMv3 requirement)
|
||||
// HOST and PORT tell the SAM bridge where to forward received datagrams
|
||||
options = ensureUDPForwardingParameters(options, udpPort)
|
||||
|
||||
// Create the base session using the common package with port configuration
|
||||
// CRITICAL: Use STYLE=DATAGRAM3 (not DATAGRAM or DATAGRAM2)
|
||||
session, err := s.SAM.NewGenericSessionWithSignatureAndPorts("DATAGRAM3", id, fromPort, toPort, keys, common.SIG_EdDSA_SHA512_Ed25519, options)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to create generic session with ports")
|
||||
udpConn.Close() // Clean up UDP listener on error
|
||||
return nil, oops.Errorf("failed to create datagram3 session: %w", err)
|
||||
}
|
||||
|
||||
// Ensure the session is of the correct type for datagram3 operations
|
||||
baseSession, ok := session.(*common.BaseSession)
|
||||
if !ok {
|
||||
logger.Error("Session is not a BaseSession")
|
||||
session.Close()
|
||||
udpConn.Close() // Clean up UDP listener on error
|
||||
return nil, oops.Errorf("invalid session type")
|
||||
}
|
||||
|
||||
// Initialize the datagram3 session with UDP forwarding enabled and hash resolver
|
||||
ds := &Datagram3Session{
|
||||
BaseSession: baseSession,
|
||||
sam: s.SAM,
|
||||
options: options,
|
||||
udpConn: udpConn,
|
||||
udpEnabled: true,
|
||||
resolver: NewHashResolver(s.SAM),
|
||||
}
|
||||
|
||||
logger.Debug("Successfully created Datagram3Session with ports and UDP forwarding")
|
||||
return ds, nil
|
||||
}
|
||||
@@ -1,8 +1,30 @@
|
||||
// Package datagram3 provides repliable datagram sessions with hash-based source identification for I2P.
|
||||
//
|
||||
// DATAGRAM3 sessions provide repliable UDP-like messaging with hash-based source identification
|
||||
// instead of full destinations. Sources are not cryptographically authenticated; applications
|
||||
// requiring authenticated sources should use datagram2 instead.
|
||||
//
|
||||
// Key features:
|
||||
// - Repliable (can send replies to sender)
|
||||
// - Hash-based source identification (32-byte hash)
|
||||
// - No source authentication (spoofable)
|
||||
// - Requires NAMING LOOKUP for replies
|
||||
// - UDP-like messaging (unreliable, unordered)
|
||||
// - Maximum 31744 bytes per datagram (11 KB recommended)
|
||||
//
|
||||
// Session creation requires 2-5 minutes for I2P tunnel establishment. Use generous timeouts
|
||||
// and exponential backoff retry logic. Hash resolution uses automatic caching to minimize
|
||||
// NAMING LOOKUP overhead.
|
||||
//
|
||||
// Basic usage:
|
||||
//
|
||||
// sam, err := common.NewSAM("127.0.0.1:7656")
|
||||
// session, err := datagram3.NewDatagram3Session(sam, "my-session", keys, []string{"inbound.length=1"})
|
||||
// defer session.Close()
|
||||
// dg, err := session.NewReader().ReceiveDatagram()
|
||||
// if err := dg.ResolveSource(session); err != nil { log.Error(err) }
|
||||
// session.NewWriter().SendDatagram([]byte("reply"), dg.Source)
|
||||
//
|
||||
// See also: Package datagram (legacy, authenticated), datagram2 (authenticated with replay
|
||||
// protection), stream (TCP-like), raw (non-repliable), primary (multi-session management).
|
||||
package datagram3
|
||||
|
||||
/*
|
||||
* TODO: implement the Datagram2Session type for SAMv3 Authenticated Datagram Sessions
|
||||
* This package provides the implementation for un-authenticated datagram sessions
|
||||
* using the SAMv3 protocol. It includes session management, datagram reading and writing,
|
||||
* and integration with the SAMv3 protocol for secure communication.
|
||||
*/
|
||||
|
||||
315
datagram3/listen.go
Normal file
315
datagram3/listen.go
Normal file
@@ -0,0 +1,315 @@
|
||||
package datagram3
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-i2p/logger"
|
||||
"github.com/samber/oops"
|
||||
)
|
||||
|
||||
// ReceiveDatagram receives a single UNAUTHENTICATED datagram from the I2P network.
|
||||
//
|
||||
// ⚠️ CRITICAL SECURITY WARNING: Sources are NOT authenticated and can be spoofed!
|
||||
// ⚠️ Do not trust datagram.SourceHash without additional verification.
|
||||
// ⚠️ Use application-layer authentication if source identity matters.
|
||||
//
|
||||
// This method blocks until a datagram is received or an error occurs, returning
|
||||
// the received datagram with its data and UNAUTHENTICATED hash-based source.
|
||||
// It handles concurrent access safely and provides proper error handling for network issues.
|
||||
//
|
||||
// Unlike DATAGRAM/DATAGRAM2, received datagrams contain only a 32-byte hash (not full destination).
|
||||
// Applications must call ResolveSource() to convert the hash to a full destination for replies.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// datagram, err := reader.ReceiveDatagram()
|
||||
// if err != nil {
|
||||
// // Handle error
|
||||
// }
|
||||
// // SECURITY: datagram.SourceHash is UNAUTHENTICATED!
|
||||
// log.Warn("Received from unverified source:", hex.EncodeToString(datagram.SourceHash))
|
||||
// // Resolve hash for reply (expensive, cached)
|
||||
// if err := datagram.ResolveSource(session); err != nil {
|
||||
// log.Error(err)
|
||||
// }
|
||||
func (r *Datagram3Reader) ReceiveDatagram() (*Datagram3, error) {
|
||||
// Hold read lock for the entire operation to prevent race with Close()
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
if r.closed {
|
||||
return nil, oops.Errorf("reader is closed")
|
||||
}
|
||||
|
||||
// Use select to handle multiple channel operations atomically
|
||||
// The lock ensures that channels won't be closed while we're selecting on them
|
||||
select {
|
||||
case datagram := <-r.recvChan:
|
||||
// Successfully received a datagram with UNAUTHENTICATED hash from the network
|
||||
return datagram, nil
|
||||
case err := <-r.errorChan:
|
||||
// An error occurred during datagram reception
|
||||
return nil, err
|
||||
case <-r.closeChan:
|
||||
// The reader has been closed while waiting for a datagram
|
||||
return nil, oops.Errorf("reader is closed")
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the Datagram3Reader and stops its receive loop.
|
||||
// This method safely terminates the reader, cleans up all associated resources,
|
||||
// and signals any waiting goroutines to stop. It's safe to call multiple times
|
||||
// and will not block if the reader is already closed.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// defer reader.Close()
|
||||
func (r *Datagram3Reader) Close() error {
|
||||
// Use sync.Once to ensure cleanup only happens once
|
||||
// This prevents double-close panics and ensures thread safety
|
||||
r.closeOnce.Do(func() {
|
||||
r.performCloseOperation()
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// performCloseOperation executes the complete close sequence with proper synchronization.
|
||||
func (r *Datagram3Reader) performCloseOperation() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.closed {
|
||||
return
|
||||
}
|
||||
|
||||
logger := r.initializeCloseLogger()
|
||||
r.signalReaderClosure(logger)
|
||||
r.waitForReceiveLoopTermination(logger)
|
||||
r.finalizeReaderClosure(logger)
|
||||
}
|
||||
|
||||
// initializeCloseLogger sets up logging context for the close operation.
|
||||
func (r *Datagram3Reader) initializeCloseLogger() *logger.Entry {
|
||||
sessionID := "unknown"
|
||||
if r.session != nil && r.session.BaseSession != nil {
|
||||
sessionID = r.session.ID()
|
||||
}
|
||||
logger := log.WithField("session_id", sessionID)
|
||||
logger.Debug("Closing Datagram3Reader")
|
||||
return logger
|
||||
}
|
||||
|
||||
// signalReaderClosure marks the reader as closed and signals termination.
|
||||
func (r *Datagram3Reader) signalReaderClosure(logger *logger.Entry) {
|
||||
r.closed = true
|
||||
// Signal the receive loop to terminate
|
||||
// This prevents the background goroutine from continuing to run
|
||||
close(r.closeChan)
|
||||
}
|
||||
|
||||
// waitForReceiveLoopTermination waits for the receive loop to stop with timeout protection.
|
||||
func (r *Datagram3Reader) waitForReceiveLoopTermination(logger *logger.Entry) {
|
||||
// Only wait for the receive loop if it was actually started
|
||||
if r.loopStarted {
|
||||
r.waitForLoopWithTimeout(logger)
|
||||
} else {
|
||||
logger.Debug("Receive loop was never started, skipping wait")
|
||||
}
|
||||
}
|
||||
|
||||
// waitForLoopWithTimeout waits for receive loop termination with timeout protection.
|
||||
func (r *Datagram3Reader) waitForLoopWithTimeout(logger *logger.Entry) {
|
||||
// Wait for the receive loop to confirm termination
|
||||
// This ensures proper cleanup before returning
|
||||
select {
|
||||
case <-r.doneChan:
|
||||
// Receive loop has confirmed it stopped
|
||||
logger.Debug("Receive loop stopped")
|
||||
case <-time.After(5 * time.Second):
|
||||
// Timeout protection to prevent indefinite blocking
|
||||
logger.Warn("Timeout waiting for receive loop to stop")
|
||||
}
|
||||
}
|
||||
|
||||
// finalizeReaderClosure performs final cleanup and logging.
|
||||
func (r *Datagram3Reader) finalizeReaderClosure(logger *logger.Entry) {
|
||||
// Clean up channels to prevent resource leaks
|
||||
// Note: We don't close r.recvChan and r.errorChan here because the receive loop
|
||||
// might still be sending on them. These channels will be garbage collected when
|
||||
// all references are dropped. Only the receive loop should close send-channels.
|
||||
|
||||
logger.Debug("Successfully closed Datagram3Reader")
|
||||
}
|
||||
|
||||
// receiveLoop continuously receives incoming UNAUTHENTICATED datagrams in a separate goroutine.
|
||||
//
|
||||
// ⚠️ SECURITY WARNING: All received datagrams have UNAUTHENTICATED sources!
|
||||
// ⚠️ Source hashes can be spoofed by malicious actors.
|
||||
//
|
||||
// This method handles the SAM protocol communication for datagram3 reception, parsing
|
||||
// UDP forwarded messages with hash-based sources and forwarding datagrams to channels.
|
||||
// It runs until the reader is closed and provides error handling for network issues.
|
||||
func (r *Datagram3Reader) receiveLoop() {
|
||||
// Initialize receive loop state in a separate function to handle locking properly
|
||||
if !r.initializeReceiveLoopState() {
|
||||
return
|
||||
}
|
||||
|
||||
logger := r.initializeReceiveLoop()
|
||||
defer r.signalReceiveLoopCompletion()
|
||||
|
||||
if err := r.validateSessionState(logger); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
r.runReceiveLoop(logger)
|
||||
}
|
||||
|
||||
// initializeReceiveLoopState safely initializes the receive loop state with proper locking.
|
||||
// Returns false if initialization failed, true if successful.
|
||||
func (r *Datagram3Reader) initializeReceiveLoopState() bool {
|
||||
// CRITICAL FIX: Check if we can acquire the lock without blocking
|
||||
// Use TryLock equivalent by checking state first
|
||||
r.mu.RLock()
|
||||
if r.closed {
|
||||
r.mu.RUnlock()
|
||||
return false
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
|
||||
// Now safely acquire write lock to set loopStarted
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
// Double-check closed state after acquiring write lock
|
||||
if r.closed {
|
||||
return false
|
||||
}
|
||||
|
||||
r.loopStarted = true
|
||||
return true
|
||||
}
|
||||
|
||||
// initializeReceiveLoop sets up logging context and returns a logger for the receive loop.
|
||||
func (r *Datagram3Reader) initializeReceiveLoop() *logger.Entry {
|
||||
sessionID := "unknown"
|
||||
if r.session != nil && r.session.BaseSession != nil {
|
||||
sessionID = r.session.ID()
|
||||
}
|
||||
logger := log.WithField("session_id", sessionID)
|
||||
logger.Warn("Starting datagram3 receive loop: sources are UNAUTHENTICATED!")
|
||||
logger.Debug("Starting datagram3 receive loop")
|
||||
return logger
|
||||
}
|
||||
|
||||
// signalReceiveLoopCompletion signals that the receive loop has completed execution.
|
||||
func (r *Datagram3Reader) signalReceiveLoopCompletion() {
|
||||
// Close doneChan to signal completion - channels should be closed by sender
|
||||
close(r.doneChan)
|
||||
}
|
||||
|
||||
// validateSessionState checks if the session is valid before starting the receive loop.
|
||||
func (r *Datagram3Reader) validateSessionState(logger *logger.Entry) error {
|
||||
if r.session == nil || r.session.BaseSession == nil {
|
||||
logger.Error("Invalid session state")
|
||||
select {
|
||||
case r.errorChan <- oops.Errorf("invalid session state"):
|
||||
case <-r.closeChan:
|
||||
}
|
||||
return oops.Errorf("invalid session state")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runReceiveLoop executes the main receive loop until the reader is closed.
|
||||
func (r *Datagram3Reader) runReceiveLoop(logger *logger.Entry) {
|
||||
for {
|
||||
select {
|
||||
case <-r.closeChan:
|
||||
logger.Debug("Receive loop terminated")
|
||||
return
|
||||
default:
|
||||
if !r.processIncomingDatagram(logger) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processIncomingDatagram receives and forwards a single UNAUTHENTICATED datagram, returning false if the loop should terminate.
|
||||
func (r *Datagram3Reader) processIncomingDatagram(logger *logger.Entry) bool {
|
||||
if !r.checkReaderActiveState() {
|
||||
return false
|
||||
}
|
||||
|
||||
datagram, err := r.receiveDatagram()
|
||||
if err != nil {
|
||||
return r.handleDatagramError(err, logger)
|
||||
}
|
||||
|
||||
return r.forwardDatagramToChannel(datagram)
|
||||
}
|
||||
|
||||
// checkReaderActiveState verifies the reader is not closed before processing.
|
||||
func (r *Datagram3Reader) checkReaderActiveState() bool {
|
||||
r.mu.RLock()
|
||||
isClosed := r.closed
|
||||
r.mu.RUnlock()
|
||||
return !isClosed
|
||||
}
|
||||
|
||||
// handleDatagramError processes errors during datagram reception and reports them.
|
||||
func (r *Datagram3Reader) handleDatagramError(err error, logger *logger.Entry) bool {
|
||||
logger.WithError(err).Debug("Error receiving datagram3 message")
|
||||
select {
|
||||
case r.errorChan <- err:
|
||||
return true
|
||||
case <-r.closeChan:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// forwardDatagramToChannel sends the received UNAUTHENTICATED datagram to the receive channel atomically.
|
||||
func (r *Datagram3Reader) forwardDatagramToChannel(datagram *Datagram3) bool {
|
||||
select {
|
||||
case r.recvChan <- datagram:
|
||||
return true
|
||||
case <-r.closeChan:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// receiveDatagram performs the actual datagram3 reception from the UDP connection.
|
||||
// This method handles UDP datagram3 reception forwarded by the SAM bridge (SAMv3).
|
||||
// V1/V2 TCP control socket reading is no longer supported.
|
||||
//
|
||||
// ⚠️ SECURITY WARNING: All datagrams contain UNAUTHENTICATED hash-based sources!
|
||||
func (r *Datagram3Reader) receiveDatagram() (*Datagram3, error) {
|
||||
if err := r.validateReaderState(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// V3-only: Read from UDP connection
|
||||
r.session.mu.RLock()
|
||||
udpConn := r.session.udpConn
|
||||
r.session.mu.RUnlock()
|
||||
|
||||
if udpConn == nil {
|
||||
return nil, oops.Errorf("UDP connection not available (v3 UDP forwarding required)")
|
||||
}
|
||||
|
||||
return r.session.readDatagramFromUDP(udpConn)
|
||||
}
|
||||
|
||||
// validateReaderState checks if reader is closed before attempting expensive I/O operation.
|
||||
func (r *Datagram3Reader) validateReaderState() error {
|
||||
r.mu.RLock()
|
||||
isClosed := r.closed
|
||||
r.mu.RUnlock()
|
||||
|
||||
if isClosed {
|
||||
return oops.Errorf("reader is closing")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
7
datagram3/log.go
Normal file
7
datagram3/log.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package datagram3
|
||||
|
||||
import (
|
||||
"github.com/go-i2p/logger"
|
||||
)
|
||||
|
||||
var log = logger.GetGoI2PLogger()
|
||||
299
datagram3/packetconn.go
Normal file
299
datagram3/packetconn.go
Normal file
@@ -0,0 +1,299 @@
|
||||
package datagram3
|
||||
|
||||
import (
|
||||
"net"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-i2p/i2pkeys"
|
||||
"github.com/samber/oops"
|
||||
)
|
||||
|
||||
// ReadFrom reads an UNAUTHENTICATED datagram from the connection.
|
||||
//
|
||||
// ⚠️ CRITICAL SECURITY WARNING: Source addresses are NOT authenticated!
|
||||
// ⚠️ The returned address contains an UNAUTHENTICATED hash-based source.
|
||||
// ⚠️ Do not trust source identity without additional verification.
|
||||
//
|
||||
// This method implements the net.PacketConn interface. It starts the receive loop if not
|
||||
// already started and blocks until a datagram is received. The data is copied to the provided
|
||||
// buffer p, and the UNAUTHENTICATED source address is returned as a Datagram3Addr.
|
||||
//
|
||||
// The source address contains the 32-byte hash (not full destination). Applications must
|
||||
// resolve the hash via ResolveSource() to reply.
|
||||
func (c *Datagram3Conn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
c.mu.RLock()
|
||||
if c.closed {
|
||||
c.mu.RUnlock()
|
||||
return 0, nil, oops.Errorf("connection is closed")
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
// Start receive loop if not already started
|
||||
go c.reader.receiveLoop()
|
||||
|
||||
datagram, err := c.reader.ReceiveDatagram()
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
// Copy data to the provided buffer
|
||||
n = copy(p, datagram.Data)
|
||||
|
||||
// Create address with UNAUTHENTICATED hash
|
||||
// Applications can check addr.(*Datagram3Addr).hash for the hash
|
||||
addr = &Datagram3Addr{
|
||||
addr: datagram.Source, // May be empty if not resolved
|
||||
hash: datagram.SourceHash, // 32-byte UNAUTHENTICATED hash
|
||||
}
|
||||
|
||||
return n, addr, nil
|
||||
}
|
||||
|
||||
// WriteTo writes a datagram to the specified address.
|
||||
// This method implements the net.PacketConn interface. The address must be a Datagram3Addr
|
||||
// or i2pkeys.I2PAddr containing a valid I2P destination. The entire byte slice p is sent
|
||||
// as a single datagram message.
|
||||
//
|
||||
// If the address is a Datagram3Addr with only a hash (not resolved), the hash will be
|
||||
// resolved automatically before sending.
|
||||
func (c *Datagram3Conn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||
c.mu.RLock()
|
||||
if c.closed {
|
||||
c.mu.RUnlock()
|
||||
return 0, oops.Errorf("connection is closed")
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
// Convert address to I2P address
|
||||
var i2pAddr i2pkeys.I2PAddr
|
||||
|
||||
switch a := addr.(type) {
|
||||
case *Datagram3Addr:
|
||||
// If address has full destination, use it
|
||||
if a.addr != "" {
|
||||
i2pAddr = a.addr
|
||||
} else if len(a.hash) == 32 {
|
||||
// Only hash available - resolve it
|
||||
log.Debug("Resolving hash for WriteTo")
|
||||
resolved, err := c.session.resolver.ResolveHash(a.hash)
|
||||
if err != nil {
|
||||
return 0, oops.Errorf("failed to resolve hash: %w", err)
|
||||
}
|
||||
i2pAddr = resolved
|
||||
} else {
|
||||
return 0, oops.Errorf("address has neither full destination nor valid hash")
|
||||
}
|
||||
case *i2pkeys.I2PAddr:
|
||||
i2pAddr = *a
|
||||
case i2pkeys.I2PAddr:
|
||||
i2pAddr = a
|
||||
default:
|
||||
return 0, oops.Errorf("address must be Datagram3Addr or I2PAddr")
|
||||
}
|
||||
|
||||
err = c.writer.SendDatagram(p, i2pAddr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Close closes the datagram3 connection and releases associated resources.
|
||||
// This method implements the net.Conn interface. It closes the reader and writer
|
||||
// but does not close the underlying session, which may be shared by other connections.
|
||||
// Multiple calls to Close are safe and will return nil after the first call.
|
||||
func (c *Datagram3Conn) Close() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.closed {
|
||||
return nil
|
||||
}
|
||||
|
||||
var sessionID string
|
||||
if c.session != nil {
|
||||
sessionID = c.session.ID()
|
||||
} else {
|
||||
sessionID = "unknown"
|
||||
}
|
||||
logger := log.WithFields(map[string]interface{}{
|
||||
"session_id": sessionID,
|
||||
"style": "DATAGRAM3",
|
||||
})
|
||||
logger.Debug("Closing Datagram3Conn")
|
||||
|
||||
c.closed = true
|
||||
|
||||
// Clear the finalizer since we're cleaning up explicitly
|
||||
c.clearCleanup()
|
||||
|
||||
// Close reader and writer - these are owned by this connection
|
||||
if c.reader != nil {
|
||||
c.reader.Close()
|
||||
}
|
||||
|
||||
// DO NOT close the session - it's a shared resource that may be used by other connections
|
||||
// The session should be closed by the code that created it, not by individual connections
|
||||
// that use it. This follows the principle that the creator owns the resource.
|
||||
|
||||
logger.Debug("Successfully closed Datagram3Conn")
|
||||
return nil
|
||||
}
|
||||
|
||||
// LocalAddr returns the local network address as a Datagram3Addr containing
|
||||
// the I2P destination address of this connection's session. This method implements
|
||||
// the net.Conn interface and provides access to the local I2P destination.
|
||||
func (c *Datagram3Conn) LocalAddr() net.Addr {
|
||||
return &Datagram3Addr{addr: c.session.Addr()}
|
||||
}
|
||||
|
||||
// SetDeadline sets both read and write deadlines for the connection.
|
||||
// This method implements the net.Conn interface by calling both SetReadDeadline
|
||||
// and SetWriteDeadline with the same time value. If either deadline cannot be set,
|
||||
// the first error encountered is returned.
|
||||
func (c *Datagram3Conn) SetDeadline(t time.Time) error {
|
||||
if err := c.SetReadDeadline(t); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
// SetReadDeadline sets the deadline for future ReadFrom calls.
|
||||
// This method implements the net.Conn interface. For datagram3 connections,
|
||||
// this is currently a placeholder implementation that always returns nil.
|
||||
// Timeout handling is managed differently for datagram operations.
|
||||
func (c *Datagram3Conn) SetReadDeadline(t time.Time) error {
|
||||
// For datagrams, we handle timeouts differently
|
||||
// This is a placeholder implementation
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetWriteDeadline sets the deadline for future WriteTo calls.
|
||||
// This method implements the net.Conn interface. If the deadline is not zero,
|
||||
// it calculates the timeout duration and sets it on the writer for subsequent
|
||||
// write operations.
|
||||
func (c *Datagram3Conn) SetWriteDeadline(t time.Time) error {
|
||||
// Calculate timeout duration
|
||||
if !t.IsZero() {
|
||||
timeout := time.Until(t)
|
||||
c.writer.SetTimeout(timeout)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read implements net.Conn by wrapping ReadFrom for stream-like usage.
|
||||
// It reads data into the provided byte slice and returns the number of bytes read.
|
||||
// When reading, it also updates the remote address of the connection for subsequent
|
||||
// Write calls.
|
||||
//
|
||||
// ⚠️ SECURITY WARNING: Remote address is UNAUTHENTICATED hash-based!
|
||||
//
|
||||
// Note: This is not typical for datagrams which are connectionless,
|
||||
// but provides compatibility with the net.Conn interface.
|
||||
func (c *Datagram3Conn) Read(b []byte) (n int, err error) {
|
||||
n, addr, err := c.ReadFrom(b)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Store remote address for Write operations
|
||||
if dg3Addr, ok := addr.(*Datagram3Addr); ok {
|
||||
i2pAddr := dg3Addr.addr
|
||||
c.remoteAddr = &i2pAddr
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
// RemoteAddr returns the remote network address of the connection.
|
||||
// This method implements the net.Conn interface. For datagram3 connections,
|
||||
// this returns the UNAUTHENTICATED address of the last peer that sent data (set by Read),
|
||||
// or nil if no data has been received yet.
|
||||
//
|
||||
// ⚠️ SECURITY WARNING: Remote address is UNAUTHENTICATED!
|
||||
func (c *Datagram3Conn) RemoteAddr() net.Addr {
|
||||
if c.remoteAddr != nil {
|
||||
return &Datagram3Addr{addr: *c.remoteAddr}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write implements net.Conn by wrapping WriteTo for stream-like usage.
|
||||
// It writes data to the remote address set by the last Read operation and
|
||||
// returns the number of bytes written. If no remote address has been set,
|
||||
// it returns an error. Note: This is not typical for datagrams which are
|
||||
// connectionless, but provides compatibility with the net.Conn interface.
|
||||
func (c *Datagram3Conn) Write(b []byte) (n int, err error) {
|
||||
if c.remoteAddr == nil {
|
||||
return 0, oops.Errorf("no remote address set, use WriteTo or Read first")
|
||||
}
|
||||
|
||||
addr := &Datagram3Addr{addr: *c.remoteAddr}
|
||||
return c.WriteTo(b, addr)
|
||||
}
|
||||
|
||||
// cleanupDatagram3Conn is called by AddCleanup to ensure resources are cleaned up
|
||||
// even if the user forgets to call Close(). This prevents goroutine leaks.
|
||||
func cleanupDatagram3Conn(c *Datagram3Conn) {
|
||||
c.mu.Lock()
|
||||
if !c.closed {
|
||||
log.Warn("Datagram3Conn was garbage collected without being closed - cleaning up resources")
|
||||
c.closed = true
|
||||
if c.reader != nil {
|
||||
c.reader.Close()
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// addCleanup sets up automatic cleanup for the connection to prevent resource leaks
|
||||
func (c *Datagram3Conn) addCleanup() {
|
||||
c.cleanup = runtime.AddCleanup(c, func(c *Datagram3Conn) {
|
||||
cleanupDatagram3Conn(c)
|
||||
}, c)
|
||||
}
|
||||
|
||||
// clearCleanup removes the automatic cleanup if Close() is called explicitly
|
||||
func (c *Datagram3Conn) clearCleanup() {
|
||||
c.cleanup.Stop()
|
||||
}
|
||||
|
||||
// PacketConn returns a net.PacketConn interface for this datagram3 session.
|
||||
// This method provides compatibility with standard Go networking code by wrapping
|
||||
// the datagram3 session in a PacketConn interface. The returned connection manages
|
||||
// its own reader and writer and implements all standard net.PacketConn methods.
|
||||
//
|
||||
// ⚠️ SECURITY WARNING: All sources are UNAUTHENTICATED!
|
||||
// ⚠️ Do not trust addresses received via ReadFrom without verification.
|
||||
//
|
||||
// The connection is automatically cleaned up by a finalizer if Close() is not called,
|
||||
// but explicit Close() calls are strongly recommended to prevent resource leaks.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// conn := session.PacketConn()
|
||||
// defer conn.Close()
|
||||
//
|
||||
// // Receive with UNAUTHENTICATED source
|
||||
// n, addr, err := conn.ReadFrom(buffer)
|
||||
// // addr is UNAUTHENTICATED!
|
||||
//
|
||||
// // Send reply
|
||||
// n, err = conn.WriteTo(reply, addr)
|
||||
func (s *Datagram3Session) PacketConn() net.PacketConn {
|
||||
conn := &Datagram3Conn{
|
||||
session: s,
|
||||
reader: s.NewReader(),
|
||||
writer: s.NewWriter(),
|
||||
mu: sync.RWMutex{},
|
||||
closed: false,
|
||||
}
|
||||
|
||||
// Add cleanup to prevent resource leaks if user forgets to call Close()
|
||||
conn.addCleanup()
|
||||
|
||||
return conn
|
||||
}
|
||||
138
datagram3/read.go
Normal file
138
datagram3/read.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package datagram3
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/oops"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// readDatagramFromUDP reads a forwarded datagram3 message from the UDP connection.
|
||||
//
|
||||
// ⚠️ CRITICAL SECURITY WARNING: Source is a 44-byte base64 hash, NOT authenticated!
|
||||
// ⚠️ The hash can be spoofed by malicious actors - do NOT trust without verification.
|
||||
// ⚠️ This is fundamentally different from DATAGRAM/DATAGRAM2 authenticated sources.
|
||||
//
|
||||
// This is used for receiving datagrams where the SAM bridge forwards messages via UDP.
|
||||
// DATAGRAM3 uses hash-based source identification instead of full destinations.
|
||||
//
|
||||
// Format per SAMv3.md:
|
||||
//
|
||||
// Line 1: $hash (44-byte base64 hash, UNAUTHENTICATED!)
|
||||
// [FROM_PORT=nnn] [TO_PORT=nnn] (SAMv3.2+, may be on one or two lines)
|
||||
// Then: \n (empty line separator)
|
||||
// Remaining: $datagram_payload (raw data)
|
||||
//
|
||||
// CRITICAL DIFFERENCE from DATAGRAM/DATAGRAM2:
|
||||
// - Source is 44-byte base64 hash (32 bytes binary)
|
||||
// - NOT a full destination (516+ chars)
|
||||
// - Hash is UNAUTHENTICATED and spoofable
|
||||
// - Must use NAMING LOOKUP to resolve hash for replies
|
||||
//
|
||||
// The hash decodes to 32 bytes binary. To reply:
|
||||
// 1. Base32-encode hash to 52 characters
|
||||
// 2. Append ".b32.i2p" suffix
|
||||
// 3. Use NAMING LOOKUP to get full destination
|
||||
// 4. Cache result to avoid repeated lookups
|
||||
func (s *Datagram3Session) readDatagramFromUDP(udpConn *net.UDPConn) (*Datagram3, error) {
|
||||
buffer := make([]byte, 65536) // Large buffer for UDP datagrams (I2P maximum)
|
||||
n, _, err := udpConn.ReadFromUDP(buffer)
|
||||
if err != nil {
|
||||
return nil, oops.Errorf("failed to read from UDP connection: %w", err)
|
||||
}
|
||||
|
||||
log.WithFields(logrus.Fields{
|
||||
"bytes_read": n,
|
||||
"style": "DATAGRAM3",
|
||||
}).Debug("Received UDP datagram3 message with UNAUTHENTICATED hash source")
|
||||
|
||||
// Parse the UDP datagram format per SAMv3.md
|
||||
response := string(buffer[:n])
|
||||
|
||||
// Find the first newline - that's the end of the header line
|
||||
firstNewline := strings.Index(response, "\n")
|
||||
if firstNewline == -1 {
|
||||
return nil, oops.Errorf("invalid UDP datagram3 format: no newline found")
|
||||
}
|
||||
|
||||
// Line 1: Source hash (44-byte base64, UNAUTHENTICATED!) followed by optional FROM_PORT=nnn TO_PORT=nnn
|
||||
headerLine := strings.TrimSpace(response[:firstNewline])
|
||||
|
||||
if headerLine == "" {
|
||||
return nil, oops.Errorf("empty header line in UDP datagram3")
|
||||
}
|
||||
|
||||
// Parse the header line to extract the UNAUTHENTICATED source hash
|
||||
// Format: "$hash_base64 FROM_PORT=nnn TO_PORT=nnn"
|
||||
// We need to split on space and take the first part as the hash
|
||||
parts := strings.Fields(headerLine)
|
||||
if len(parts) == 0 {
|
||||
return nil, oops.Errorf("empty header line in UDP datagram3")
|
||||
}
|
||||
|
||||
hashBase64 := parts[0] // First field is the UNAUTHENTICATED source hash
|
||||
// Remaining parts are FROM_PORT and TO_PORT which we ignore for now
|
||||
|
||||
// CRITICAL VALIDATION: Hash MUST be exactly 44 bytes base64 (32 bytes binary)
|
||||
if len(hashBase64) != 44 {
|
||||
return nil, oops.Errorf("invalid hash length: %d (expected 44 base64 chars)", len(hashBase64))
|
||||
}
|
||||
|
||||
// Decode hash from base64 to 32-byte binary
|
||||
hashBytes, err := base64.StdEncoding.DecodeString(hashBase64)
|
||||
if err != nil {
|
||||
return nil, oops.Errorf("failed to decode source hash from base64: %w", err)
|
||||
}
|
||||
|
||||
// Validate decoded hash is exactly 32 bytes
|
||||
if len(hashBytes) != 32 {
|
||||
return nil, oops.Errorf("invalid hash binary length: %d (expected 32 bytes)", len(hashBytes))
|
||||
}
|
||||
|
||||
log.WithFields(logrus.Fields{
|
||||
"hash_base64": hashBase64,
|
||||
"hash_len": len(hashBytes),
|
||||
}).Debug("Parsed UNAUTHENTICATED source hash")
|
||||
|
||||
// Everything after the first newline is the payload
|
||||
data := response[firstNewline+1:]
|
||||
|
||||
if data == "" {
|
||||
return nil, oops.Errorf("no data in UDP datagram3")
|
||||
}
|
||||
|
||||
return s.createDatagram(hashBytes, data)
|
||||
}
|
||||
|
||||
// createDatagram constructs the final Datagram3 from parsed UNAUTHENTICATED hash and data.
|
||||
//
|
||||
// ⚠️ SECURITY WARNING: The hash is NOT authenticated and can be spoofed!
|
||||
// ⚠️ Do NOT trust the source without additional verification.
|
||||
//
|
||||
// The datagram is created with:
|
||||
// - Data: Raw payload bytes
|
||||
// - SourceHash: 32-byte UNAUTHENTICATED hash (spoofable!)
|
||||
// - Source: Empty (not resolved, requires NAMING LOOKUP for replies)
|
||||
// - Local: This session's I2P address
|
||||
//
|
||||
// Applications must call ResolveSource() to convert hash to full destination for replies.
|
||||
func (s *Datagram3Session) createDatagram(hashBytes []byte, data string) (*Datagram3, error) {
|
||||
// Create datagram with UNAUTHENTICATED hash
|
||||
// Source is empty (not resolved) - applications resolve on-demand for replies
|
||||
datagram := &Datagram3{
|
||||
Data: []byte(data),
|
||||
SourceHash: hashBytes, // 32-byte UNAUTHENTICATED hash (spoofable!)
|
||||
Source: "", // Not resolved (empty = requires ResolveSource())
|
||||
Local: s.Addr(),
|
||||
}
|
||||
|
||||
log.WithFields(logrus.Fields{
|
||||
"data_len": len(datagram.Data),
|
||||
"hash_len": len(datagram.SourceHash),
|
||||
"source_set": datagram.Source != "",
|
||||
}).Debug("Created datagram3 with UNAUTHENTICATED hash source")
|
||||
|
||||
return datagram, nil
|
||||
}
|
||||
241
datagram3/resolver.go
Normal file
241
datagram3/resolver.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package datagram3
|
||||
|
||||
import (
|
||||
"encoding/base32"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-i2p/go-sam-go/common"
|
||||
"github.com/go-i2p/i2pkeys"
|
||||
"github.com/samber/oops"
|
||||
)
|
||||
|
||||
// HashResolver provides caching for hash-to-destination lookups via NAMING LOOKUP.
|
||||
// This prevents repeated network queries for the same hash, which is critical for
|
||||
// DATAGRAM3 performance since every received datagram contains only a hash.
|
||||
//
|
||||
// ⚠️ SECURITY WARNING: Resolving hashes does NOT authenticate sources!
|
||||
// ⚠️ Even with cached full destinations, sources remain UNAUTHENTICATED.
|
||||
// ⚠️ Cache entries are based on hash values which can be spoofed.
|
||||
//
|
||||
// The resolver maintains an in-memory cache mapping b32.i2p addresses to full I2P
|
||||
// destinations. This cache is thread-safe using RWMutex and grows unbounded (applications
|
||||
// should monitor memory usage for long-running sessions receiving from many sources).
|
||||
//
|
||||
// Hash Resolution Process:
|
||||
// 1. Convert 32-byte hash to base32 (52 characters)
|
||||
// 2. Append ".b32.i2p" suffix
|
||||
// 3. Check cache for existing entry
|
||||
// 4. If not cached, perform NAMING LOOKUP via SAM bridge
|
||||
// 5. Cache successful result
|
||||
// 6. Return full I2P destination
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// resolver := NewHashResolver(sam)
|
||||
// dest, err := resolver.ResolveHash(hashBytes)
|
||||
// if err != nil {
|
||||
// log.Error("Resolution failed:", err)
|
||||
// }
|
||||
type HashResolver struct {
|
||||
sam *common.SAM
|
||||
cache map[string]i2pkeys.I2PAddr // map[b32_address] -> full destination
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewHashResolver creates a new hash resolver with empty cache.
|
||||
// The resolver uses the provided SAM connection for NAMING LOOKUP operations
|
||||
// when cache misses occur.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// resolver := NewHashResolver(sam)
|
||||
func NewHashResolver(sam *common.SAM) *HashResolver {
|
||||
return &HashResolver{
|
||||
sam: sam,
|
||||
cache: make(map[string]i2pkeys.I2PAddr),
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveHash converts a 32-byte hash to a full I2P destination using NAMING LOOKUP.
|
||||
//
|
||||
// ⚠️ SECURITY WARNING: This does NOT authenticate the source!
|
||||
// ⚠️ Resolution only enables replies, it does NOT verify identity.
|
||||
// ⚠️ Malicious actors can provide hashes that resolve to attacker-controlled destinations.
|
||||
//
|
||||
// Process:
|
||||
// 1. Validate hash is exactly 32 bytes
|
||||
// 2. Convert to b32.i2p address (base32 encoding + suffix)
|
||||
// 3. Check cache for existing result
|
||||
// 4. If cached, return immediately (fast path)
|
||||
// 5. If not cached, perform NAMING LOOKUP (slow path, network I/O)
|
||||
// 6. Cache successful result for future lookups
|
||||
// 7. Return full destination
|
||||
//
|
||||
// This is an expensive operation on cache misses due to network round-trip to I2P router.
|
||||
// Applications should minimize unnecessary resolutions by caching at application level
|
||||
// or reusing the same session resolver.
|
||||
//
|
||||
// Error conditions:
|
||||
// - Invalid hash length (not 32 bytes)
|
||||
// - Base32 encoding failure (malformed hash)
|
||||
// - NAMING LOOKUP failure (hash not resolvable, network error, etc.)
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// dest, err := resolver.ResolveHash(datagram.SourceHash)
|
||||
// if err != nil {
|
||||
// log.Error("Failed to resolve hash:", err)
|
||||
// return err
|
||||
// }
|
||||
// // dest contains full I2P destination (still unverified!)
|
||||
// writer.SendDatagram(reply, dest)
|
||||
func (r *HashResolver) ResolveHash(hash []byte) (i2pkeys.I2PAddr, error) {
|
||||
// Validate hash length (CRITICAL: must be exactly 32 bytes)
|
||||
if len(hash) != 32 {
|
||||
return "", oops.Errorf("invalid hash length: %d (expected 32)", len(hash))
|
||||
}
|
||||
|
||||
// Validate SAM connection is available
|
||||
if r.sam == nil {
|
||||
return "", oops.Errorf("SAM connection not available for hash resolution")
|
||||
}
|
||||
|
||||
// Convert hash to b32.i2p address
|
||||
b32Addr := hashToB32Address(hash)
|
||||
|
||||
// Check cache (read lock for concurrent access)
|
||||
r.mu.RLock()
|
||||
if dest, ok := r.cache[b32Addr]; ok {
|
||||
r.mu.RUnlock()
|
||||
log.WithField("b32", b32Addr).Debug("Hash resolved from cache")
|
||||
return dest, nil
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
|
||||
// Cache miss - perform NAMING LOOKUP (expensive network operation)
|
||||
log.WithField("b32", b32Addr).Debug("Cache miss - performing NAMING LOOKUP")
|
||||
dest, err := r.sam.Lookup(b32Addr)
|
||||
if err != nil {
|
||||
return "", oops.Errorf("NAMING LOOKUP failed for %s: %w", b32Addr, err)
|
||||
}
|
||||
|
||||
// Cache successful result (write lock for exclusive access)
|
||||
r.mu.Lock()
|
||||
r.cache[b32Addr] = dest
|
||||
cacheSize := len(r.cache)
|
||||
r.mu.Unlock()
|
||||
|
||||
log.WithField("b32", b32Addr).WithField("cache_size", cacheSize).Debug("Hash resolved and cached")
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
// GetCached returns cached destination without performing lookup.
|
||||
// This allows checking if a hash has been previously resolved without triggering
|
||||
// a potentially expensive NAMING LOOKUP operation.
|
||||
//
|
||||
// Returns:
|
||||
// - destination: Full I2P destination if cached
|
||||
// - found: true if entry exists in cache, false otherwise
|
||||
//
|
||||
// This method is useful for applications that want to avoid network I/O and only
|
||||
// use already-resolved destinations. It's also useful for testing cache behavior.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// if dest, ok := resolver.GetCached(hash); ok {
|
||||
// // Use cached destination without network lookup
|
||||
// writer.SendDatagram(reply, dest)
|
||||
// } else {
|
||||
// // Hash not yet resolved - decide whether to resolve now
|
||||
// log.Info("Hash not in cache, resolution required for reply")
|
||||
// }
|
||||
func (r *HashResolver) GetCached(hash []byte) (i2pkeys.I2PAddr, bool) {
|
||||
if len(hash) != 32 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
b32Addr := hashToB32Address(hash)
|
||||
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
dest, ok := r.cache[b32Addr]
|
||||
return dest, ok
|
||||
}
|
||||
|
||||
// Clear removes all cached entries.
|
||||
// This is useful for testing, memory management in long-running sessions, or when
|
||||
// you want to force fresh NAMING LOOKUP operations.
|
||||
//
|
||||
// ⚠️ WARNING: Clearing cache will cause subsequent resolutions to perform network I/O.
|
||||
// ⚠️ Only clear cache if necessary (testing, memory pressure, or security concerns).
|
||||
//
|
||||
// Applications with memory constraints may want to implement periodic cache clearing
|
||||
// or LRU eviction policies on top of this basic cache.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// // Clear cache after processing batch
|
||||
// resolver.Clear()
|
||||
//
|
||||
// // Or clear periodically
|
||||
// ticker := time.NewTicker(1 * time.Hour)
|
||||
// go func() {
|
||||
// for range ticker.C {
|
||||
// resolver.Clear()
|
||||
// }
|
||||
// }()
|
||||
func (r *HashResolver) Clear() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
oldSize := len(r.cache)
|
||||
r.cache = make(map[string]i2pkeys.I2PAddr)
|
||||
log.WithField("old_size", oldSize).Debug("Cache cleared")
|
||||
}
|
||||
|
||||
// CacheSize returns the current number of cached entries.
|
||||
// This is useful for monitoring memory usage and cache effectiveness.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// size := resolver.CacheSize()
|
||||
// log.Info("Cache contains", size, "entries")
|
||||
func (r *HashResolver) CacheSize() int {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return len(r.cache)
|
||||
}
|
||||
|
||||
// hashToB32Address converts a 32-byte hash to a b32.i2p address string.
|
||||
// This performs base32 encoding (RFC 4648) and appends the ".b32.i2p" suffix.
|
||||
//
|
||||
// Format: <52-char-lowercase-base32>.b32.i2p
|
||||
//
|
||||
// The resulting address can be used for NAMING LOOKUP to resolve the full destination.
|
||||
// This is a pure utility function with no network I/O.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// hash := []byte{0x01, 0x02, ...} // 32 bytes
|
||||
// addr := hashToB32Address(hash)
|
||||
// // addr = "aebagbafaydqqcikbmgq...xyz.b32.i2p"
|
||||
func hashToB32Address(hash []byte) string {
|
||||
if len(hash) != 32 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Base32 encode the hash (RFC 4648 standard encoding)
|
||||
// This produces 52 characters for 32 bytes of input
|
||||
b32 := base32.StdEncoding.EncodeToString(hash)
|
||||
|
||||
// Convert to lowercase (I2P convention for b32 addresses)
|
||||
b32 = strings.ToLower(b32)
|
||||
|
||||
// Remove padding if present (= characters)
|
||||
b32 = strings.TrimRight(b32, "=")
|
||||
|
||||
// Append .b32.i2p suffix (I2P naming convention)
|
||||
return b32 + ".b32.i2p"
|
||||
}
|
||||
239
datagram3/session.go
Normal file
239
datagram3/session.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package datagram3
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-i2p/go-sam-go/common"
|
||||
"github.com/go-i2p/i2pkeys"
|
||||
"github.com/samber/oops"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// NewDatagram3Session creates a new datagram3 session with hash-based source identification.
|
||||
// It initializes the session with the provided SAM connection, session ID, cryptographic keys,
|
||||
// and configuration options. The session automatically creates a UDP listener for receiving
|
||||
// forwarded datagrams per SAMv3 requirements and initializes a hash resolver for source lookups.
|
||||
// Note: DATAGRAM3 sources are not authenticated; use datagram2 if authentication is required.
|
||||
// Example usage: session, err := NewDatagram3Session(sam, "my-session", keys, []string{"inbound.length=1"})
|
||||
func NewDatagram3Session(sam *common.SAM, id string, keys i2pkeys.I2PKeys, options []string) (*Datagram3Session, error) {
|
||||
// Log session creation with SECURITY WARNING
|
||||
logger := log.WithFields(logrus.Fields{
|
||||
"id": id,
|
||||
"style": "DATAGRAM3",
|
||||
"options": options,
|
||||
})
|
||||
logger.Warn("Creating DATAGRAM3 session: sources are UNAUTHENTICATED and can be spoofed!")
|
||||
logger.Debug("Creating new Datagram3Session with SAMv3 UDP forwarding")
|
||||
|
||||
// Create UDP listener for receiving forwarded datagrams (SAMv3 requirement)
|
||||
// The SAM bridge will forward incoming DATAGRAM3 messages to this local UDP port
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to resolve UDP address")
|
||||
return nil, oops.Errorf("failed to resolve UDP address: %w", err)
|
||||
}
|
||||
|
||||
udpConn, err := net.ListenUDP("udp", udpAddr)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to create UDP listener")
|
||||
return nil, oops.Errorf("failed to create UDP listener: %w", err)
|
||||
}
|
||||
|
||||
// Get the actual port assigned by the OS
|
||||
udpPort := udpConn.LocalAddr().(*net.UDPAddr).Port
|
||||
logger.WithField("udp_port", udpPort).Debug("Created UDP listener for datagram3 forwarding")
|
||||
|
||||
// Inject UDP forwarding parameters into session options (SAMv3 requirement)
|
||||
// PORT/HOST tell the SAM bridge where to forward received datagrams
|
||||
options = ensureUDPForwardingParameters(options, udpPort)
|
||||
|
||||
// CRITICAL: Use STYLE=DATAGRAM3 (not DATAGRAM or DATAGRAM2)
|
||||
// Create the base session using the common package for session management
|
||||
// This handles the underlying SAM protocol communication and session establishment
|
||||
session, err := sam.NewGenericSession("DATAGRAM3", id, keys, options)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to create generic session")
|
||||
udpConn.Close() // Clean up UDP listener on error
|
||||
return nil, oops.Errorf("failed to create datagram3 session: %w", err)
|
||||
}
|
||||
|
||||
// Ensure the session is of the correct type for datagram3 operations
|
||||
baseSession, ok := session.(*common.BaseSession)
|
||||
if !ok {
|
||||
logger.Error("Session is not a BaseSession")
|
||||
session.Close()
|
||||
udpConn.Close() // Clean up UDP listener on error
|
||||
return nil, oops.Errorf("invalid session type")
|
||||
}
|
||||
|
||||
// Initialize the datagram3 session with UDP forwarding enabled and hash resolver
|
||||
ds := &Datagram3Session{
|
||||
BaseSession: baseSession,
|
||||
sam: sam,
|
||||
options: options,
|
||||
udpConn: udpConn,
|
||||
udpEnabled: true, // Always true for SAMv3
|
||||
resolver: NewHashResolver(sam), // Initialize hash-to-destination cache
|
||||
}
|
||||
|
||||
logger.Debug("Successfully created Datagram3Session with UDP forwarding and hash resolver")
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
// ensureUDPForwardingParameters injects UDP forwarding parameters into session options if not already present.
|
||||
// This ensures SAMv3 UDP forwarding is configured with PORT and HOST parameters.
|
||||
// PORT/HOST specify where the SAM bridge should forward datagrams TO (the client's UDP listener).
|
||||
// sam.udp.port/sam.udp.host are NOT set here - they configure the SAM bridge's own UDP port (default 7655).
|
||||
// This is required for all datagram3 sessions in v3-only mode.
|
||||
func ensureUDPForwardingParameters(options []string, udpPort int) []string {
|
||||
updatedOptions := make([]string, 0, len(options)+2)
|
||||
|
||||
hasPort := false
|
||||
hasHost := false
|
||||
|
||||
// Check existing options
|
||||
for _, opt := range options {
|
||||
if strings.HasPrefix(opt, "PORT=") {
|
||||
hasPort = true
|
||||
} else if strings.HasPrefix(opt, "HOST=") {
|
||||
hasHost = true
|
||||
}
|
||||
updatedOptions = append(updatedOptions, opt)
|
||||
}
|
||||
|
||||
// Inject missing UDP forwarding parameters
|
||||
// PORT/HOST tell SAM bridge where to forward datagrams TO (our UDP listener)
|
||||
if !hasHost {
|
||||
updatedOptions = append(updatedOptions, "HOST=127.0.0.1")
|
||||
}
|
||||
if !hasPort {
|
||||
updatedOptions = append(updatedOptions, "PORT="+strconv.Itoa(udpPort))
|
||||
}
|
||||
|
||||
return updatedOptions
|
||||
}
|
||||
|
||||
// NewDatagram3SessionFromSubsession creates a Datagram3Session for a subsession that has already been
|
||||
// registered with a PRIMARY session using SESSION ADD. This constructor skips the session
|
||||
// creation step since the subsession is already registered with the SAM bridge.
|
||||
//
|
||||
// For PRIMARY datagram3 subsessions, UDP forwarding is mandatory (SAMv3 requirement).
|
||||
// The UDP connection must be provided for proper datagram reception.
|
||||
// Note: Sources are not authenticated; use NewDatagramSubSession if authentication is required.
|
||||
//
|
||||
// Example usage: sub, err := NewDatagram3SessionFromSubsession(sam, "sub1", keys, options, udpConn)
|
||||
func NewDatagram3SessionFromSubsession(sam *common.SAM, id string, keys i2pkeys.I2PKeys, options []string, udpConn *net.UDPConn) (*Datagram3Session, error) {
|
||||
logger := log.WithFields(logrus.Fields{
|
||||
"id": id,
|
||||
"style": "DATAGRAM3",
|
||||
"options": options,
|
||||
"udp_enabled": udpConn != nil,
|
||||
})
|
||||
logger.Warn("Creating DATAGRAM3 subsession: sources are UNAUTHENTICATED")
|
||||
logger.Debug("Creating Datagram3Session from existing subsession with SAMv3 UDP forwarding")
|
||||
|
||||
// Validate UDP connection is provided (mandatory for SAMv3 datagram3 subsessions)
|
||||
if udpConn == nil {
|
||||
logger.Error("UDP connection is required for SAMv3 datagram3 subsessions")
|
||||
return nil, oops.Errorf("udp connection is required for datagram3 subsessions (v3 only)")
|
||||
}
|
||||
|
||||
// Create a BaseSession manually since the subsession is already registered via SESSION ADD
|
||||
// This bypasses SESSION CREATE and uses the existing registration
|
||||
baseSession, err := common.NewBaseSessionFromSubsession(sam, id, keys)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to create base session from subsession")
|
||||
return nil, oops.Errorf("failed to create datagram3 session from subsession: %w", err)
|
||||
}
|
||||
|
||||
ds := &Datagram3Session{
|
||||
BaseSession: baseSession,
|
||||
sam: sam,
|
||||
options: options,
|
||||
udpConn: udpConn,
|
||||
udpEnabled: true, // Always true for SAMv3
|
||||
resolver: NewHashResolver(sam), // Initialize hash-to-destination cache
|
||||
}
|
||||
|
||||
logger.Debug("Successfully created Datagram3Session from subsession with UDP forwarding and hash resolver")
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
// NewReader creates a Datagram3Reader for receiving datagrams with hash-based sources.
|
||||
// This method initializes a new reader with buffered channels for asynchronous datagram
|
||||
// reception. The reader must be started manually with receiveLoop() for continuous operation.
|
||||
// Received datagrams contain 32-byte hashes; call ResolveSource() to obtain full destinations for replies.
|
||||
// Example usage: reader := session.NewReader(); go reader.receiveLoop(); datagram, err := reader.ReceiveDatagram()
|
||||
func (s *Datagram3Session) NewReader() *Datagram3Reader {
|
||||
// Create reader with buffered channels for non-blocking operation
|
||||
// The buffer size of 10 prevents blocking when multiple datagrams arrive rapidly
|
||||
return &Datagram3Reader{
|
||||
session: s,
|
||||
recvChan: make(chan *Datagram3, 10), // Buffer for incoming datagrams
|
||||
errorChan: make(chan error, 1),
|
||||
closeChan: make(chan struct{}),
|
||||
doneChan: make(chan struct{}, 1),
|
||||
closed: false,
|
||||
loopStarted: false,
|
||||
mu: sync.RWMutex{},
|
||||
closeOnce: sync.Once{},
|
||||
}
|
||||
}
|
||||
|
||||
// NewWriter creates a Datagram3Writer for sending datagrams to I2P destinations.
|
||||
// This method initializes a new writer with a default timeout of 30 seconds for send operations.
|
||||
// The timeout can be customized using the SetTimeout method on the returned writer.
|
||||
// Example usage: writer := session.NewWriter().SetTimeout(60*time.Second); err := writer.SendDatagram(data, dest)
|
||||
func (s *Datagram3Session) NewWriter() *Datagram3Writer {
|
||||
// Initialize writer with default timeout for send operations
|
||||
// The timeout prevents indefinite blocking on send operations
|
||||
return &Datagram3Writer{
|
||||
session: s,
|
||||
timeout: 30, // Default timeout in seconds
|
||||
}
|
||||
}
|
||||
|
||||
// Close terminates the datagram3 session and cleans up all resources.
|
||||
// This method ensures proper cleanup of the UDP connection and I2P tunnels.
|
||||
// After calling Close(), the session cannot be reused.
|
||||
// Example usage: defer session.Close()
|
||||
func (s *Datagram3Session) Close() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.closed {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.WithField("session", s.ID()).Debug("Closing Datagram3Session")
|
||||
|
||||
// Close UDP connection if present
|
||||
if s.udpConn != nil {
|
||||
if err := s.udpConn.Close(); err != nil {
|
||||
log.WithError(err).Warn("Error closing UDP connection")
|
||||
}
|
||||
}
|
||||
|
||||
// Clear hash resolver cache to free memory
|
||||
if s.resolver != nil {
|
||||
s.resolver.Clear()
|
||||
}
|
||||
|
||||
// Close base session (closes I2P tunnels)
|
||||
if err := s.BaseSession.Close(); err != nil {
|
||||
return oops.Errorf("failed to close base session: %w", err)
|
||||
}
|
||||
|
||||
s.closed = true
|
||||
log.WithField("session", s.ID()).Debug("Datagram3Session closed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Addr returns the local I2P address of this datagram3 session.
|
||||
// This is the destination address that other I2P nodes can use to send datagrams to this session.
|
||||
func (s *Datagram3Session) Addr() i2pkeys.I2PAddr {
|
||||
return s.Keys().Addr()
|
||||
}
|
||||
453
datagram3/session_test.go
Normal file
453
datagram3/session_test.go
Normal file
@@ -0,0 +1,453 @@
|
||||
package datagram3
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-i2p/go-sam-go/common"
|
||||
"github.com/go-i2p/i2pkeys"
|
||||
)
|
||||
|
||||
const testSAMAddr = "127.0.0.1:7656"
|
||||
|
||||
func setupTestSAM(t *testing.T) (*common.SAM, i2pkeys.I2PKeys) {
|
||||
t.Helper()
|
||||
|
||||
sam, err := common.NewSAM(testSAMAddr)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create SAM connection: %v", err)
|
||||
}
|
||||
|
||||
keys, err := sam.NewKeys()
|
||||
if err != nil {
|
||||
sam.Close()
|
||||
t.Fatalf("Failed to generate keys: %v", err)
|
||||
}
|
||||
|
||||
return sam, keys
|
||||
}
|
||||
|
||||
// generateUniqueSessionID creates a unique session ID to prevent conflicts during concurrent test execution.
|
||||
// This ensures test isolation when multiple tests run simultaneously (e.g., during race detection).
|
||||
// DATAGRAM3 sessions use a unique prefix to distinguish from other datagram types.
|
||||
func generateUniqueSessionID(testName string) string {
|
||||
// Use timestamp (nanoseconds) and random number to ensure uniqueness across concurrent executions
|
||||
timestamp := time.Now().UnixNano()
|
||||
random := rand.Intn(99999)
|
||||
return fmt.Sprintf("dg3_%s_%d_%05d", testName, timestamp, random)
|
||||
}
|
||||
|
||||
func TestNewDatagram3Session(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping I2P integration test in short mode")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
idBase string
|
||||
options []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "basic datagram3 session creation",
|
||||
idBase: "test_session",
|
||||
options: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "session with custom options",
|
||||
idBase: "test_with_opts",
|
||||
options: []string{"inbound.length=1", "outbound.length=1"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "session with small tunnel config",
|
||||
idBase: "test_small",
|
||||
options: []string{
|
||||
"inbound.length=1",
|
||||
"outbound.length=1",
|
||||
"inbound.lengthVariance=0",
|
||||
"outbound.lengthVariance=0",
|
||||
"inbound.quantity=1",
|
||||
"outbound.quantity=1",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "session with Ed25519 signature",
|
||||
idBase: "test_ed25519",
|
||||
options: []string{
|
||||
"inbound.length=0",
|
||||
"outbound.length=0",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sam, keys := setupTestSAM(t)
|
||||
defer sam.Close()
|
||||
|
||||
// Generate unique session ID to prevent conflicts during concurrent test execution
|
||||
sessionID := generateUniqueSessionID(tt.idBase)
|
||||
|
||||
t.Logf("Creating DATAGRAM3 session with ID: %s", sessionID)
|
||||
t.Logf("⚠️ SECURITY WARNING: DATAGRAM3 sources are UNAUTHENTICATED")
|
||||
t.Logf("Note: I2P tunnel establishment can take 2-5 minutes")
|
||||
|
||||
session, err := NewDatagram3Session(sam, sessionID, keys, tt.options)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("NewDatagram3Session() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return // Expected error, test passed
|
||||
}
|
||||
|
||||
// Verify session was created
|
||||
if session == nil {
|
||||
t.Fatal("Expected non-nil session")
|
||||
}
|
||||
|
||||
// Verify session has resolver
|
||||
if session.resolver == nil {
|
||||
t.Error("Expected session to have resolver")
|
||||
}
|
||||
|
||||
// Verify resolver has empty cache initially
|
||||
if session.resolver.CacheSize() != 0 {
|
||||
t.Errorf("Expected empty cache initially, got size %d", session.resolver.CacheSize())
|
||||
}
|
||||
|
||||
// Verify UDP connection was established
|
||||
if session.udpConn == nil {
|
||||
t.Error("Expected UDP connection to be established")
|
||||
}
|
||||
|
||||
// Verify session address is valid
|
||||
addr := session.Addr()
|
||||
if addr == "" {
|
||||
t.Error("Expected non-empty session address")
|
||||
}
|
||||
|
||||
t.Logf("Session created successfully with address: %s", addr)
|
||||
|
||||
// Clean up
|
||||
err = session.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Failed to close session: %v", err)
|
||||
}
|
||||
|
||||
// Verify resolver cache cleared after close
|
||||
if session.resolver.CacheSize() != 0 {
|
||||
t.Errorf("Expected cache cleared after close, got size %d", session.resolver.CacheSize())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatagram3SessionClose(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping I2P integration test in short mode")
|
||||
}
|
||||
|
||||
sam, keys := setupTestSAM(t)
|
||||
defer sam.Close()
|
||||
|
||||
sessionID := generateUniqueSessionID("test_close")
|
||||
|
||||
t.Logf("Creating DATAGRAM3 session for close test: %s", sessionID)
|
||||
session, err := NewDatagram3Session(sam, sessionID, keys, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session: %v", err)
|
||||
}
|
||||
|
||||
// Add some entries to cache
|
||||
testHash := make([]byte, 32)
|
||||
for i := range testHash {
|
||||
testHash[i] = byte(i)
|
||||
}
|
||||
b32 := hashToB32Address(testHash)
|
||||
session.resolver.cache[b32] = i2pkeys.I2PAddr("test-destination")
|
||||
|
||||
if session.resolver.CacheSize() != 1 {
|
||||
t.Errorf("Expected cache size 1, got %d", session.resolver.CacheSize())
|
||||
}
|
||||
|
||||
// Close session
|
||||
err = session.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Failed to close session: %v", err)
|
||||
}
|
||||
|
||||
// Verify cache cleared
|
||||
if session.resolver.CacheSize() != 0 {
|
||||
t.Errorf("Expected cache cleared after close, got size %d", session.resolver.CacheSize())
|
||||
}
|
||||
|
||||
// Verify double-close doesn't panic or error
|
||||
err = session.Close()
|
||||
if err != nil {
|
||||
t.Logf("Second close returned error (acceptable): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatagram3SessionReaderWriter(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping I2P integration test in short mode")
|
||||
}
|
||||
|
||||
sam, keys := setupTestSAM(t)
|
||||
defer sam.Close()
|
||||
|
||||
sessionID := generateUniqueSessionID("test_rw")
|
||||
|
||||
t.Logf("Creating DATAGRAM3 session for reader/writer test: %s", sessionID)
|
||||
session, err := NewDatagram3Session(sam, sessionID, keys, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session: %v", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
// Create reader
|
||||
reader := session.NewReader()
|
||||
if reader == nil {
|
||||
t.Fatal("Expected non-nil reader")
|
||||
}
|
||||
if reader.session != session {
|
||||
t.Error("Reader session mismatch")
|
||||
}
|
||||
|
||||
// Create writer
|
||||
writer := session.NewWriter()
|
||||
if writer == nil {
|
||||
t.Fatal("Expected non-nil writer")
|
||||
}
|
||||
if writer.session != session {
|
||||
t.Error("Writer session mismatch")
|
||||
}
|
||||
|
||||
// Test writer timeout configuration
|
||||
writer2 := session.NewWriter().SetTimeout(10 * time.Second)
|
||||
if writer2.timeout != 10*time.Second {
|
||||
t.Errorf("Expected timeout 10s, got %v", writer2.timeout)
|
||||
}
|
||||
|
||||
// Clean up reader
|
||||
err = reader.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Failed to close reader: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDatagram3SessionConcurrentAccess tests concurrent operations on session
|
||||
func TestDatagram3SessionConcurrentAccess(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping I2P integration test in short mode")
|
||||
}
|
||||
|
||||
sam, keys := setupTestSAM(t)
|
||||
defer sam.Close()
|
||||
|
||||
sessionID := generateUniqueSessionID("test_concurrent")
|
||||
|
||||
t.Logf("Creating DATAGRAM3 session for concurrency test: %s", sessionID)
|
||||
session, err := NewDatagram3Session(sam, sessionID, keys, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session: %v", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
// Create multiple readers and writers concurrently
|
||||
done := make(chan bool, 10)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
go func(n int) {
|
||||
reader := session.NewReader()
|
||||
if reader == nil {
|
||||
t.Errorf("Reader %d: got nil reader", n)
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
reader.Close()
|
||||
done <- true
|
||||
}(i)
|
||||
}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
go func(n int) {
|
||||
writer := session.NewWriter()
|
||||
if writer == nil {
|
||||
t.Errorf("Writer %d: got nil writer", n)
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
done <- true
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all goroutines
|
||||
for i := 0; i < 10; i++ {
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("Timeout waiting for concurrent operations")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDatagram3RoundTrip tests sending and receiving datagrams with hash resolution
|
||||
func TestDatagram3RoundTrip(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping I2P integration test in short mode")
|
||||
}
|
||||
|
||||
// This test requires two I2P sessions to communicate
|
||||
// Create session A
|
||||
samA, keysA := setupTestSAM(t)
|
||||
defer samA.Close()
|
||||
|
||||
sessionA_ID := generateUniqueSessionID("alice")
|
||||
t.Logf("Creating session A (Alice): %s", sessionA_ID)
|
||||
sessionA, err := NewDatagram3Session(samA, sessionA_ID, keysA, []string{
|
||||
"inbound.length=0",
|
||||
"outbound.length=0",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session A: %v", err)
|
||||
}
|
||||
defer sessionA.Close()
|
||||
|
||||
// Create session B
|
||||
samB, keysB := setupTestSAM(t)
|
||||
defer samB.Close()
|
||||
|
||||
sessionB_ID := generateUniqueSessionID("bob")
|
||||
t.Logf("Creating session B (Bob): %s", sessionB_ID)
|
||||
sessionB, err := NewDatagram3Session(samB, sessionB_ID, keysB, []string{
|
||||
"inbound.length=0",
|
||||
"outbound.length=0",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session B: %v", err)
|
||||
}
|
||||
defer sessionB.Close()
|
||||
|
||||
t.Logf("Alice address: %s", sessionA.Addr())
|
||||
t.Logf("Bob address: %s", sessionB.Addr())
|
||||
|
||||
// Start reader on B
|
||||
readerB := sessionB.NewReader()
|
||||
defer readerB.Close()
|
||||
|
||||
receivedChan := make(chan *Datagram3, 1)
|
||||
errorChan := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
dg, err := readerB.ReceiveDatagram()
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
return
|
||||
}
|
||||
receivedChan <- dg
|
||||
}()
|
||||
|
||||
// Give reader time to start
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// Send from A to B
|
||||
writerA := sessionA.NewWriter()
|
||||
testMessage := []byte("Hello from Alice! This is an UNAUTHENTICATED DATAGRAM3 message.")
|
||||
|
||||
t.Logf("Alice sending to Bob...")
|
||||
err = writerA.SendDatagram(testMessage, sessionB.Addr())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to send datagram: %v", err)
|
||||
}
|
||||
|
||||
// Wait for reception with generous timeout for I2P
|
||||
t.Logf("Waiting for Bob to receive (up to 60 seconds)...")
|
||||
select {
|
||||
case dg := <-receivedChan:
|
||||
t.Logf("Bob received datagram!")
|
||||
|
||||
// Verify data
|
||||
if string(dg.Data) != string(testMessage) {
|
||||
t.Errorf("Data mismatch: got %q, want %q", string(dg.Data), string(testMessage))
|
||||
}
|
||||
|
||||
// Verify hash is present
|
||||
if len(dg.SourceHash) != 32 {
|
||||
t.Errorf("Expected 32-byte source hash, got %d bytes", len(dg.SourceHash))
|
||||
}
|
||||
|
||||
// Test GetSourceB32
|
||||
b32 := dg.GetSourceB32()
|
||||
if len(b32) != 60 {
|
||||
t.Errorf("Expected 60-char b32 address, got %d", len(b32))
|
||||
}
|
||||
t.Logf("Source b32: %s", b32)
|
||||
|
||||
// ⚠️ SECURITY TEST: Verify source is UNAUTHENTICATED
|
||||
t.Logf("⚠️ SECURITY NOTE: This source hash is UNAUTHENTICATED and could be spoofed!")
|
||||
t.Logf("Source hash: %x", dg.SourceHash)
|
||||
|
||||
// Test hash resolution (requires NAMING LOOKUP)
|
||||
t.Logf("Resolving source hash to full destination...")
|
||||
err = dg.ResolveSource(sessionB)
|
||||
if err != nil {
|
||||
// This might fail if the destination isn't published yet
|
||||
t.Logf("Hash resolution failed (may be expected for new sessions): %v", err)
|
||||
} else {
|
||||
t.Logf("Resolved source: %s", dg.Source)
|
||||
|
||||
// Verify it's in cache now
|
||||
cachedDest, ok := sessionB.resolver.GetCached(dg.SourceHash)
|
||||
if !ok {
|
||||
t.Error("Expected resolved destination in cache")
|
||||
} else {
|
||||
t.Logf("Cached destination: %s", cachedDest)
|
||||
}
|
||||
|
||||
// Test sending reply
|
||||
t.Logf("Bob sending reply to Alice...")
|
||||
writerB := sessionB.NewWriter()
|
||||
replyMessage := []byte("Reply from Bob! Source verification is YOUR responsibility!")
|
||||
err = writerB.ReplyToDatagram(replyMessage, dg)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to send reply: %v", err)
|
||||
} else {
|
||||
t.Logf("Reply sent successfully")
|
||||
}
|
||||
}
|
||||
|
||||
case err := <-errorChan:
|
||||
t.Fatalf("Error receiving datagram: %v", err)
|
||||
|
||||
case <-time.After(60 * time.Second):
|
||||
t.Fatal("Timeout waiting for datagram (I2P may be congested or tunnels not ready)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDatagram3UnauthenticatedSourceWarning documents the unauthenticated nature
|
||||
func TestDatagram3UnauthenticatedSourceWarning(t *testing.T) {
|
||||
t.Log("⚠️ SECURITY WARNING TEST")
|
||||
t.Log("=" + string(make([]byte, 78)))
|
||||
t.Log("DATAGRAM3 sources are UNAUTHENTICATED and can be SPOOFED!")
|
||||
t.Log("")
|
||||
t.Log("This test documents that:")
|
||||
t.Log(" 1. Source addresses use 32-byte hashes, not verified destinations")
|
||||
t.Log(" 2. Any attacker can claim to be any sender")
|
||||
t.Log(" 3. Applications MUST NOT trust source identity without verification")
|
||||
t.Log(" 4. Use DATAGRAM2 if source authentication is required")
|
||||
t.Log("")
|
||||
t.Log("DATAGRAM3 is appropriate ONLY when:")
|
||||
t.Log(" - Source authentication handled at application layer")
|
||||
t.Log(" - Source identity not critical to security")
|
||||
t.Log(" - Low overhead more important than authentication")
|
||||
t.Log("=" + string(make([]byte, 78)))
|
||||
|
||||
// This is a documentation test, always passes
|
||||
t.Log("✓ Security warning documented")
|
||||
}
|
||||
300
datagram3/types.go
Normal file
300
datagram3/types.go
Normal file
@@ -0,0 +1,300 @@
|
||||
package datagram3
|
||||
|
||||
import (
|
||||
"net"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-i2p/go-sam-go/common"
|
||||
"github.com/go-i2p/i2pkeys"
|
||||
"github.com/samber/oops"
|
||||
)
|
||||
|
||||
// Datagram3Session represents a repliable but UNAUTHENTICATED datagram3 session.
|
||||
//
|
||||
// ⚠️ CRITICAL SECURITY WARNING: Source addresses are NOT authenticated and can be spoofed!
|
||||
// ⚠️ Applications requiring source authentication MUST use DATAGRAM2 instead.
|
||||
// ⚠️ Do NOT trust source identity without additional application-level authentication.
|
||||
//
|
||||
// DATAGRAM3 provides UDP-like messaging with hash-based source identification instead of
|
||||
// full authenticated destinations. This reduces overhead at the cost of source verification.
|
||||
// Received datagrams contain a 32-byte hash that requires NAMING LOOKUP to resolve for replies.
|
||||
//
|
||||
// Key differences from DATAGRAM/DATAGRAM2:
|
||||
// - Repliable: Can reply to sender (like DATAGRAM/DATAGRAM2)
|
||||
// - Unauthenticated: Source is NOT verified (unlike DATAGRAM/DATAGRAM2)
|
||||
// - Hash-based source: 32-byte hash instead of full destination
|
||||
// - Lower overhead: No signature verification required
|
||||
// - Reply overhead: Requires NAMING LOOKUP to resolve hash
|
||||
//
|
||||
// The session manages I2P tunnels and provides methods for creating readers and writers.
|
||||
// For SAMv3 mode, it uses UDP forwarding where datagrams are received via a local UDP socket
|
||||
// that the SAM bridge forwards to. The session maintains a hash resolver cache to avoid
|
||||
// repeated NAMING LOOKUP operations when replying to the same source.
|
||||
//
|
||||
// I2P Timing Considerations:
|
||||
// - Session creation: 2-5 minutes for tunnel establishment
|
||||
// - Message delivery: Variable latency (network-dependent)
|
||||
// - Hash resolution: Additional network round-trip for NAMING LOOKUP
|
||||
// - Use generous timeouts and retry logic with exponential backoff
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
// defer cancel()
|
||||
// session, err := NewDatagram3Session(sam, "my-session", keys, options)
|
||||
// reader := session.NewReader()
|
||||
// dg, err := reader.ReceiveDatagram()
|
||||
// // dg.SourceHash is UNAUTHENTICATED - verify separately if needed!
|
||||
// if err := dg.ResolveSource(session); err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// session.NewWriter().SendDatagram(reply, dg.Source)
|
||||
type Datagram3Session struct {
|
||||
*common.BaseSession
|
||||
sam *common.SAM
|
||||
options []string
|
||||
mu sync.RWMutex
|
||||
closed bool
|
||||
udpConn *net.UDPConn // UDP connection for receiving forwarded datagrams (SAMv3 mode)
|
||||
udpEnabled bool // Whether UDP forwarding is enabled (always true for SAMv3)
|
||||
resolver *HashResolver // Cache for hash-to-destination lookups
|
||||
}
|
||||
|
||||
// Datagram3Reader handles incoming UNAUTHENTICATED datagram3 reception from I2P.
|
||||
//
|
||||
// ⚠️ SECURITY WARNING: All received datagrams have UNAUTHENTICATED sources!
|
||||
// ⚠️ The SourceHash field in received datagrams can be spoofed by attackers.
|
||||
// ⚠️ Do not trust source identity without additional verification.
|
||||
//
|
||||
// The reader provides asynchronous datagram reception through buffered channels, allowing
|
||||
// applications to receive datagrams without blocking. It manages its own goroutine for
|
||||
// continuous message processing and provides thread-safe access to received datagrams.
|
||||
//
|
||||
// Unlike DATAGRAM/DATAGRAM2, sources are represented as 32-byte hashes rather than full
|
||||
// destinations. Applications must call ResolveSource() on received datagrams to obtain
|
||||
// the full destination for replies. The session's resolver cache minimizes lookup overhead.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// reader := session.NewReader()
|
||||
// for {
|
||||
// datagram, err := reader.ReceiveDatagram()
|
||||
// if err != nil {
|
||||
// // Handle error
|
||||
// }
|
||||
// // SECURITY: datagram.SourceHash is UNAUTHENTICATED!
|
||||
// // Verify using application-layer authentication before trusting
|
||||
// if err := datagram.ResolveSource(session); err != nil {
|
||||
// // Handle resolution error
|
||||
// }
|
||||
// // Now datagram.Source contains full destination for reply
|
||||
// }
|
||||
type Datagram3Reader struct {
|
||||
session *Datagram3Session
|
||||
recvChan chan *Datagram3
|
||||
errorChan chan error
|
||||
closeChan chan struct{}
|
||||
doneChan chan struct{}
|
||||
closed bool
|
||||
loopStarted bool
|
||||
mu sync.RWMutex
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
// Datagram3Writer handles outgoing datagram3 transmission to I2P destinations.
|
||||
// It provides methods for sending datagrams with configurable timeouts and handles
|
||||
// the underlying SAM protocol communication for message delivery. The writer supports
|
||||
// method chaining for configuration and provides error handling for send operations.
|
||||
//
|
||||
// Maximum datagram size is 31744 bytes total (including headers), with 11 KB recommended
|
||||
// for best reliability. Destinations can be specified as full base64 destinations,
|
||||
// hostnames (.i2p), or b32 addresses.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// writer := session.NewWriter().SetTimeout(30*time.Second)
|
||||
// err := writer.SendDatagram(data, destination)
|
||||
type Datagram3Writer struct {
|
||||
session *Datagram3Session
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// Datagram3 represents an I2P datagram3 message with UNAUTHENTICATED source.
|
||||
//
|
||||
// ⚠️ CRITICAL SECURITY WARNING: SourceHash is NOT authenticated and can be spoofed!
|
||||
// ⚠️ Any malicious actor can claim to be any source by providing a fake hash.
|
||||
// ⚠️ Applications MUST implement their own authentication if source identity matters.
|
||||
// ⚠️ Use DATAGRAM2 if you need cryptographically authenticated sources.
|
||||
//
|
||||
// This structure encapsulates the payload data along with the unauthenticated source hash
|
||||
// and optional resolved destination. The SourceHash is always present (32 bytes), while
|
||||
// Source is only populated after calling ResolveSource() to perform NAMING LOOKUP.
|
||||
//
|
||||
// Fields:
|
||||
// - Data: Raw datagram payload (up to ~31KB)
|
||||
// - SourceHash: 32-byte UNAUTHENTICATED hash of sender (spoofable!)
|
||||
// - Source: Resolved full destination (nil until ResolveSource() called)
|
||||
// - Local: Local destination (this session)
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// // Received datagram has only hash, not full source
|
||||
// log.Warn("Received from UNAUTHENTICATED hash:", hex.EncodeToString(dg.SourceHash))
|
||||
//
|
||||
// // Resolve hash to full destination for reply
|
||||
// if err := dg.ResolveSource(session); err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// // Now can reply using resolved source (still unverified!)
|
||||
// writer.SendDatagram(reply, dg.Source)
|
||||
type Datagram3 struct {
|
||||
Data []byte // Raw datagram payload (up to ~31KB)
|
||||
SourceHash []byte // 32-byte UNAUTHENTICATED hash (spoofable!)
|
||||
Source i2pkeys.I2PAddr // Resolved destination (nil until ResolveSource)
|
||||
Local i2pkeys.I2PAddr // Local destination (this session)
|
||||
}
|
||||
|
||||
// ResolveSource resolves the source hash to a full I2P destination for replying.
|
||||
// This performs a NAMING LOOKUP to convert the 32-byte hash into a full destination
|
||||
// address. The operation is cached in the session's resolver to avoid repeated lookups.
|
||||
//
|
||||
// ⚠️ SECURITY WARNING: Resolving the hash does NOT authenticate the source!
|
||||
// ⚠️ Even with full destination, the source can still be spoofed.
|
||||
// ⚠️ This method only enables replies, it does NOT verify identity.
|
||||
//
|
||||
// Process:
|
||||
// 1. Check if already resolved (Source not nil)
|
||||
// 2. Validate SourceHash is 32 bytes
|
||||
// 3. Convert hash to b32.i2p address (base32 encoding)
|
||||
// 4. Perform NAMING LOOKUP via SAM bridge
|
||||
// 5. Cache result in session resolver
|
||||
// 6. Populate Source field with full destination
|
||||
//
|
||||
// This is an expensive operation (network round-trip) so results are cached.
|
||||
// Applications replying to the same source repeatedly benefit from caching.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// if err := datagram.ResolveSource(session); err != nil {
|
||||
// log.Error("Failed to resolve source:", err)
|
||||
// return err
|
||||
// }
|
||||
// // datagram.Source now contains full destination
|
||||
func (d *Datagram3) ResolveSource(session *Datagram3Session) error {
|
||||
// Validate input
|
||||
if session == nil {
|
||||
return oops.Errorf("session cannot be nil")
|
||||
}
|
||||
if len(d.SourceHash) == 0 {
|
||||
return oops.Errorf("no source hash available")
|
||||
}
|
||||
|
||||
// Check if already resolved (I2PAddr is a string type, empty = not resolved)
|
||||
if d.Source != "" {
|
||||
return nil // Already resolved
|
||||
}
|
||||
|
||||
// Resolve via session resolver (uses cache)
|
||||
dest, err := session.resolver.ResolveHash(d.SourceHash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.Source = dest
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSourceB32 returns the b32.i2p address for the source hash without full resolution.
|
||||
// This converts the 32-byte hash to a base32-encoded .b32.i2p address string without
|
||||
// performing NAMING LOOKUP. This is faster than full resolution and sufficient for
|
||||
// display, logging, or caching purposes.
|
||||
//
|
||||
// ⚠️ SECURITY WARNING: The returned address is still UNAUTHENTICATED!
|
||||
// ⚠️ This method does not add source verification.
|
||||
//
|
||||
// Returns empty string if SourceHash is invalid (not 32 bytes).
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// b32Addr := datagram.GetSourceB32()
|
||||
// log.Info("Received from (unverified):", b32Addr)
|
||||
func (d *Datagram3) GetSourceB32() string {
|
||||
if len(d.SourceHash) != 32 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return hashToB32Address(d.SourceHash)
|
||||
}
|
||||
|
||||
// Datagram3Addr implements net.Addr interface for I2P datagram3 addresses.
|
||||
//
|
||||
// ⚠️ SECURITY WARNING: If constructed from received hash, this address is UNAUTHENTICATED!
|
||||
// ⚠️ Do not trust the address for security-critical operations without additional verification.
|
||||
//
|
||||
// This type provides standard Go networking address representation for I2P destinations,
|
||||
// allowing seamless integration with existing Go networking code that expects net.Addr.
|
||||
// The address can wrap either a full I2P destination or just a hash from reception.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// addr := &Datagram3Addr{addr: destination, hash: sourceHash}
|
||||
// fmt.Println(addr.Network(), addr.String())
|
||||
type Datagram3Addr struct {
|
||||
addr i2pkeys.I2PAddr
|
||||
hash []byte // Original 32-byte hash if from reception (UNAUTHENTICATED!)
|
||||
}
|
||||
|
||||
// Network returns the network type for I2P datagram3 addresses.
|
||||
// This implements the net.Addr interface by returning "datagram3" as the network type.
|
||||
func (a *Datagram3Addr) Network() string {
|
||||
return "datagram3"
|
||||
}
|
||||
|
||||
// String returns the string representation of the I2P address.
|
||||
// This implements the net.Addr interface. If a full address is available, returns base32
|
||||
// representation. If only hash is available, returns the b32.i2p derived address.
|
||||
//
|
||||
// ⚠️ SECURITY WARNING: Hash-derived addresses are UNAUTHENTICATED!
|
||||
func (a *Datagram3Addr) String() string {
|
||||
// Return full address if available (I2PAddr is a string type, empty = not available)
|
||||
if a.addr != "" {
|
||||
return a.addr.Base32()
|
||||
}
|
||||
// Fall back to hash-derived b32 address if only hash available
|
||||
if len(a.hash) == 32 {
|
||||
return hashToB32Address(a.hash)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Datagram3Conn implements net.PacketConn interface for I2P datagram3 communication.
|
||||
//
|
||||
// ⚠️ SECURITY WARNING: All sources received via this connection are UNAUTHENTICATED!
|
||||
// ⚠️ Applications MUST implement their own authentication layer if source identity matters.
|
||||
//
|
||||
// This type provides compatibility with standard Go networking patterns by wrapping
|
||||
// datagram3 session functionality in a familiar PacketConn interface. It manages
|
||||
// internal readers and writers while providing standard connection operations.
|
||||
//
|
||||
// The connection provides thread-safe concurrent access to I2P datagram3 operations
|
||||
// and properly handles cleanup on close. Unlike DATAGRAM/DATAGRAM2, sources are
|
||||
// hash-based and not cryptographically verified.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// conn := session.PacketConn()
|
||||
// n, addr, err := conn.ReadFrom(buffer)
|
||||
// // addr represents UNAUTHENTICATED source!
|
||||
// n, err = conn.WriteTo(data, destination)
|
||||
type Datagram3Conn struct {
|
||||
session *Datagram3Session
|
||||
reader *Datagram3Reader
|
||||
writer *Datagram3Writer
|
||||
remoteAddr *i2pkeys.I2PAddr
|
||||
mu sync.RWMutex
|
||||
closed bool
|
||||
cleanup runtime.Cleanup
|
||||
}
|
||||
310
datagram3/types_test.go
Normal file
310
datagram3/types_test.go
Normal file
@@ -0,0 +1,310 @@
|
||||
package datagram3
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/go-i2p/go-sam-go/common"
|
||||
"github.com/go-i2p/i2pkeys"
|
||||
)
|
||||
|
||||
// Compile-time interface checks to ensure types implement expected interfaces
|
||||
var (
|
||||
_ common.Session = &Datagram3Session{}
|
||||
_ net.PacketConn = &Datagram3Conn{}
|
||||
_ net.Addr = &Datagram3Addr{}
|
||||
)
|
||||
|
||||
// TestDatagram3AddrImplementsNetAddr verifies Datagram3Addr properly implements net.Addr
|
||||
func TestDatagram3AddrImplementsNetAddr(t *testing.T) {
|
||||
// Test with nil/empty address
|
||||
addr1 := &Datagram3Addr{}
|
||||
if addr1.Network() != "datagram3" {
|
||||
t.Errorf("Expected network 'datagram3', got %q", addr1.Network())
|
||||
}
|
||||
|
||||
// Test with hash
|
||||
hash := make([]byte, 32)
|
||||
for i := range hash {
|
||||
hash[i] = byte(i)
|
||||
}
|
||||
addr2 := &Datagram3Addr{hash: hash}
|
||||
if addr2.Network() != "datagram3" {
|
||||
t.Errorf("Expected network 'datagram3', got %q", addr2.Network())
|
||||
}
|
||||
|
||||
// Test String() returns something
|
||||
if addr2.String() == "" {
|
||||
t.Error("Expected non-empty string representation")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDatagram3GetSourceB32 tests the hash-to-b32 conversion without full resolution
|
||||
func TestDatagram3GetSourceB32(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hash []byte
|
||||
wantLen int
|
||||
wantErr bool
|
||||
wantZero bool
|
||||
}{
|
||||
{
|
||||
name: "valid 32-byte hash",
|
||||
hash: make([]byte, 32),
|
||||
wantLen: 60, // 52 chars base32 + ".b32.i2p" (8 chars)
|
||||
wantErr: false,
|
||||
wantZero: false,
|
||||
},
|
||||
{
|
||||
name: "nil hash",
|
||||
hash: nil,
|
||||
wantLen: 0,
|
||||
wantErr: false,
|
||||
wantZero: true,
|
||||
},
|
||||
{
|
||||
name: "empty hash",
|
||||
hash: []byte{},
|
||||
wantLen: 0,
|
||||
wantErr: false,
|
||||
wantZero: true,
|
||||
},
|
||||
{
|
||||
name: "invalid hash length (31 bytes)",
|
||||
hash: make([]byte, 31),
|
||||
wantLen: 0,
|
||||
wantErr: false,
|
||||
wantZero: true,
|
||||
},
|
||||
{
|
||||
name: "invalid hash length (33 bytes)",
|
||||
hash: make([]byte, 33),
|
||||
wantLen: 0,
|
||||
wantErr: false,
|
||||
wantZero: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dg := &Datagram3{
|
||||
SourceHash: tt.hash,
|
||||
}
|
||||
|
||||
result := dg.GetSourceB32()
|
||||
|
||||
if tt.wantZero {
|
||||
if result != "" {
|
||||
t.Errorf("Expected empty string for invalid hash, got %q", result)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if len(result) != tt.wantLen {
|
||||
t.Errorf("Expected b32 address length %d, got %d", tt.wantLen, len(result))
|
||||
}
|
||||
|
||||
// Verify it ends with .b32.i2p
|
||||
if len(result) > 0 && result[len(result)-8:] != ".b32.i2p" {
|
||||
t.Errorf("Expected address to end with '.b32.i2p', got %q", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDatagram3ResolveSourceValidation tests validation before resolution
|
||||
func TestDatagram3ResolveSourceValidation(t *testing.T) {
|
||||
// Create a dummy session for testing
|
||||
dummySession := &Datagram3Session{
|
||||
resolver: &HashResolver{
|
||||
sam: nil,
|
||||
cache: make(map[string]i2pkeys.I2PAddr),
|
||||
},
|
||||
}
|
||||
|
||||
// Test with nil session
|
||||
dg0 := &Datagram3{SourceHash: make([]byte, 32)}
|
||||
err := dg0.ResolveSource(nil)
|
||||
if err == nil {
|
||||
t.Error("Expected error when session is nil")
|
||||
}
|
||||
|
||||
// Test with no hash
|
||||
dg1 := &Datagram3{}
|
||||
err = dg1.ResolveSource(dummySession)
|
||||
if err == nil {
|
||||
t.Error("Expected error when resolving without hash")
|
||||
}
|
||||
|
||||
// Test with already resolved source
|
||||
dg2 := &Datagram3{
|
||||
SourceHash: make([]byte, 32),
|
||||
Source: i2pkeys.I2PAddr("test"),
|
||||
}
|
||||
err = dg2.ResolveSource(dummySession)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for already resolved source, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHashResolverCacheOperations tests the cache management without I2P
|
||||
func TestHashResolverCacheOperations(t *testing.T) {
|
||||
// Create resolver with nil SAM (cache-only operations)
|
||||
resolver := &HashResolver{
|
||||
sam: nil,
|
||||
cache: make(map[string]i2pkeys.I2PAddr),
|
||||
}
|
||||
|
||||
// Test CacheSize on empty cache
|
||||
if size := resolver.CacheSize(); size != 0 {
|
||||
t.Errorf("Expected cache size 0, got %d", size)
|
||||
}
|
||||
|
||||
// Add entries to cache manually for testing
|
||||
hash1 := make([]byte, 32)
|
||||
for i := range hash1 {
|
||||
hash1[i] = 1
|
||||
}
|
||||
b32_1 := hashToB32Address(hash1)
|
||||
resolver.cache[b32_1] = i2pkeys.I2PAddr("destination1")
|
||||
|
||||
hash2 := make([]byte, 32)
|
||||
for i := range hash2 {
|
||||
hash2[i] = 2
|
||||
}
|
||||
b32_2 := hashToB32Address(hash2)
|
||||
resolver.cache[b32_2] = i2pkeys.I2PAddr("destination2")
|
||||
|
||||
// Test CacheSize
|
||||
if size := resolver.CacheSize(); size != 2 {
|
||||
t.Errorf("Expected cache size 2, got %d", size)
|
||||
}
|
||||
|
||||
// Test GetCached for hit
|
||||
dest, ok := resolver.GetCached(hash1)
|
||||
if !ok {
|
||||
t.Error("Expected cache hit for hash1")
|
||||
}
|
||||
if dest != "destination1" {
|
||||
t.Errorf("Expected destination1, got %v", dest)
|
||||
}
|
||||
|
||||
// Test GetCached for miss
|
||||
hash3 := make([]byte, 32)
|
||||
for i := range hash3 {
|
||||
hash3[i] = 3
|
||||
}
|
||||
_, ok = resolver.GetCached(hash3)
|
||||
if ok {
|
||||
t.Error("Expected cache miss for hash3")
|
||||
}
|
||||
|
||||
// Test Clear
|
||||
resolver.Clear()
|
||||
if size := resolver.CacheSize(); size != 0 {
|
||||
t.Errorf("Expected cache size 0 after clear, got %d", size)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHashToB32AddressConversion tests the hash-to-b32 conversion logic
|
||||
func TestHashToB32AddressConversion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hash []byte
|
||||
wantLen int
|
||||
}{
|
||||
{
|
||||
name: "all zeros",
|
||||
hash: make([]byte, 32),
|
||||
wantLen: 60, // 52 + 8 (.b32.i2p)
|
||||
},
|
||||
{
|
||||
name: "all ones",
|
||||
hash: func() []byte {
|
||||
h := make([]byte, 32)
|
||||
for i := range h {
|
||||
h[i] = 0xFF
|
||||
}
|
||||
return h
|
||||
}(),
|
||||
wantLen: 60,
|
||||
},
|
||||
{
|
||||
name: "sequential bytes",
|
||||
hash: func() []byte {
|
||||
h := make([]byte, 32)
|
||||
for i := range h {
|
||||
h[i] = byte(i)
|
||||
}
|
||||
return h
|
||||
}(),
|
||||
wantLen: 60,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
b32 := hashToB32Address(tt.hash)
|
||||
|
||||
if len(b32) != tt.wantLen {
|
||||
t.Errorf("Expected b32 address length %d, got %d", tt.wantLen, len(b32))
|
||||
}
|
||||
|
||||
if b32[len(b32)-8:] != ".b32.i2p" {
|
||||
t.Errorf("Expected address to end with '.b32.i2p', got %q", b32)
|
||||
}
|
||||
|
||||
// Verify it's lowercase (base32 standard)
|
||||
for i, c := range b32[:len(b32)-8] { // Don't check the suffix
|
||||
if c >= 'A' && c <= 'Z' {
|
||||
t.Errorf("Expected lowercase base32, found uppercase at position %d: %c", i, c)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveHashInvalidLength tests that invalid hash lengths are rejected
|
||||
func TestResolveHashInvalidLength(t *testing.T) {
|
||||
resolver := &HashResolver{
|
||||
sam: nil, // Will fail before SAM access
|
||||
cache: make(map[string]i2pkeys.I2PAddr),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
hashLen int
|
||||
wantFail bool
|
||||
}{
|
||||
{"0 bytes", 0, true},
|
||||
{"1 byte", 1, true},
|
||||
{"16 bytes", 16, true},
|
||||
{"31 bytes", 31, true},
|
||||
{"33 bytes", 33, true},
|
||||
{"64 bytes", 64, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
hash := make([]byte, tt.hashLen)
|
||||
_, err := resolver.ResolveHash(hash)
|
||||
|
||||
if tt.wantFail {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for hash length %d, got nil", tt.hashLen)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test valid hash length (32 bytes) separately since it will try SAM lookup
|
||||
t.Run("32 bytes valid length", func(t *testing.T) {
|
||||
hash := make([]byte, 32)
|
||||
_, err := resolver.ResolveHash(hash)
|
||||
// Will fail due to nil SAM, but shouldn't panic or fail length check
|
||||
if err == nil {
|
||||
t.Error("Expected error due to nil SAM, got nil")
|
||||
}
|
||||
// Just verify it doesn't panic and returns an error (SAM or lookup related)
|
||||
})
|
||||
}
|
||||
149
datagram3/write.go
Normal file
149
datagram3/write.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package datagram3
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/go-i2p/i2pkeys"
|
||||
"github.com/samber/oops"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// SetTimeout sets the timeout for datagram3 write operations.
|
||||
// This method configures the maximum time to wait for datagram send operations
|
||||
// to complete. The timeout prevents indefinite blocking during network congestion or connection
|
||||
// issues. Returns the writer instance for method chaining convenience.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// writer.SetTimeout(30*time.Second).SendDatagram(data, destination)
|
||||
func (w *Datagram3Writer) SetTimeout(timeout time.Duration) *Datagram3Writer {
|
||||
// Configure the timeout for send operations to prevent indefinite blocking
|
||||
w.timeout = timeout
|
||||
return w
|
||||
}
|
||||
|
||||
// SendDatagram sends a datagram to the specified I2P destination.
|
||||
//
|
||||
// This method uses the SAMv3 UDP approach: sending via UDP socket to port 7655 with DATAGRAM3 format.
|
||||
// The destination can be:
|
||||
// - Full base64 destination (516+ chars)
|
||||
// - Hostname (.i2p address)
|
||||
// - B32 address (52 chars + .b32.i2p)
|
||||
// - B32 address derived from received DATAGRAM3 hash (via ResolveSource)
|
||||
//
|
||||
// Maximum datagram size is 31744 bytes total (including headers), with 11 KB recommended for
|
||||
// best reliability across the I2P network. It blocks until the datagram is sent or an error occurs,
|
||||
// respecting the configured timeout.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// // Send to full destination
|
||||
// err := writer.SendDatagram([]byte("hello world"), destinationAddr)
|
||||
//
|
||||
// // Reply to received datagram (requires hash resolution)
|
||||
// if err := receivedDatagram.ResolveSource(session); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// err := writer.SendDatagram([]byte("reply"), receivedDatagram.Source)
|
||||
func (w *Datagram3Writer) SendDatagram(data []byte, dest i2pkeys.I2PAddr) error {
|
||||
// Check if the session is closed before attempting to send
|
||||
w.session.mu.RLock()
|
||||
if w.session.closed {
|
||||
w.session.mu.RUnlock()
|
||||
return oops.Errorf("session is closed")
|
||||
}
|
||||
w.session.mu.RUnlock()
|
||||
|
||||
// Create detailed logging context for debugging send operations
|
||||
logger := log.WithFields(logrus.Fields{
|
||||
"session_id": w.session.ID(),
|
||||
"destination": dest.Base32(),
|
||||
"size": len(data),
|
||||
"style": "DATAGRAM3",
|
||||
})
|
||||
logger.Debug("Sending datagram3 message via UDP socket")
|
||||
|
||||
// Use UDP socket approach (SAMv3 method) to send DATAGRAM3 messages
|
||||
// Connect to SAM's UDP port (default 7655) for datagram3 transmission
|
||||
samHost := w.session.sam.SAMEmit.I2PConfig.SamHost
|
||||
if samHost == "" {
|
||||
samHost = "127.0.0.1" // Default SAM host
|
||||
}
|
||||
samUDPPort := "7655" // Default SAM UDP port for datagram3 transmission
|
||||
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(samHost, samUDPPort))
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to resolve SAM UDP address")
|
||||
return oops.Errorf("failed to resolve SAM UDP address: %w", err)
|
||||
}
|
||||
|
||||
udpConn, err := net.DialUDP("udp", nil, udpAddr)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to connect to SAM UDP port")
|
||||
return oops.Errorf("failed to connect to SAM UDP port: %w", err)
|
||||
}
|
||||
defer udpConn.Close()
|
||||
|
||||
// Construct the SAMv3 UDP datagram3 format:
|
||||
// First line: "3.3 <session_id> <destination> [options]\n"
|
||||
// Remaining data: the actual message payload
|
||||
sessionID := w.session.ID()
|
||||
destination := dest.Base64()
|
||||
|
||||
// Create the header line according to SAMv3 specification
|
||||
// DATAGRAM3 uses same format as DATAGRAM/DATAGRAM2 for sending
|
||||
// Only reception format differs (hash-based source)
|
||||
headerLine := fmt.Sprintf("3.3 %s %s\n", sessionID, destination)
|
||||
|
||||
// Combine header and data into final UDP packet
|
||||
udpMessage := append([]byte(headerLine), data...)
|
||||
|
||||
logger.WithFields(logrus.Fields{
|
||||
"header": headerLine,
|
||||
"total_size": len(udpMessage),
|
||||
}).Debug("Sending UDP datagram3 to SAM")
|
||||
|
||||
// Send the datagram3 message via UDP to SAM bridge
|
||||
_, err = udpConn.Write(udpMessage)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to send UDP datagram3 to SAM")
|
||||
return oops.Errorf("failed to send UDP datagram3 to SAM: %w", err)
|
||||
}
|
||||
|
||||
logger.Debug("Successfully sent datagram3 message via UDP")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReplyToDatagram sends a reply to a received DATAGRAM3 message.
|
||||
//
|
||||
// This automatically resolves the source hash if not already resolved, then sends the reply.
|
||||
// The source hash is resolved via NAMING LOOKUP and cached to avoid repeated lookups.
|
||||
//
|
||||
// ⚠️ SECURITY WARNING: Even after resolution, the source is still UNAUTHENTICATED!
|
||||
// ⚠️ Do not trust the reply destination without additional verification.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// // Receive datagram
|
||||
// dg, err := reader.ReceiveDatagram()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// // Reply (automatically resolves hash)
|
||||
// writer := session.NewWriter()
|
||||
// err = writer.ReplyToDatagram([]byte("reply"), dg)
|
||||
func (w *Datagram3Writer) ReplyToDatagram(data []byte, original *Datagram3) error {
|
||||
// Ensure source is resolved (performs NAMING LOOKUP if needed)
|
||||
if original.Source == "" {
|
||||
log.Debug("Source not resolved, performing hash resolution for reply")
|
||||
if err := original.ResolveSource(w.session); err != nil {
|
||||
return oops.Errorf("failed to resolve source hash for reply: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Send to resolved source (still UNAUTHENTICATED!)
|
||||
return w.SendDatagram(data, original.Source)
|
||||
}
|
||||
121
primary/DOC.md
121
primary/DOC.md
@@ -5,6 +5,65 @@
|
||||
|
||||
## Usage
|
||||
|
||||
#### type Datagram3SubSession
|
||||
|
||||
```go
|
||||
type Datagram3SubSession struct {
|
||||
*datagram3.Datagram3Session
|
||||
}
|
||||
```
|
||||
|
||||
Datagram3SubSession wraps a datagram3.Datagram3Session to implement the
|
||||
SubSession interface. This adapter allows Datagram3Session instances to be
|
||||
managed by primary sessions while maintaining their full functionality and
|
||||
thread-safe operations.
|
||||
|
||||
⚠️ SECURITY WARNING: DATAGRAM3 sources are NOT authenticated and can be spoofed!
|
||||
⚠️ This sub-session type uses hash-based source identification which is
|
||||
unauthenticated. ⚠️ Do not trust source addresses without additional
|
||||
application-level authentication. ⚠️ If you need authenticated sources, use
|
||||
DatagramSubSession (DATAGRAM) instead.
|
||||
|
||||
#### func NewDatagram3SubSession
|
||||
|
||||
```go
|
||||
func NewDatagram3SubSession(id string, session *datagram3.Datagram3Session) *Datagram3SubSession
|
||||
```
|
||||
NewDatagram3SubSession creates a Datagram3SubSession wrapper around a
|
||||
Datagram3Session. This constructor initializes the wrapper with proper
|
||||
identification and state management to enable primary session integration.
|
||||
|
||||
⚠️ SECURITY WARNING: Sources are UNAUTHENTICATED and can be spoofed!
|
||||
|
||||
#### func (*Datagram3SubSession) Active
|
||||
|
||||
```go
|
||||
func (s *Datagram3SubSession) Active() bool
|
||||
```
|
||||
Active returns whether this datagram3 sub-session is currently active.
|
||||
|
||||
#### func (*Datagram3SubSession) Close
|
||||
|
||||
```go
|
||||
func (s *Datagram3SubSession) Close() error
|
||||
```
|
||||
Close closes the datagram3 sub-session and marks it as inactive.
|
||||
|
||||
#### func (*Datagram3SubSession) ID
|
||||
|
||||
```go
|
||||
func (s *Datagram3SubSession) ID() string
|
||||
```
|
||||
ID returns the unique identifier for this datagram3 sub-session.
|
||||
|
||||
#### func (*Datagram3SubSession) Type
|
||||
|
||||
```go
|
||||
func (s *Datagram3SubSession) Type() string
|
||||
```
|
||||
Type returns the session type identifier for datagram3 sessions. Returns
|
||||
"DATAGRAM3" to distinguish from authenticated DATAGRAM sessions.
|
||||
|
||||
#### type DatagramSubSession
|
||||
|
||||
```go
|
||||
@@ -201,6 +260,42 @@ Example usage:
|
||||
log.Printf("Sub-session %s (type: %s) is active: %v", sub.ID(), sub.Type(), sub.Active())
|
||||
}
|
||||
|
||||
#### func (*PrimarySession) NewDatagram3SubSession
|
||||
|
||||
```go
|
||||
func (p *PrimarySession) NewDatagram3SubSession(id string, options []string) (*Datagram3SubSession, error)
|
||||
```
|
||||
NewDatagram3SubSession creates a new datagram3 sub-session within this primary
|
||||
session using SAMv3 UDP forwarding. The sub-session shares the primary session's
|
||||
I2P identity and tunnel infrastructure while providing full Datagram3Session
|
||||
functionality for repliable but UNAUTHENTICATED datagram communication. Each
|
||||
sub-session must have a unique identifier within the primary session scope.
|
||||
|
||||
⚠️ SECURITY WARNING: DATAGRAM3 sources are NOT authenticated and can be spoofed!
|
||||
⚠️ Do not trust source addresses without additional application-level
|
||||
authentication. ⚠️ If you need authenticated sources, use NewDatagramSubSession
|
||||
(DATAGRAM) instead.
|
||||
|
||||
This implementation uses the SAMv3.3 SESSION ADD protocol to properly register
|
||||
the subsession with the primary session's SAM connection, ensuring compliance
|
||||
with the I2P SAM protocol specification for PRIMARY session management.
|
||||
|
||||
Per SAMv3.3 specification, DATAGRAM3 subsessions REQUIRE UDP forwarding for
|
||||
proper operation. Received datagrams contain a 32-byte hash instead of full
|
||||
authenticated destination. Use the session's hash resolver to convert hashes to
|
||||
destinations for replies.
|
||||
|
||||
Example usage:
|
||||
|
||||
datagram3Sub, err := primary.NewDatagram3SubSession("udp3-handler", []string{"FROM_PORT=8080"})
|
||||
reader := datagram3Sub.NewReader()
|
||||
writer := datagram3Sub.NewWriter()
|
||||
// Receive datagram with UNAUTHENTICATED source hash
|
||||
dg, err := reader.ReceiveDatagram()
|
||||
// Resolve hash to reply (cached by session)
|
||||
err = dg.ResolveSource(datagram3Sub)
|
||||
err = writer.SendDatagram([]byte("reply"), dg.Source)
|
||||
|
||||
#### func (*PrimarySession) NewDatagramSubSession
|
||||
|
||||
```go
|
||||
@@ -216,9 +311,13 @@ This implementation uses the SAMv3.3 SESSION ADD protocol to properly register
|
||||
the subsession with the primary session's SAM connection, ensuring compliance
|
||||
with the I2P SAM protocol specification for PRIMARY session management.
|
||||
|
||||
Per SAMv3.3 specification, DATAGRAM subsessions REQUIRE a PORT parameter. If
|
||||
PORT is not included in the options, PORT=0 (any port) will be added
|
||||
automatically.
|
||||
|
||||
Example usage:
|
||||
|
||||
datagramSub, err := primary.NewDatagramSubSession("udp-handler", []string{"FROM_PORT=8080"})
|
||||
datagramSub, err := primary.NewDatagramSubSession("udp-handler", []string{"PORT=8080", "FROM_PORT=8080"})
|
||||
writer := datagramSub.NewWriter()
|
||||
reader := datagramSub.NewReader()
|
||||
|
||||
@@ -227,16 +326,19 @@ Example usage:
|
||||
```go
|
||||
func (p *PrimarySession) NewRawSubSession(id string, options []string) (*RawSubSession, error)
|
||||
```
|
||||
NewRawSubSession creates a new raw sub-session within this primary session. The
|
||||
sub-session shares the primary session's I2P identity and tunnel infrastructure
|
||||
while providing full RawSession functionality for unrepliable datagram
|
||||
communication. Each sub-session must have a unique identifier within the primary
|
||||
session scope.
|
||||
NewRawSubSession creates a new raw sub-session within this primary session using
|
||||
SAMv3 UDP forwarding. The sub-session shares the primary session's I2P identity
|
||||
and tunnel infrastructure while providing full RawSession functionality for
|
||||
unrepliable datagram communication. Each sub-session must have a unique
|
||||
identifier within the primary session scope.
|
||||
|
||||
This implementation uses the SAMv3.3 SESSION ADD protocol to properly register
|
||||
the subsession with the primary session's SAM connection, ensuring compliance
|
||||
with the I2P SAM protocol specification for PRIMARY session management.
|
||||
|
||||
Per SAMv3.3 specification, RAW subsessions REQUIRE UDP forwarding for proper
|
||||
operation. V1/V2 TCP control socket reading is no longer supported.
|
||||
|
||||
Example usage:
|
||||
|
||||
rawSub, err := primary.NewRawSubSession("raw-sender", []string{"FROM_PORT=8080"})
|
||||
@@ -492,7 +594,7 @@ Type returns the session type identifier for stream sessions.
|
||||
type SubSession interface {
|
||||
// ID returns the unique identifier for this sub-session
|
||||
ID() string
|
||||
// Type returns the session type ("STREAM", "DATAGRAM", "RAW")
|
||||
// Type returns the session type ("STREAM", "DATAGRAM", "DATAGRAM3", "RAW")
|
||||
Type() string
|
||||
// Close closes the sub-session and releases its resources
|
||||
Close() error
|
||||
@@ -502,8 +604,9 @@ type SubSession interface {
|
||||
```
|
||||
|
||||
SubSession represents a generic interface for sub-sessions that can be managed
|
||||
by a primary session. All sub-session types (stream, datagram, raw) implement
|
||||
this interface to provide unified lifecycle management and identification.
|
||||
by a primary session. All sub-session types (stream, datagram, datagram3, raw)
|
||||
implement this interface to provide unified lifecycle management and
|
||||
identification.
|
||||
|
||||
#### type SubSessionRegistry
|
||||
|
||||
|
||||
20
primary/README.md
Normal file
20
primary/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# go-sam-go/primary
|
||||
|
||||
Primary session library for multi-session management over I2P using the SAMv3 protocol.
|
||||
|
||||
## Installation
|
||||
|
||||
Install using Go modules with the package path `github.com/go-i2p/go-sam-go/primary`.
|
||||
|
||||
## Usage
|
||||
|
||||
The package provides primary session management for creating multiple sub-sessions over I2P networks. PrimarySession manages the primary session lifecycle and allows creation of stream, datagram, and raw sub-sessions.
|
||||
|
||||
Create primary sessions using NewPrimarySession(), then create sub-sessions using NewStreamSubSession(), NewDatagramSubSession(), or NewRawSubSession(). All sub-sessions share the same I2P destination.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- github.com/go-i2p/go-sam-go/common - Core SAM protocol implementation
|
||||
- github.com/go-i2p/i2pkeys - I2P cryptographic key handling
|
||||
- github.com/sirupsen/logrus - Structured logging
|
||||
- github.com/samber/oops - Enhanced error handling
|
||||
27
primary/doc.go
Normal file
27
primary/doc.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Package primary provides PRIMARY session management for sharing I2P tunnels across multiple subsessions.
|
||||
//
|
||||
// PRIMARY sessions allow multiple subsessions (stream, datagram, datagram2, datagram3, raw)
|
||||
// to share a single set of I2P tunnels, reducing resource usage and tunnel setup overhead.
|
||||
// Each subsession operates independently while using the master session's tunnels.
|
||||
//
|
||||
// Key features:
|
||||
// - Single tunnel setup for multiple subsessions
|
||||
// - Mixed subsession types (stream, datagram, raw)
|
||||
// - Independent subsession lifecycle management
|
||||
// - Reduced resource usage and setup time
|
||||
// - SAMv3.3 PRIMARY protocol compliance
|
||||
//
|
||||
// Primary session creation requires 2-5 minutes for I2P tunnel establishment. Subsessions
|
||||
// attach quickly since tunnels are already established. Use generous timeouts for initial
|
||||
// PRIMARY session creation.
|
||||
//
|
||||
// Basic usage:
|
||||
//
|
||||
// sam, err := common.NewSAM("127.0.0.1:7656")
|
||||
// primary, err := primary.NewPrimarySession(sam, "master", keys, []string{"inbound.length=1"})
|
||||
// defer primary.Close()
|
||||
// streamSub, err := primary.NewStreamSubsession("stream-1")
|
||||
// datagramSub, err := primary.NewDatagramSubsession("dgram-1")
|
||||
//
|
||||
// See also: Package stream, datagram, datagram2, datagram3, raw for individual session types.
|
||||
package primary
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/go-i2p/go-sam-go/common"
|
||||
"github.com/go-i2p/go-sam-go/datagram"
|
||||
"github.com/go-i2p/go-sam-go/datagram3"
|
||||
"github.com/go-i2p/go-sam-go/raw"
|
||||
"github.com/go-i2p/go-sam-go/stream"
|
||||
"github.com/go-i2p/i2pkeys"
|
||||
@@ -16,14 +17,7 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// PrimarySession provides master session capabilities for managing multiple sub-sessions
|
||||
// of different types (stream, datagram, raw) within a single I2P session context.
|
||||
// It enables complex applications with multiple communication patterns while sharing
|
||||
// the same I2P identity and tunnel infrastructure for enhanced efficiency and anonymity.
|
||||
//
|
||||
// The primary session manages the lifecycle of all sub-sessions, ensures proper cleanup
|
||||
// cascading when the primary session is closed, and provides thread-safe operations
|
||||
// for creating, managing, and terminating sub-sessions across different protocols.
|
||||
// PrimarySession manages multiple sub-sessions sharing the same I2P identity and tunnels.
|
||||
type PrimarySession struct {
|
||||
*common.BaseSession
|
||||
sam *common.SAM
|
||||
@@ -33,21 +27,11 @@ type PrimarySession struct {
|
||||
closed bool
|
||||
}
|
||||
|
||||
// NewPrimarySession creates a new primary session with the provided SAM connection,
|
||||
// session ID, cryptographic keys, and configuration options. The primary session
|
||||
// acts as a master container that can create and manage multiple sub-sessions of
|
||||
// different types while sharing the same I2P identity and tunnel infrastructure.
|
||||
//
|
||||
// The session uses PRIMARY session type in the SAM protocol, which allows multiple
|
||||
// sub-sessions to be created using the same underlying I2P destination and keys.
|
||||
// This provides better resource efficiency and maintains consistent identity across
|
||||
// different communication patterns within the same application.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// session, err := NewPrimarySession(sam, "my-primary", keys, []string{"inbound.length=2"})
|
||||
// streamSub, err := session.NewStreamSubSession("stream-1", streamOptions)
|
||||
// datagramSub, err := session.NewDatagramSubSession("datagram-1", datagramOptions)
|
||||
// NewPrimarySession creates a new primary session for managing multiple sub-sessions.
|
||||
// It initializes the session with the provided SAM connection, session ID, cryptographic keys,
|
||||
// and configuration options. The primary session allows creating multiple sub-sessions of
|
||||
// different types (stream, datagram, raw) while sharing the same I2P identity and tunnels.
|
||||
// Example usage: session, err := NewPrimarySession(sam, "my-primary", keys, []string{"inbound.length=2"})
|
||||
func NewPrimarySession(sam *common.SAM, id string, keys i2pkeys.I2PKeys, options []string) (*PrimarySession, error) {
|
||||
logger := log.WithFields(logrus.Fields{
|
||||
"id": id,
|
||||
@@ -82,18 +66,9 @@ func NewPrimarySession(sam *common.SAM, id string, keys i2pkeys.I2PKeys, options
|
||||
}
|
||||
|
||||
// NewPrimarySessionWithSignature creates a new primary session with the specified signature type.
|
||||
// This is a package-level function that provides direct access to signature-aware session creation
|
||||
// without requiring wrapper types. It delegates to the common package for session creation while
|
||||
// maintaining the same primary session functionality and sub-session management capabilities.
|
||||
//
|
||||
// The signature type allows specifying custom cryptographic parameters for enhanced security
|
||||
// or compatibility with specific I2P network configurations. Different signature types provide
|
||||
// various security levels, performance characteristics, and compatibility options.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// session, err := NewPrimarySessionWithSignature(sam, "secure-primary", keys, options, "EdDSA_SHA512_Ed25519")
|
||||
// streamSub, err := session.NewStreamSubSession("stream-1", streamOptions)
|
||||
// This method allows specifying custom cryptographic parameters for enhanced security or
|
||||
// compatibility with specific I2P network configurations.
|
||||
// Example usage: session, err := NewPrimarySessionWithSignature(sam, "secure-primary", keys, options, "EdDSA_SHA512_Ed25519")
|
||||
func NewPrimarySessionWithSignature(sam *common.SAM, id string, keys i2pkeys.I2PKeys, options []string, sigType string) (*PrimarySession, error) {
|
||||
logger := log.WithFields(logrus.Fields{
|
||||
"id": id,
|
||||
@@ -134,16 +109,7 @@ func NewPrimarySessionWithSignature(sam *common.SAM, id string, keys i2pkeys.I2P
|
||||
// The sub-session shares the primary session's I2P identity and tunnel infrastructure
|
||||
// while providing full StreamSession functionality for TCP-like reliable connections.
|
||||
// Each sub-session must have a unique identifier within the primary session scope.
|
||||
//
|
||||
// This implementation uses the SAMv3.3 SESSION ADD protocol to properly register
|
||||
// the subsession with the primary session's SAM connection, ensuring compliance
|
||||
// with the I2P SAM protocol specification for PRIMARY session management.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// streamSub, err := primary.NewStreamSubSession("tcp-handler", []string{"FROM_PORT=8080"})
|
||||
// listener, err := streamSub.Listen()
|
||||
// conn, err := streamSub.Dial("destination.b32.i2p")
|
||||
// Example usage: streamSub, err := primary.NewStreamSubSession("tcp-handler", []string{"FROM_PORT=8080"})
|
||||
func (p *PrimarySession) NewStreamSubSession(id string, options []string) (*StreamSubSession, error) {
|
||||
// Use write lock to ensure atomic sub-session creation and prevent race conditions
|
||||
// during concurrent session creation operations in I2P SAM protocol
|
||||
@@ -400,6 +366,114 @@ func (p *PrimarySession) NewRawSubSession(id string, options []string) (*RawSubS
|
||||
return subSession, nil
|
||||
}
|
||||
|
||||
// NewDatagram3SubSession creates a new datagram3 sub-session within this primary session using SAMv3 UDP forwarding.
|
||||
// The sub-session shares the primary session's I2P identity and tunnel infrastructure
|
||||
// while providing full Datagram3Session functionality for repliable but UNAUTHENTICATED datagram communication.
|
||||
// Each sub-session must have a unique identifier within the primary session scope.
|
||||
//
|
||||
// ⚠️ SECURITY WARNING: DATAGRAM3 sources are NOT authenticated and can be spoofed!
|
||||
// ⚠️ Do not trust source addresses without additional application-level authentication.
|
||||
// ⚠️ If you need authenticated sources, use NewDatagramSubSession (DATAGRAM) instead.
|
||||
//
|
||||
// This implementation uses the SAMv3.3 SESSION ADD protocol to properly register
|
||||
// the subsession with the primary session's SAM connection, ensuring compliance
|
||||
// with the I2P SAM protocol specification for PRIMARY session management.
|
||||
//
|
||||
// Per SAMv3.3 specification, DATAGRAM3 subsessions REQUIRE UDP forwarding for proper operation.
|
||||
// Received datagrams contain a 32-byte hash instead of full authenticated destination.
|
||||
// Use the session's hash resolver to convert hashes to destinations for replies.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// datagram3Sub, err := primary.NewDatagram3SubSession("udp3-handler", []string{"FROM_PORT=8080"})
|
||||
// reader := datagram3Sub.NewReader()
|
||||
// writer := datagram3Sub.NewWriter()
|
||||
// // Receive datagram with UNAUTHENTICATED source hash
|
||||
// dg, err := reader.ReceiveDatagram()
|
||||
// // Resolve hash to reply (cached by session)
|
||||
// err = dg.ResolveSource(datagram3Sub)
|
||||
// err = writer.SendDatagram([]byte("reply"), dg.Source)
|
||||
func (p *PrimarySession) NewDatagram3SubSession(id string, options []string) (*Datagram3SubSession, error) {
|
||||
// Use write lock to ensure atomic sub-session creation and prevent race conditions
|
||||
// during concurrent session creation operations in I2P SAM protocol
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.closed {
|
||||
return nil, oops.Errorf("primary session is closed")
|
||||
}
|
||||
|
||||
logger := log.WithFields(logrus.Fields{
|
||||
"primary_id": p.ID(),
|
||||
"sub_id": id,
|
||||
"options": options,
|
||||
})
|
||||
logger.Warn("Creating DATAGRAM3 sub-session - sources are UNAUTHENTICATED and can be spoofed!")
|
||||
|
||||
// PRIMARY datagram3 subsessions MUST use UDP forwarding because the control socket
|
||||
// is already used by the PRIMARY session. Per SAMv3.md: "If $port is not set,
|
||||
// datagrams will NOT be forwarded, they will be received on the control socket"
|
||||
// Setup UDP listener for receiving forwarded datagrams with hash-based sources
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0") // Port 0 = let OS choose
|
||||
if err != nil {
|
||||
return nil, oops.Errorf("failed to resolve UDP address: %w", err)
|
||||
}
|
||||
|
||||
udpConn, err := net.ListenUDP("udp", udpAddr)
|
||||
if err != nil {
|
||||
return nil, oops.Errorf("failed to create UDP listener: %w", err)
|
||||
}
|
||||
|
||||
// Get the actual port assigned by the OS
|
||||
udpPort := udpConn.LocalAddr().(*net.UDPAddr).Port
|
||||
logger.WithField("udp_port", udpPort).Debug("Created UDP listener for datagram3 forwarding")
|
||||
|
||||
// Ensure PORT parameter is present and add UDP forwarding parameters
|
||||
// This tells SAM bridge to forward datagrams to our UDP port instead of control socket
|
||||
finalOptions := ensureDatagram3ForwardingParameters(options, udpPort)
|
||||
|
||||
// Add the subsession to the primary session using SESSION ADD with DATAGRAM3 style
|
||||
if err := p.sam.AddSubSession("DATAGRAM3", id, finalOptions); err != nil {
|
||||
logger.WithError(err).Error("Failed to add datagram3 subsession")
|
||||
udpConn.Close()
|
||||
return nil, oops.Errorf("failed to create datagram3 sub-session: %w", err)
|
||||
}
|
||||
|
||||
// Create a new SAM connection for the sub-session data operations (for sending)
|
||||
subSAM, err := p.createSubSAMConnection()
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to create sub-SAM connection")
|
||||
udpConn.Close()
|
||||
p.sam.RemoveSubSession(id)
|
||||
return nil, oops.Errorf("failed to create sub-SAM connection: %w", err)
|
||||
}
|
||||
|
||||
// Create the datagram3 session with UDP connection for receiving forwarded datagrams
|
||||
// The session will initialize its own hash resolver for converting sources to destinations
|
||||
datagram3Session, err := datagram3.NewDatagram3SessionFromSubsession(subSAM, id, p.Keys(), options, udpConn)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to create datagram3 session wrapper")
|
||||
subSAM.Close()
|
||||
udpConn.Close()
|
||||
p.sam.RemoveSubSession(id)
|
||||
return nil, oops.Errorf("failed to create datagram3 sub-session: %w", err)
|
||||
}
|
||||
|
||||
// Wrap the datagram3 session in a sub-session adapter
|
||||
subSession := NewDatagram3SubSession(id, datagram3Session)
|
||||
|
||||
// Register the sub-session with the primary session registry
|
||||
if err := p.registry.Register(id, subSession); err != nil {
|
||||
logger.WithError(err).Error("Failed to register datagram3 sub-session")
|
||||
datagram3Session.Close()
|
||||
p.sam.RemoveSubSession(id)
|
||||
return nil, oops.Errorf("failed to register datagram3 sub-session: %w", err)
|
||||
}
|
||||
|
||||
logger.WithField("udp_port", udpPort).Warn("Successfully created datagram3 sub-session - remember sources are UNAUTHENTICATED!")
|
||||
return subSession, nil
|
||||
}
|
||||
|
||||
// GetSubSession retrieves a sub-session by its unique identifier.
|
||||
// Returns the sub-session instance if found, or an error if the sub-session
|
||||
// does not exist or the primary session is closed. This method provides
|
||||
@@ -609,14 +683,7 @@ func ensurePortParameter(options []string) []string {
|
||||
return result
|
||||
}
|
||||
|
||||
// ensureDatagramForwardingParameters ensures proper UDP forwarding parameters for PRIMARY datagram subsessions.
|
||||
// Per SAMv3.md, PRIMARY datagram subsessions MUST use UDP forwarding because the control socket is already
|
||||
// used by the PRIMARY session. This function:
|
||||
// 1. Ensures PORT parameter is present (required for DATAGRAM subsessions)
|
||||
// 2. Adds sam.udp.host and sam.udp.port to enable UDP forwarding by SAM bridge
|
||||
// 3. If sam.udp.port is already present in options, it is preserved
|
||||
//
|
||||
// Without these parameters, datagrams would try to use the control socket which is not possible for subsessions.
|
||||
// ensureDatagramForwardingParameters ensures PORT and HOST parameters for UDP forwarding.
|
||||
func ensureDatagramForwardingParameters(options []string, udpPort int) []string {
|
||||
hasPort := false
|
||||
hasHost := false
|
||||
@@ -647,9 +714,7 @@ func ensureDatagramForwardingParameters(options []string, udpPort int) []string
|
||||
return result
|
||||
}
|
||||
|
||||
// ensureRawForwardingParameters ensures proper UDP forwarding parameters for PRIMARY raw subsessions.
|
||||
// Per SAMv3.md, PRIMARY raw subsessions MUST use UDP forwarding because the control socket is already
|
||||
// used by the PRIMARY session. PORT/HOST tell SAM where to forward datagrams TO (our UDP listener).
|
||||
// ensureRawForwardingParameters ensures PORT and HOST parameters for UDP forwarding.
|
||||
func ensureRawForwardingParameters(options []string, udpPort int) []string {
|
||||
hasPort := false
|
||||
hasHost := false
|
||||
@@ -679,3 +744,34 @@ func ensureRawForwardingParameters(options []string, udpPort int) []string {
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ensureDatagram3ForwardingParameters ensures PORT and HOST parameters for UDP forwarding.
|
||||
func ensureDatagram3ForwardingParameters(options []string, udpPort int) []string {
|
||||
hasPort := false
|
||||
hasHost := false
|
||||
|
||||
// Check what parameters are already present
|
||||
for _, opt := range options {
|
||||
if len(opt) >= 5 && (opt[:5] == "PORT=" || opt[:5] == "port=") {
|
||||
hasPort = true
|
||||
}
|
||||
if len(opt) >= 5 && (opt[:5] == "HOST=" || opt[:5] == "host=") {
|
||||
hasHost = true
|
||||
}
|
||||
}
|
||||
|
||||
// Build result with necessary parameters
|
||||
result := make([]string, len(options), len(options)+2)
|
||||
copy(result, options)
|
||||
|
||||
// Add PORT/HOST to tell SAM bridge where to forward datagrams TO (our UDP listener)
|
||||
// Do NOT set sam.udp.port/sam.udp.host - those configure SAM bridge's own UDP port (default 7655)
|
||||
if !hasPort {
|
||||
result = append(result, fmt.Sprintf("PORT=%d", udpPort)) // Forward to our UDP port
|
||||
}
|
||||
if !hasHost {
|
||||
result = append(result, "HOST=127.0.0.1") // Forward to localhost
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -227,10 +227,38 @@ func TestPrimarySessionSubSessions(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("create datagram3 sub-session", func(t *testing.T) {
|
||||
datagram3SubID := "datagram3_sub_1"
|
||||
// DATAGRAM3 subsessions require a PORT parameter per SAM v3.3 specification
|
||||
// WARNING: DATAGRAM3 sources are UNAUTHENTICATED and can be spoofed!
|
||||
datagram3Sub, err := session.NewDatagram3SubSession(datagram3SubID, []string{"PORT=8082"})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create datagram3 sub-session: %v", err)
|
||||
}
|
||||
defer datagram3Sub.Close()
|
||||
|
||||
if datagram3Sub.ID() != datagram3SubID {
|
||||
t.Errorf("Datagram3 sub-session ID mismatch: got %s, want %s", datagram3Sub.ID(), datagram3SubID)
|
||||
}
|
||||
|
||||
if datagram3Sub.Type() != "DATAGRAM3" {
|
||||
t.Errorf("Datagram3 sub-session type mismatch: got %s, want DATAGRAM3", datagram3Sub.Type())
|
||||
}
|
||||
|
||||
if !datagram3Sub.Active() {
|
||||
t.Error("Datagram3 sub-session should be active")
|
||||
}
|
||||
|
||||
// Check it's registered (should be 4 now with stream, datagram, raw, datagram3)
|
||||
if session.SubSessionCount() != 4 {
|
||||
t.Errorf("Expected 4 sub-sessions, got %d", session.SubSessionCount())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("list all sub-sessions", func(t *testing.T) {
|
||||
subSessions := session.ListSubSessions()
|
||||
if len(subSessions) != 3 {
|
||||
t.Errorf("Expected 3 sub-sessions in list, got %d", len(subSessions))
|
||||
if len(subSessions) != 4 {
|
||||
t.Errorf("Expected 4 sub-sessions in list, got %d", len(subSessions))
|
||||
}
|
||||
|
||||
// Check all types are present
|
||||
@@ -239,7 +267,7 @@ func TestPrimarySessionSubSessions(t *testing.T) {
|
||||
types[sub.Type()] = true
|
||||
}
|
||||
|
||||
expectedTypes := []string{"STREAM", "DATAGRAM", "RAW"}
|
||||
expectedTypes := []string{"STREAM", "DATAGRAM", "RAW", "DATAGRAM3"}
|
||||
for _, expectedType := range expectedTypes {
|
||||
if !types[expectedType] {
|
||||
t.Errorf("Expected sub-session type %s not found", expectedType)
|
||||
|
||||
@@ -4,17 +4,18 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/go-i2p/go-sam-go/datagram"
|
||||
"github.com/go-i2p/go-sam-go/datagram3"
|
||||
"github.com/go-i2p/go-sam-go/raw"
|
||||
"github.com/go-i2p/go-sam-go/stream"
|
||||
)
|
||||
|
||||
// SubSession represents a generic interface for sub-sessions that can be managed
|
||||
// by a primary session. All sub-session types (stream, datagram, raw) implement
|
||||
// by a primary session. All sub-session types (stream, datagram, datagram3, raw) implement
|
||||
// this interface to provide unified lifecycle management and identification.
|
||||
type SubSession interface {
|
||||
// ID returns the unique identifier for this sub-session
|
||||
ID() string
|
||||
// Type returns the session type ("STREAM", "DATAGRAM", "RAW")
|
||||
// Type returns the session type ("STREAM", "DATAGRAM", "DATAGRAM3", "RAW")
|
||||
Type() string
|
||||
// Close closes the sub-session and releases its resources
|
||||
Close() error
|
||||
@@ -336,3 +337,62 @@ func (s *RawSubSession) Close() error {
|
||||
s.active = false
|
||||
return s.RawSession.Close()
|
||||
}
|
||||
|
||||
// Datagram3SubSession wraps a datagram3.Datagram3Session to implement the SubSession interface.
|
||||
// This adapter allows Datagram3Session instances to be managed by primary sessions
|
||||
// while maintaining their full functionality and thread-safe operations.
|
||||
//
|
||||
// ⚠️ SECURITY WARNING: DATAGRAM3 sources are NOT authenticated and can be spoofed!
|
||||
// ⚠️ This sub-session type uses hash-based source identification which is unauthenticated.
|
||||
// ⚠️ Do not trust source addresses without additional application-level authentication.
|
||||
// ⚠️ If you need authenticated sources, use DatagramSubSession (DATAGRAM) instead.
|
||||
type Datagram3SubSession struct {
|
||||
*datagram3.Datagram3Session
|
||||
id string
|
||||
active bool
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewDatagram3SubSession creates a Datagram3SubSession wrapper around a Datagram3Session.
|
||||
// This constructor initializes the wrapper with proper identification and state
|
||||
// management to enable primary session integration.
|
||||
//
|
||||
// ⚠️ SECURITY WARNING: Sources are UNAUTHENTICATED and can be spoofed!
|
||||
func NewDatagram3SubSession(id string, session *datagram3.Datagram3Session) *Datagram3SubSession {
|
||||
return &Datagram3SubSession{
|
||||
Datagram3Session: session,
|
||||
id: id,
|
||||
active: true,
|
||||
}
|
||||
}
|
||||
|
||||
// ID returns the unique identifier for this datagram3 sub-session.
|
||||
func (s *Datagram3SubSession) ID() string {
|
||||
return s.id
|
||||
}
|
||||
|
||||
// Type returns the session type identifier for datagram3 sessions.
|
||||
// Returns "DATAGRAM3" to distinguish from authenticated DATAGRAM sessions.
|
||||
func (s *Datagram3SubSession) Type() string {
|
||||
return "DATAGRAM3"
|
||||
}
|
||||
|
||||
// Active returns whether this datagram3 sub-session is currently active.
|
||||
func (s *Datagram3SubSession) Active() bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.active
|
||||
}
|
||||
|
||||
// Close closes the datagram3 sub-session and marks it as inactive.
|
||||
func (s *Datagram3SubSession) Close() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if !s.active {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.active = false
|
||||
return s.Datagram3Session.Close()
|
||||
}
|
||||
|
||||
40
raw/DOC.md
40
raw/DOC.md
@@ -226,22 +226,28 @@ type RawSession struct {
|
||||
```
|
||||
|
||||
RawSession represents a raw session that can send and receive raw datagrams
|
||||
using SAMv3 UDP forwarding. V1/V2 TCP control socket reading is no longer
|
||||
supported.
|
||||
|
||||
#### func NewRawSession
|
||||
|
||||
```go
|
||||
func NewRawSession(sam *common.SAM, id string, keys i2pkeys.I2PKeys, options []string) (*RawSession, error)
|
||||
```
|
||||
NewRawSession creates a new raw session for sending and receiving raw datagrams.
|
||||
It initializes the session with the provided SAM connection, session ID,
|
||||
cryptographic keys, and configuration options, returning a RawSession instance
|
||||
or an error if creation fails. Example usage: session, err := NewRawSession(sam,
|
||||
"my-session", keys, []string{"inbound.length=1"})
|
||||
NewRawSession creates a new raw session for sending and receiving raw datagrams
|
||||
using SAMv3 UDP forwarding. It initializes the session with the provided SAM
|
||||
connection, session ID, cryptographic keys, and configuration options. It
|
||||
automatically creates a UDP listener for receiving forwarded datagrams (SAMv3
|
||||
requirement) and configures the session with PORT/HOST parameters. V1/V2
|
||||
compatibility (reading from TCP control socket) is no longer supported. Returns
|
||||
a RawSession instance that uses UDP forwarding for all raw datagram reception.
|
||||
Example usage: session, err := NewRawSession(sam, "my-session", keys,
|
||||
[]string{"inbound.length=1"})
|
||||
|
||||
#### func NewRawSessionFromSubsession
|
||||
|
||||
```go
|
||||
func NewRawSessionFromSubsession(sam *common.SAM, id string, keys i2pkeys.I2PKeys, options []string) (*RawSession, error)
|
||||
func NewRawSessionFromSubsession(sam *common.SAM, id string, keys i2pkeys.I2PKeys, options []string, udpConn *net.UDPConn) (*RawSession, error)
|
||||
```
|
||||
NewRawSessionFromSubsession creates a RawSession for a subsession that has
|
||||
already been registered with a PRIMARY session using SESSION ADD. This
|
||||
@@ -252,12 +258,17 @@ This function is specifically designed for use with SAMv3.3 PRIMARY sessions
|
||||
where subsessions are created using SESSION ADD rather than SESSION CREATE
|
||||
commands.
|
||||
|
||||
For PRIMARY raw subsessions, UDP forwarding is mandatory (SAMv3 requirement).
|
||||
The UDP connection must be provided for proper raw datagram reception via UDP
|
||||
forwarding.
|
||||
|
||||
Parameters:
|
||||
|
||||
- sam: SAM connection for data operations (separate from the primary session's control connection)
|
||||
- id: The subsession ID that was already registered with SESSION ADD
|
||||
- keys: The I2P keys from the primary session (shared across all subsessions)
|
||||
- options: Configuration options for the subsession
|
||||
- udpConn: UDP connection for receiving forwarded raw datagrams (required, not nil)
|
||||
|
||||
Returns a RawSession ready for use without attempting to create a new SAM
|
||||
session.
|
||||
@@ -276,10 +287,11 @@ session's cryptographic keys. Example usage: addr := session.Addr()
|
||||
```go
|
||||
func (s *RawSession) Close() error
|
||||
```
|
||||
Close closes the raw session and all associated resources. This method is safe
|
||||
to call multiple times and will only perform cleanup once. All readers and
|
||||
writers created from this session will become invalid after closing. Example
|
||||
usage: defer session.Close()
|
||||
Close closes the raw session and all associated resources. This method safely
|
||||
terminates the session, closes the UDP listener and underlying connection, and
|
||||
cleans up any background goroutines. It's safe to call multiple times. All
|
||||
readers and writers created from this session will become invalid after closing.
|
||||
Example usage: defer session.Close()
|
||||
|
||||
#### func (*RawSession) Dial
|
||||
|
||||
@@ -391,10 +403,10 @@ Example usage: conn := session.PacketConn(); n, addr, err := conn.ReadFrom(buf)
|
||||
```go
|
||||
func (s *RawSession) ReceiveDatagram() (*RawDatagram, error)
|
||||
```
|
||||
ReceiveDatagram receives a single raw datagram from any source. This is a
|
||||
convenience method that creates a temporary reader, starts the receive loop,
|
||||
gets one datagram, and cleans up the resources automatically. Example usage:
|
||||
datagram, err := session.ReceiveDatagram()
|
||||
ReceiveDatagram receives a single raw datagram from any source using SAMv3 UDP
|
||||
forwarding. This method performs a direct UDP read without creating a reader or
|
||||
receive loop. V1/V2 TCP control socket reading is no longer supported. Example
|
||||
usage: datagram, err := session.ReceiveDatagram()
|
||||
|
||||
#### func (*RawSession) SendDatagram
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# go-sam-go/raw
|
||||
|
||||
High-level raw datagram library for unencrypted message delivery over I2P using the SAMv3 protocol.
|
||||
Raw datagram library for encrypted but unauthenticated message delivery over I2P using the SAMv3 protocol.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -8,16 +8,13 @@ Install using Go modules with the package path `github.com/go-i2p/go-sam-go/raw`
|
||||
|
||||
## Usage
|
||||
|
||||
The package provides unencrypted raw datagram messaging over I2P networks. [`RawSession`](raw/types.go) manages the session lifecycle, [`RawReader`](raw/types.go) handles incoming raw datagrams, [`RawWriter`](raw/types.go) sends outgoing raw datagrams, and [`RawConn`](raw/types.go) implements the standard `net.PacketConn` interface for seamless integration with existing Go networking code.
|
||||
The package provides encrypted but unauthenticated datagram messaging over I2P networks. RawSession manages the session lifecycle, RawReader handles incoming datagrams, RawWriter sends outgoing datagrams, and RawConn implements the standard net.PacketConn interface.
|
||||
|
||||
Create sessions using [`NewRawSession`](raw/session.go), send messages with [`SendDatagram()`](raw/session.go), and receive messages using [`ReceiveDatagram()`](raw/session.go). The implementation supports I2P address resolution, configurable tunnel parameters, and comprehensive error handling with proper resource cleanup.
|
||||
|
||||
Key features include full `net.PacketConn` compatibility, I2P destination management, base64 payload encoding, and concurrent raw datagram processing with proper synchronization.
|
||||
Create sessions using NewRawSession(), send messages with SendDatagram(), and receive messages using ReceiveDatagram(). Messages are encrypted but senders are not authenticated. Implement application-layer authentication for security.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- github.com/go-i2p/go-sam-go/common - Core SAM protocol implementation
|
||||
- github.com/go-i2p/i2pkeys - I2P cryptographic key handling
|
||||
- github.com/go-i2p/logger - Logging functionality
|
||||
- github.com/go-i2p/i2pkeys - I2P cryptographic key handling
|
||||
- github.com/sirupsen/logrus - Structured logging
|
||||
- github.com/samber/oops - Enhanced error handling
|
||||
- github.com/samber/oops - Enhanced error handling
|
||||
|
||||
27
raw/doc.go
Normal file
27
raw/doc.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Package raw provides encrypted but unauthenticated, non-repliable datagram sessions for I2P.
|
||||
//
|
||||
// RAW sessions send encrypted datagrams without source authentication or reply capability.
|
||||
// Recipients cannot verify sender identity or send replies. Suitable for one-way broadcast
|
||||
// scenarios (logging, metrics, announcements) where reply capability is not needed.
|
||||
//
|
||||
// Key features:
|
||||
// - Encrypted transmission (confidentiality)
|
||||
// - No source authentication (spoofable)
|
||||
// - Non-repliable (recipient cannot reply)
|
||||
// - UDP-like messaging (unreliable, unordered)
|
||||
// - Maximum 31744 bytes per datagram (11 KB recommended)
|
||||
//
|
||||
// Session creation requires 2-5 minutes for I2P tunnel establishment. Use generous timeouts
|
||||
// and exponential backoff retry logic.
|
||||
//
|
||||
// Basic usage:
|
||||
//
|
||||
// sam, err := common.NewSAM("127.0.0.1:7656")
|
||||
// session, err := raw.NewRawSession(sam, "my-session", keys, []string{"inbound.length=1"})
|
||||
// defer session.Close()
|
||||
// conn := session.PacketConn()
|
||||
// n, err := conn.WriteTo(data, destination)
|
||||
//
|
||||
// See also: Package datagram (authenticated, repliable), datagram2 (with replay protection),
|
||||
// datagram3 (hash-based sources), stream (TCP-like), primary (multi-session management).
|
||||
package raw
|
||||
@@ -1,6 +1,6 @@
|
||||
# go-sam-go/stream
|
||||
|
||||
High-level streaming library for reliable TCP-like connections over I2P using the SAMv3 protocol.
|
||||
Streaming library for TCP-like connections over I2P using the SAMv3 protocol.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -8,16 +8,13 @@ Install using Go modules with the package path `github.com/go-i2p/go-sam-go/stre
|
||||
|
||||
## Usage
|
||||
|
||||
The package provides TCP-like streaming connections over I2P networks. [`StreamSession`](stream/types.go) manages the connection lifecycle, [`StreamListener`](stream/types.go) handles incoming connections, and [`StreamConn`](stream/types.go) implements the standard `net.Conn` interface for seamless integration with existing Go networking code.
|
||||
The package provides TCP-like streaming connections over I2P networks. StreamSession manages the connection lifecycle, StreamListener handles incoming connections, and StreamConn implements the standard net.Conn interface.
|
||||
|
||||
Create sessions using [`NewStreamSession`](stream/session.go), establish listeners with [`Listen()`](stream/session.go), and dial outbound connections using [`Dial()`](stream/session.go) or [`DialI2P()`](stream/session.go). The implementation supports context-based timeouts, concurrent operations, and automatic connection management.
|
||||
|
||||
Key features include full `net.Listener` and `net.Conn` compatibility, I2P address resolution, configurable tunnel parameters, and comprehensive error handling with proper resource cleanup.
|
||||
Create sessions using NewStreamSession(), establish listeners with Listen(), and dial outbound connections using Dial() or DialI2P(). Supports context-based timeouts, concurrent operations, and automatic connection management.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- github.com/go-i2p/go-sam-go/common - Core SAM protocol implementation
|
||||
- github.com/go-i2p/i2pkeys - I2P cryptographic key handling
|
||||
- github.com/go-i2p/logger - Logging functionality
|
||||
- github.com/go-i2p/i2pkeys - I2P cryptographic key handling
|
||||
- github.com/sirupsen/logrus - Structured logging
|
||||
- github.com/samber/oops - Enhanced error handling
|
||||
29
stream/doc.go
Normal file
29
stream/doc.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Package stream provides TCP-like reliable connections for I2P using SAMv3 STREAM sessions.
|
||||
//
|
||||
// STREAM sessions provide ordered, reliable, bidirectional byte streams over I2P tunnels,
|
||||
// implementing standard net.Conn and net.Listener interfaces. Ideal for applications
|
||||
// requiring TCP-like semantics (HTTP servers, file transfers, persistent connections).
|
||||
//
|
||||
// Key features:
|
||||
// - Ordered, reliable delivery
|
||||
// - Bidirectional communication
|
||||
// - Standard net.Conn/net.Listener interfaces
|
||||
// - Automatic connection management
|
||||
// - Compatible with io.Reader/io.Writer
|
||||
//
|
||||
// Session creation requires 2-5 minutes for I2P tunnel establishment. Individual connections
|
||||
// (Accept/Dial) require additional time for circuit building. Use generous timeouts and
|
||||
// exponential backoff retry logic.
|
||||
//
|
||||
// Basic usage:
|
||||
//
|
||||
// sam, err := common.NewSAM("127.0.0.1:7656")
|
||||
// session, err := stream.NewStreamSession(sam, "my-session", keys, []string{"inbound.length=1"})
|
||||
// defer session.Close()
|
||||
// listener, err := session.Listen()
|
||||
// conn, err := listener.Accept()
|
||||
// defer conn.Close()
|
||||
//
|
||||
// See also: Package datagram (UDP-like messaging), raw (unrepliable datagrams),
|
||||
// primary (multi-session management).
|
||||
package stream
|
||||
Reference in New Issue
Block a user