mirror of
https://github.com/go-i2p/go-sam-go.git
synced 2026-01-12 21:21:44 -05:00
329 lines
12 KiB
Go
329 lines
12 KiB
Go
package raw
|
|
|
|
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"
|
|
)
|
|
|
|
// ensureRawUDPForwardingParameters injects UDP forwarding parameters into session options if not already present.
|
|
// This ensures SAMv3 UDP forwarding is configured with PORT, HOST, sam.udp.port, and sam.udp.host parameters.
|
|
// This is required for all raw sessions in v3-only mode.
|
|
func ensureRawUDPForwardingParameters(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)
|
|
}
|
|
|
|
// 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 !hasHost {
|
|
updatedOptions = append(updatedOptions, "HOST=127.0.0.1")
|
|
}
|
|
if !hasPort {
|
|
updatedOptions = append(updatedOptions, "PORT="+strconv.Itoa(udpPort))
|
|
}
|
|
|
|
return updatedOptions
|
|
}
|
|
|
|
// 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 NewRawSession(sam *common.SAM, id string, keys i2pkeys.I2PKeys, options []string) (*RawSession, error) {
|
|
logger := log.WithFields(logrus.Fields{
|
|
"id": id,
|
|
"options": options,
|
|
})
|
|
logger.Debug("Creating new RawSession with SAMv3 UDP forwarding")
|
|
|
|
// Create UDP listener for receiving forwarded raw datagrams (SAMv3 requirement)
|
|
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 raw datagram forwarding")
|
|
|
|
// Inject UDP forwarding parameters into session options (SAMv3 requirement)
|
|
options = ensureRawUDPForwardingParameters(options, udpPort)
|
|
|
|
// Create the base session using the common package
|
|
session, err := sam.NewGenericSession("RAW", 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 raw session: %w", err)
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
rs := &RawSession{
|
|
BaseSession: baseSession,
|
|
sam: sam,
|
|
options: options,
|
|
udpConn: udpConn,
|
|
udpEnabled: true,
|
|
}
|
|
|
|
logger.Debug("Successfully created RawSession with UDP forwarding")
|
|
return rs, nil
|
|
}
|
|
|
|
// NewRawSessionFromSubsession creates a RawSession 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 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.
|
|
func NewRawSessionFromSubsession(sam *common.SAM, id string, keys i2pkeys.I2PKeys, options []string, udpConn *net.UDPConn) (*RawSession, error) {
|
|
logger := log.WithFields(logrus.Fields{
|
|
"id": id,
|
|
"options": options,
|
|
"udp_enabled": udpConn != nil,
|
|
})
|
|
logger.Debug("Creating RawSession from existing subsession with SAMv3 UDP forwarding")
|
|
|
|
if udpConn == nil {
|
|
logger.Error("UDP connection is required for SAMv3 raw subsessions")
|
|
return nil, oops.Errorf("udp connection is required for raw subsessions (v3 only)")
|
|
}
|
|
|
|
// Create a BaseSession manually since the session is already registered
|
|
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 raw session from subsession: %w", err)
|
|
}
|
|
|
|
rs := &RawSession{
|
|
BaseSession: baseSession,
|
|
sam: sam,
|
|
options: options,
|
|
udpConn: udpConn,
|
|
udpEnabled: true,
|
|
}
|
|
|
|
logger.Debug("Successfully created RawSession from subsession with UDP forwarding")
|
|
return rs, nil
|
|
}
|
|
|
|
// NewReader creates a RawReader for receiving raw datagrams from any source.
|
|
// It initializes buffered channels for incoming datagrams and errors, returning nil if the session is closed.
|
|
// The caller must start the receive loop manually by calling receiveLoop() in a goroutine.
|
|
// Example usage: reader := session.NewReader(); go reader.receiveLoop()
|
|
func (s *RawSession) NewReader() *RawReader {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
if s.closed {
|
|
return nil
|
|
}
|
|
|
|
return &RawReader{
|
|
session: s,
|
|
recvChan: make(chan *RawDatagram, 10), // Buffer for incoming datagrams
|
|
errorChan: make(chan error, 1),
|
|
closeChan: make(chan struct{}),
|
|
doneChan: make(chan struct{}),
|
|
closed: false,
|
|
mu: sync.RWMutex{},
|
|
}
|
|
}
|
|
|
|
// ...existing code...
|
|
|
|
// NewWriter creates a RawWriter for sending raw datagrams to specific destinations.
|
|
// It initializes the 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)
|
|
func (s *RawSession) NewWriter() *RawWriter {
|
|
return &RawWriter{
|
|
session: s,
|
|
timeout: 30, // Default timeout in seconds
|
|
}
|
|
}
|
|
|
|
// PacketConn returns a net.PacketConn interface for this session.
|
|
// This provides compatibility with standard Go networking code by wrapping the session
|
|
// in a RawConn that implements the PacketConn interface for datagram operations.
|
|
// Example usage: conn := session.PacketConn(); n, addr, err := conn.ReadFrom(buf)
|
|
func (s *RawSession) PacketConn() net.PacketConn {
|
|
conn := &RawConn{
|
|
session: s,
|
|
reader: s.NewReader(),
|
|
writer: s.NewWriter(),
|
|
}
|
|
|
|
// Set up cleanup to prevent resource leaks if Close() is not called
|
|
conn.addCleanup()
|
|
|
|
return conn
|
|
}
|
|
|
|
// SendDatagram sends a raw datagram to the specified destination address.
|
|
// 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(data, destAddr)
|
|
func (s *RawSession) SendDatagram(data []byte, dest i2pkeys.I2PAddr) error {
|
|
return s.NewWriter().SendDatagram(data, dest)
|
|
}
|
|
|
|
// 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 (s *RawSession) ReceiveDatagram() (*RawDatagram, 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.readRawFromUDP(udpConn)
|
|
}
|
|
|
|
// readRawFromUDP reads a forwarded raw datagram from the UDP connection.
|
|
// This is used for SAMv3 UDP forwarding where raw datagrams are forwarded as-is.
|
|
// Format per SAMv3.md: Raw datagrams are forwarded as-is to the specified host:port without a prefix.
|
|
// For RAW sessions with HEADER=true option, datagrams are prepended with a line containing
|
|
// PROTOCOL=nnn FROM_PORT=nnnn TO_PORT=nnnn.
|
|
func (s *RawSession) readRawFromUDP(udpConn *net.UDPConn) (*RawDatagram, error) {
|
|
buffer := make([]byte, 65536) // Large buffer for UDP datagrams
|
|
n, remoteAddr, 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,
|
|
"remote_addr": remoteAddr,
|
|
}).Debug("Received UDP raw datagram")
|
|
|
|
// Raw datagrams are forwarded as-is (no prefix in standard mode)
|
|
// TODO: Handle HEADER=true mode if needed in the future
|
|
data := buffer[:n]
|
|
|
|
// For raw datagrams without header, we don't have source destination information
|
|
// This is by design for RAW datagrams - they are anonymous
|
|
// Create empty I2P address for anonymous source
|
|
emptyKeys := i2pkeys.I2PKeys{}
|
|
datagram := &RawDatagram{
|
|
Data: data,
|
|
Source: emptyKeys.Addr(), // Empty source for anonymous raw datagrams
|
|
Local: s.Addr(),
|
|
}
|
|
|
|
return datagram, nil
|
|
}
|
|
|
|
// 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 (s *RawSession) Close() error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
if s.closed {
|
|
return nil
|
|
}
|
|
|
|
logger := log.WithField("id", s.ID())
|
|
logger.Debug("Closing RawSession")
|
|
|
|
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 base session
|
|
if err := s.BaseSession.Close(); err != nil {
|
|
logger.WithError(err).Error("Failed to close base session")
|
|
return oops.Errorf("failed to close raw session: %w", err)
|
|
}
|
|
|
|
logger.Debug("Successfully closed RawSession")
|
|
return nil
|
|
}
|
|
|
|
// Addr returns the I2P address of this session.
|
|
// This address can be used by other I2P nodes to send datagrams to this session.
|
|
// The address is derived from the session's cryptographic keys.
|
|
// Example usage: addr := session.Addr()
|
|
func (s *RawSession) Addr() i2pkeys.I2PAddr {
|
|
return s.Keys().Addr()
|
|
}
|
|
|
|
// Network returns the network type for this address.
|
|
// This method implements the net.Addr interface and always returns "i2p-raw"
|
|
// to identify this as an I2P raw datagram address type.
|
|
// Example usage: network := addr.Network() // returns "i2p-raw"
|
|
func (a *RawAddr) Network() string {
|
|
return "i2p-raw"
|
|
}
|
|
|
|
// String returns the string representation of the address.
|
|
// This method implements the net.Addr interface and returns the Base32 encoded
|
|
// representation of the I2P address for human-readable display.
|
|
// Example usage: addrStr := addr.String() // returns "abcd1234...xyz.b32.i2p"
|
|
func (a *RawAddr) String() string {
|
|
return a.addr.Base32()
|
|
}
|