82 Commits

Author SHA1 Message Date
eyedeekay
f10259c6d7 break down complex function 2025-07-21 19:16:11 -04:00
eyedeekay
6e40a3b3e4 break down complex test 2025-07-21 18:58:09 -04:00
eyedeekay
31f7ae4812 Fix data race in raw and datagram readers by adding atomic coordination 2025-07-17 22:21:43 -04:00
eyedeekay
cd23be59e0 Fix deadlock in stream session close by avoiding nested mutex acquisition 2025-07-17 21:59:47 -04:00
eyedeekay
a9f8a08c91 more breaking down functions 2025-07-17 18:56:05 -04:00
eyedeekay
7a424c0146 more breaking down functions 2025-07-17 18:52:25 -04:00
eyedeekay
b0ef45af5f more breaking down functions 2025-07-17 18:40:50 -04:00
eyedeekay
e16613c98d more breaking down functions 2025-07-17 18:22:34 -04:00
eyedeekay
ea4b2c2d69 more breaking down functions 2025-07-17 18:18:52 -04:00
eyedeekay
03c47a27b6 more breaking down functions 2025-07-17 18:05:18 -04:00
eyedeekay
705224293b more breaking down functions 2025-07-17 17:56:40 -04:00
eyedeekay
e258f714b7 more breaking down functions 2025-07-17 17:52:29 -04:00
eyedeekay
4c6717d322 break things down into smaller functions, more managable chunks 2025-07-17 17:47:33 -04:00
eyedeekay
600d9e6aae fix(stream): prevent goroutine leak in StreamListener acceptLoop via context cancellation 2025-07-17 17:14:44 -04:00
eyedeekay
6aa7c9375e Document stream package 2025-07-17 17:01:03 -04:00
eyedeekay
643dbd044e Document stream package 2025-07-17 17:00:16 -04:00
eyedeekay
a6855e2788 Document stream package 2025-07-17 16:30:50 -04:00
eyedeekay
0e214d52a7 Document dg1 package 2025-07-17 16:18:57 -04:00
eyedeekay
64fad63a80 Document dg1 package 2025-07-17 16:03:43 -04:00
eyedeekay
4dae898e62 Document raw package 2025-07-17 15:14:07 -04:00
eyedeekay
a415dcf5fa Document raw package 2025-07-17 15:09:34 -04:00
eyedeekay
6cc2a22354 Document raw package 2025-07-17 15:07:58 -04:00
eyedeekay
eb099b72ef Document raw package 2025-07-17 15:02:16 -04:00
eyedeekay
acc7416631 Fix channel double-close race condition in DatagramReader 2025-07-17 14:14:58 -04:00
eyedeekay
9668ba4681 fix failing test issue 2025-07-17 14:14:58 -04:00
eyedeekay
7132538a2c Fix goroutine leak in Raw Session by implementing proper reader lifecycle management 2025-07-17 14:14:58 -04:00
eyedeekay
f60f23c302 Fix critical data race in RandPort using thread-safe crypto/rand 2025-07-17 14:14:58 -04:00
eyedeekay
7875d47ea4 use crypto/rand instead of rand 2025-07-17 14:14:58 -04:00
idk
56ed7a3d04 Update README.md 2025-06-02 15:39:11 +00:00
eyedeekay
4386355e1f Work on raw datagram tests 2025-06-01 18:25:35 -04:00
eyedeekay
e9ae8600bc Add dirs for dg2 and 3 2025-06-01 18:15:17 -04:00
eyedeekay
7eea63b351 Fix close race in DatagramSession 2025-06-01 18:14:24 -04:00
eyedeekay
88c2168ee0 Fix close race in DatagramSession 2025-06-01 18:07:09 -04:00
eyedeekay
b609d06306 Update README 2025-06-01 18:03:55 -04:00
eyedeekay
63a00527b9 Add Conn() getter to BaseSession 2025-06-01 18:01:04 -04:00
eyedeekay
27312d3e94 Add Conn() getter to BaseSession 2025-06-01 17:56:23 -04:00
eyedeekay
12559c0335 Implement tests for RawSession 2025-06-01 17:32:11 -04:00
eyedeekay
5ee5d0e181 add tests for raw sessions 2025-06-01 17:27:48 -04:00
eyedeekay
3dee698d01 Implement tests for RawSession 2025-06-01 17:21:15 -04:00
eyedeekay
102c79f535 Fixes the DatagramSession implementation 2025-06-01 17:07:23 -04:00
eyedeekay
69d5f950fd Work on concurrent reader operations in datagrams 2025-06-01 16:45:48 -04:00
eyedeekay
4755612f11 Work on datagram1 2025-06-01 16:38:11 -04:00
eyedeekay
c957176d25 Remove sam3 compatiblity layer until I decide what to do about Datagrams 2025-06-01 13:25:31 -04:00
eyedeekay
f796823337 start dg2 and dg3 2025-06-01 13:21:11 -04:00
eyedeekay
27794b1002 compatibility layer 2025-05-29 19:47:50 -04:00
eyedeekay
4f85e7f1c7 add github config 2025-05-29 19:46:22 -04:00
eyedeekay
a53acecb87 godoc 2025-05-29 19:44:45 -04:00
eyedeekay
5899247685 gitignore 2025-05-29 19:02:42 -04:00
eyedeekay
9c8d590a4d re-add raw datagrams, likely does not work yet 2025-05-29 19:01:23 -04:00
eyedeekay
f4ea6ae232 Work on making DatagramConn implement net.Conn 2025-05-29 18:52:08 -04:00
eyedeekay
1cfdd98e18 Work on making DatagramConn implement net.Conn 2025-05-29 18:49:54 -04:00
eyedeekay
38182694c5 Add datagram library back in 2025-05-29 18:45:52 -04:00
eyedeekay
b114d8b337 simplify NewGenericSessionWithSignatureAndPorts 2025-05-29 16:59:55 -04:00
eyedeekay
ddd25e0bb8 simplify NewGenericSessionWithSignatureAndPorts 2025-05-29 16:42:56 -04:00
eyedeekay
be432d256d fix min/max api version functions 2025-05-29 16:22:52 -04:00
eyedeekay
9604afd647 add session tests to common 2025-05-27 20:37:17 -04:00
eyedeekay
7febf2dd07 Fix dupe config issues 2025-05-27 20:22:20 -04:00
eyedeekay
21e42bf030 Simplify and move stuff 2025-05-27 20:12:40 -04:00
eyedeekay
51806362f9 Simplify ID 2025-05-27 20:08:55 -04:00
eyedeekay
fbdeea0e4a Simplify Print 2025-05-27 20:04:58 -04:00
eyedeekay
26ae8f209b Simplify common.NewSAM again 2025-05-27 20:01:34 -04:00
eyedeekay
987662da9b Simplify common.NewSAM 2025-05-27 19:59:02 -04:00
eyedeekay
df3728c042 Simplify Config Constructor 2025-05-27 19:45:38 -04:00
eyedeekay
1f95e9fa3a fmt 2025-05-27 19:43:15 -04:00
eyedeekay
0b2d1927de fmt 2025-05-27 19:40:50 -04:00
eyedeekay
3e89669fec Simplify ExtractDest 2025-05-27 19:37:52 -04:00
eyedeekay
95ccf4c187 Simplify formatting 2025-05-27 19:32:05 -04:00
eyedeekay
8514ba8e89 Simplify, add readme 2025-05-27 19:25:56 -04:00
eyedeekay
e31035d632 OK so the bug has to be in common then 2025-05-27 19:01:15 -04:00
eyedeekay
60fcf85129 Rewrite the whole StreamSession thing, which still doesn't work 2025-05-27 18:54:18 -04:00
eyedeekay
d3f085b2c8 eliminate a bunch of broken crap and start over from common 2025-05-27 18:25:13 -04:00
eyedeekay
479f2d20cc simplify common some more 2025-05-27 17:28:40 -04:00
eyedeekay
958f90edc6 simplify common some more 2025-05-27 16:46:29 -04:00
eyedeekay
aa9fe1ef62 Switch to oops, simplify error handling in common 2025-05-27 16:40:15 -04:00
eyedeekay
d2f1de4094 fix common tests 2025-05-27 00:02:12 -04:00
eyedeekay
099746a724 fix conflicts 2025-05-26 23:30:37 -04:00
eyedeekay
a325a30b06 fix conflicts 2025-05-26 23:30:26 -04:00
eyedeekay
9a62a09f40 get away from logrus 2025-05-26 23:27:42 -04:00
eyedeekay
8a23944e43 work on it 2025-05-26 22:58:06 -04:00
idk
11110ed5a2 Merge pull request #2 from urgentquest/polish-a-few-things
General code qualitity and readability
2025-04-30 22:19:52 -04:00
urgentquest
477796de71 general code qualitity and readability. Mostlly no functional changes besides fixing potential null dereferece in SAM.go, avoiding potential (again) infitite loop in util.go. 2025-04-07 23:48:02 +00:00
idk
7c5ff30a30 Merge pull request #1 from urgentquest/refactor-clean-up
Clean up and fixes
2025-03-21 13:47:00 -04:00
114 changed files with 12027 additions and 4029 deletions

60
.github/workflows/page.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: Generate and Deploy GitHub Pages
on:
# Run once hourly
schedule:
- cron: '0 * * * *'
# Allow manual trigger
workflow_dispatch:
# Run on pushes to main branch
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for proper repo data
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24.x'
cache: true
- name: Build Site Generator
run: |
go install github.com/go-i2p/go-gh-page/cmd/github-site-gen@latest
export GOBIN=$(go env GOPATH)/bin
cp -v "$GOBIN/github-site-gen" ./github-site-gen
# Ensure the binary is executable
chmod +x github-site-gen
- name: Generate Site
run: |
# Determine current repository owner and name
REPO_OWNER=$(echo $GITHUB_REPOSITORY | cut -d '/' -f 1)
REPO_NAME=$(echo $GITHUB_REPOSITORY | cut -d '/' -f 2)
# Generate the site
./github-site-gen -repo "${REPO_OWNER}/${REPO_NAME}" -output ./site
# Create a .nojekyll file to disable Jekyll processing
touch ./site/.nojekyll
# Add a .gitattributes file to ensure consistent line endings
echo "* text=auto" > ./site/.gitattributes
- name: Deploy to GitHub Pages
uses: JamesIves/github-pages-deploy-action@v4
with:
folder: site # The folder the action should deploy
branch: gh-pages # The branch the action should deploy to
clean: true # Automatically remove deleted files from the deploy branch
commit-message: "Deploy site generated on ${{ github.sha }}"

3
.gitignore vendored
View File

@@ -1,4 +1,5 @@
review.md
SAMv3.md
testplan.md
err
err
/*.log

1251
DOC.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +1,36 @@
fmt:
find . -name '*.go' -exec gofumpt -w -s -extra {} \;
find . -name '*.go' -exec gofumpt -w -s -extra {} \;
export DEBUG_I2P=debug
#export WARNFAIL_I2P=true
common-test:
go test --tags nettest -v ./common/...
datagram-test:
go test --tags nettest -v ./datagram/...
primary-test:
go test --tags nettest -v ./primary/...
stream-test:
go test --tags nettest -v ./stream/...
raw-test:
go test --tags nettest -v ./raw/...
test:
make common-test
make stream-test
make datagram-test
make raw-test
#make primary-test
test-logs:
make common-test 2> common-err.log 1> common-out.log; cat common-err.log common-out.log
make stream-test 2> stream-err.log 1> stream-out.log; cat stream-err.log stream-out.log
make datagram-test 2> datagram-err.log 1> datagram-out.log; cat datagram-err.log datagram-out.log
make raw-test 2> raw-err.log 1> raw-out.log; cat raw-err.log raw-out.log
#make primary-test 2> primary-err.log 1> primary-out.log; cat primary-err.log primary-out.log
godoc:
find ./*/ -type d -exec godocdown --output={}/DOC.md {} \;

View File

@@ -5,9 +5,9 @@
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 and nothing works yet.**
**BUT, the point of it is to have carefully designed fixes to sam3's rough edges, so the API should be stable**
**It should be ready soon but right now it's broke.**
**WARNING: This is a new package. Streaming works. Repliable datagrams work except for some harmless errors. Raw datagrams kind of work. Primary Sessions, Authenticated Datagrams, and Unauthenticated Datagrams will be supported by I2P 2.11.0**
**The API should not change much.**
**It needs more people looking at it.**
## 📦 Installation
@@ -94,6 +94,20 @@ raw, err := session.NewRawSession("raw", keys, options, 0)
n, err := raw.WriteTo(data, dest)
```
#### `datagram2` Package
Authenticated repliable datagrams:
```go
dgram2, err := session.NewDatagram2Session("udp", keys, options, 0)
n, err := dgram.WriteTo(data, dest)
```
#### `datagram3` Package
Authenticated repliable datagrams:
```go
dgram3, err := session.NewDatagram3Session("udp", keys, options, 0)
n, err := dgram.WriteTo(data, dest)
```
### Configuration
Built-in configuration profiles:
@@ -133,4 +147,4 @@ MIT License
## 🙏 Acknowledgments
Based on the original [github.com/go-i2p/sam3](https://github.com/go-i2p/sam3) library.
Based on the original [github.com/go-i2p/sam3](https://github.com/go-i2p/sam3) library.

View File

@@ -1,11 +0,0 @@
// package sam3 wraps the original sam3 API from github.com/go-i2p/sam3
package sam3
import (
"github.com/go-i2p/go-sam-go/stream"
)
// Implements net.Conn
type SAMConn struct {
*stream.StreamConn
}

950
common/DOC.md Normal file
View File

@@ -0,0 +1,950 @@
# common
--
import "github.com/go-i2p/go-sam-go/common"
## Usage
```go
const (
DEFAULT_SAM_MIN = "3.1"
DEFAULT_SAM_MAX = "3.3"
)
```
```go
const (
SESSION_OK = "SESSION STATUS RESULT=OK DESTINATION="
SESSION_DUPLICATE_ID = "SESSION STATUS RESULT=DUPLICATED_ID\n"
SESSION_DUPLICATE_DEST = "SESSION STATUS RESULT=DUPLICATED_DEST\n"
SESSION_INVALID_KEY = "SESSION STATUS RESULT=INVALID_KEY\n"
SESSION_I2P_ERROR = "SESSION STATUS RESULT=I2P_ERROR MESSAGE="
)
```
```go
const (
SIG_NONE = "SIGNATURE_TYPE=EdDSA_SHA512_Ed25519"
SIG_DSA_SHA1 = "SIGNATURE_TYPE=DSA_SHA1"
SIG_ECDSA_SHA256_P256 = "SIGNATURE_TYPE=ECDSA_SHA256_P256"
SIG_ECDSA_SHA384_P384 = "SIGNATURE_TYPE=ECDSA_SHA384_P384"
SIG_ECDSA_SHA512_P521 = "SIGNATURE_TYPE=ECDSA_SHA512_P521"
SIG_EdDSA_SHA512_Ed25519 = "SIGNATURE_TYPE=EdDSA_SHA512_Ed25519"
// Add a default constant that points to the recommended secure signature type
SIG_DEFAULT = SIG_EdDSA_SHA512_Ed25519
)
```
```go
const (
SAM_RESULT_OK = "RESULT=OK"
SAM_RESULT_INVALID_KEY = "RESULT=INVALID_KEY"
SAM_RESULT_KEY_NOT_FOUND = "RESULT=KEY_NOT_FOUND"
)
```
```go
const (
HELLO_REPLY_OK = "HELLO REPLY RESULT=OK"
HELLO_REPLY_NOVERSION = "HELLO REPLY RESULT=NOVERSION\n"
)
```
```go
const (
SESSION_STYLE_STREAM = "STREAM"
SESSION_STYLE_DATAGRAM = "DATAGRAM"
SESSION_STYLE_RAW = "RAW"
)
```
```go
const (
ACCESS_TYPE_WHITELIST = "whitelist"
ACCESS_TYPE_BLACKLIST = "blacklist"
ACCESS_TYPE_NONE = "none"
)
```
#### func ExtractDest
```go
func ExtractDest(input string) string
```
#### func ExtractPairInt
```go
func ExtractPairInt(input, value string) int
```
#### func ExtractPairString
```go
func ExtractPairString(input, value string) string
```
#### func IgnorePortError
```go
func IgnorePortError(err error) error
```
#### func RandPort
```go
func RandPort() (portNumber string, err error)
```
#### func SetAccessList
```go
func SetAccessList(s []string) func(*SAMEmit) error
```
SetAccessList tells the system to treat the AccessList as a whitelist
#### func SetAccessListType
```go
func SetAccessListType(s string) func(*SAMEmit) error
```
SetAccessListType tells the system to treat the AccessList as a whitelist
#### func SetAllowZeroIn
```go
func SetAllowZeroIn(b bool) func(*SAMEmit) error
```
SetAllowZeroIn tells the tunnel to accept zero-hop peers
#### func SetAllowZeroOut
```go
func SetAllowZeroOut(b bool) func(*SAMEmit) error
```
SetAllowZeroOut tells the tunnel to accept zero-hop peers
#### func SetCloseIdle
```go
func SetCloseIdle(b bool) func(*SAMEmit) error
```
SetCloseIdle tells the connection to close it's tunnels during extended idle
time.
#### func SetCloseIdleTime
```go
func SetCloseIdleTime(u int) func(*SAMEmit) error
```
SetCloseIdleTime sets the time to wait before closing tunnels to idle levels
#### func SetCloseIdleTimeMs
```go
func SetCloseIdleTimeMs(u int) func(*SAMEmit) error
```
SetCloseIdleTimeMs sets the time to wait before closing tunnels to idle levels
in milliseconds
#### func SetCompress
```go
func SetCompress(b bool) func(*SAMEmit) error
```
SetCompress tells clients to use compression
#### func SetEncrypt
```go
func SetEncrypt(b bool) func(*SAMEmit) error
```
SetEncrypt tells the router to use an encrypted leaseset
#### func SetFastRecieve
```go
func SetFastRecieve(b bool) func(*SAMEmit) error
```
SetFastRecieve tells clients to use compression
#### func SetInBackups
```go
func SetInBackups(u int) func(*SAMEmit) error
```
SetInBackups sets the inbound tunnel backups
#### func SetInLength
```go
func SetInLength(u int) func(*SAMEmit) error
```
SetInLength sets the number of hops inbound
#### func SetInQuantity
```go
func SetInQuantity(u int) func(*SAMEmit) error
```
SetInQuantity sets the inbound tunnel quantity
#### func SetInVariance
```go
func SetInVariance(i int) func(*SAMEmit) error
```
SetInVariance sets the variance of a number of hops inbound
#### func SetLeaseSetKey
```go
func SetLeaseSetKey(s string) func(*SAMEmit) error
```
SetLeaseSetKey sets the host of the SAMEmit's SAM bridge
#### func SetLeaseSetPrivateKey
```go
func SetLeaseSetPrivateKey(s string) func(*SAMEmit) error
```
SetLeaseSetPrivateKey sets the host of the SAMEmit's SAM bridge
#### func SetLeaseSetPrivateSigningKey
```go
func SetLeaseSetPrivateSigningKey(s string) func(*SAMEmit) error
```
SetLeaseSetPrivateSigningKey sets the host of the SAMEmit's SAM bridge
#### func SetMessageReliability
```go
func SetMessageReliability(s string) func(*SAMEmit) error
```
SetMessageReliability sets the host of the SAMEmit's SAM bridge
#### func SetName
```go
func SetName(s string) func(*SAMEmit) error
```
SetName sets the host of the SAMEmit's SAM bridge
#### func SetOutBackups
```go
func SetOutBackups(u int) func(*SAMEmit) error
```
SetOutBackups sets the inbound tunnel backups
#### func SetOutLength
```go
func SetOutLength(u int) func(*SAMEmit) error
```
SetOutLength sets the number of hops outbound
#### func SetOutQuantity
```go
func SetOutQuantity(u int) func(*SAMEmit) error
```
SetOutQuantity sets the outbound tunnel quantity
#### func SetOutVariance
```go
func SetOutVariance(i int) func(*SAMEmit) error
```
SetOutVariance sets the variance of a number of hops outbound
#### func SetReduceIdle
```go
func SetReduceIdle(b bool) func(*SAMEmit) error
```
SetReduceIdle tells the connection to reduce it's tunnels during extended idle
time.
#### func SetReduceIdleQuantity
```go
func SetReduceIdleQuantity(u int) func(*SAMEmit) error
```
SetReduceIdleQuantity sets minimum number of tunnels to reduce to during idle
time
#### func SetReduceIdleTime
```go
func SetReduceIdleTime(u int) func(*SAMEmit) error
```
SetReduceIdleTime sets the time to wait before reducing tunnels to idle levels
#### func SetReduceIdleTimeMs
```go
func SetReduceIdleTimeMs(u int) func(*SAMEmit) error
```
SetReduceIdleTimeMs sets the time to wait before reducing tunnels to idle levels
in milliseconds
#### func SetSAMAddress
```go
func SetSAMAddress(s string) func(*SAMEmit) error
```
SetSAMAddress sets the SAM address all-at-once
#### func SetSAMHost
```go
func SetSAMHost(s string) func(*SAMEmit) error
```
SetSAMHost sets the host of the SAMEmit's SAM bridge
#### func SetSAMPort
```go
func SetSAMPort(s string) func(*SAMEmit) error
```
SetSAMPort sets the port of the SAMEmit's SAM bridge using a string
#### func SetType
```go
func SetType(s string) func(*SAMEmit) error
```
SetType sets the type of the forwarder server
#### func SplitHostPort
```go
func SplitHostPort(hostport string) (string, string, error)
```
#### type BaseSession
```go
type BaseSession struct {
SAM SAM
}
```
#### func (*BaseSession) Close
```go
func (bs *BaseSession) Close() error
```
#### func (*BaseSession) Conn
```go
func (bs *BaseSession) Conn() net.Conn
```
#### func (*BaseSession) From
```go
func (bs *BaseSession) From() string
```
#### func (*BaseSession) ID
```go
func (bs *BaseSession) ID() string
```
#### func (*BaseSession) Keys
```go
func (bs *BaseSession) Keys() i2pkeys.I2PKeys
```
#### func (*BaseSession) LocalAddr
```go
func (bs *BaseSession) LocalAddr() net.Addr
```
#### func (*BaseSession) Read
```go
func (bs *BaseSession) Read(b []byte) (int, error)
```
#### func (*BaseSession) RemoteAddr
```go
func (bs *BaseSession) RemoteAddr() net.Addr
```
#### func (*BaseSession) SetDeadline
```go
func (bs *BaseSession) SetDeadline(t time.Time) error
```
#### func (*BaseSession) SetReadDeadline
```go
func (bs *BaseSession) SetReadDeadline(t time.Time) error
```
#### func (*BaseSession) SetWriteDeadline
```go
func (bs *BaseSession) SetWriteDeadline(t time.Time) error
```
#### func (*BaseSession) To
```go
func (bs *BaseSession) To() string
```
#### func (*BaseSession) Write
```go
func (bs *BaseSession) Write(b []byte) (int, error)
```
#### type I2PConfig
```go
type I2PConfig struct {
SamHost string
SamPort int
TunName string
SamMin string
SamMax string
Fromport string
Toport string
Style string
TunType string
DestinationKeys *i2pkeys.I2PKeys
SigType string
EncryptLeaseSet bool
LeaseSetKey string
LeaseSetPrivateKey string
LeaseSetPrivateSigningKey string
LeaseSetKeys i2pkeys.I2PKeys
InAllowZeroHop bool
OutAllowZeroHop bool
InLength int
OutLength int
InQuantity int
OutQuantity int
InVariance int
OutVariance int
InBackupQuantity int
OutBackupQuantity int
FastRecieve bool
UseCompression bool
MessageReliability string
CloseIdle bool
CloseIdleTime int
ReduceIdle bool
ReduceIdleTime int
ReduceIdleQuantity int
LeaseSetEncryption string
// Streaming Library options
AccessListType string
AccessList []string
}
```
I2PConfig is a struct which manages I2P configuration options.
#### func NewConfig
```go
func NewConfig(opts ...func(*I2PConfig) error) (*I2PConfig, error)
```
#### func (*I2PConfig) Accesslist
```go
func (f *I2PConfig) Accesslist() string
```
Accesslist generates the I2CP access list configuration string based on the
configured access list
#### func (*I2PConfig) Accesslisttype
```go
func (f *I2PConfig) Accesslisttype() string
```
Accesslisttype returns the I2CP access list configuration string based on the
AccessListType setting
#### func (*I2PConfig) Close
```go
func (f *I2PConfig) Close() string
```
Close returns I2CP close-on-idle configuration settings as a string if enabled
#### func (*I2PConfig) DestinationKey
```go
func (f *I2PConfig) DestinationKey() string
```
DestinationKey returns the DESTINATION configuration string for the SAM bridge
If destination keys are set, returns them as a string, otherwise returns
"TRANSIENT"
#### func (*I2PConfig) DoZero
```go
func (f *I2PConfig) DoZero() string
```
DoZero returns the zero hop and fast receive configuration string settings
#### func (*I2PConfig) EncryptLease
```go
func (f *I2PConfig) EncryptLease() string
```
EncryptLease returns the lease set encryption configuration string Returns
"i2cp.encryptLeaseSet=true" if encryption is enabled, empty string otherwise
#### func (*I2PConfig) FromPort
```go
func (f *I2PConfig) FromPort() string
```
FromPort returns the FROM_PORT configuration string for SAM bridges >= 3.1
Returns an empty string if SAM version < 3.1 or if fromport is "0"
#### func (*I2PConfig) ID
```go
func (f *I2PConfig) ID() string
```
ID returns the tunnel name as a formatted string. If no tunnel name is set,
generates a random 12-character name using lowercase letters.
#### func (*I2PConfig) InboundBackupQuantity
```go
func (f *I2PConfig) InboundBackupQuantity() string
```
#### func (*I2PConfig) InboundLength
```go
func (f *I2PConfig) InboundLength() string
```
#### func (*I2PConfig) InboundLengthVariance
```go
func (f *I2PConfig) InboundLengthVariance() string
```
#### func (*I2PConfig) InboundQuantity
```go
func (f *I2PConfig) InboundQuantity() string
```
#### func (*I2PConfig) LeaseSetEncryptionType
```go
func (f *I2PConfig) LeaseSetEncryptionType() string
```
LeaseSetEncryptionType returns the I2CP lease set encryption type configuration
string. If no encryption type is set, returns default value "4,0". Validates
that all encryption types are valid integers.
#### func (*I2PConfig) LeaseSetSettings
```go
func (f *I2PConfig) LeaseSetSettings() (string, string, string)
```
Leasesetsettings returns the lease set configuration strings for I2P Returns
three strings: lease set key, private key, and private signing key settings
#### func (*I2PConfig) MaxSAM
```go
func (f *I2PConfig) MaxSAM() string
```
MaxSAM returns the maximum SAM version supported as a string If no maximum
version is set, returns default value "3.1"
#### func (*I2PConfig) MinSAM
```go
func (f *I2PConfig) MinSAM() string
```
MinSAM returns the minimum SAM version supported as a string If no minimum
version is set, returns default value "3.0"
#### func (*I2PConfig) OutboundBackupQuantity
```go
func (f *I2PConfig) OutboundBackupQuantity() string
```
#### func (*I2PConfig) OutboundLength
```go
func (f *I2PConfig) OutboundLength() string
```
#### func (*I2PConfig) OutboundLengthVariance
```go
func (f *I2PConfig) OutboundLengthVariance() string
```
#### func (*I2PConfig) OutboundQuantity
```go
func (f *I2PConfig) OutboundQuantity() string
```
#### func (*I2PConfig) Print
```go
func (f *I2PConfig) Print() []string
```
Print returns a slice of strings containing all the I2P configuration settings
#### func (*I2PConfig) Reduce
```go
func (f *I2PConfig) Reduce() string
```
Reduce returns I2CP reduce-on-idle configuration settings as a string if enabled
#### func (*I2PConfig) Reliability
```go
func (f *I2PConfig) Reliability() string
```
Reliability returns the message reliability configuration string for the SAM
bridge If a reliability setting is specified, returns formatted
i2cp.messageReliability setting
#### func (*I2PConfig) SAMAddress
```go
func (f *I2PConfig) SAMAddress() string
```
SAMAddress returns the SAM bridge address in the format "host:port" This is a
convenience method that uses the Sam() function to get the address. It is used
to provide a consistent interface for retrieving the SAM address.
#### func (*I2PConfig) Sam
```go
func (f *I2PConfig) Sam() string
```
Sam returns the SAM bridge address as a string in the format "host:port"
#### func (*I2PConfig) SessionStyle
```go
func (f *I2PConfig) SessionStyle() string
```
SessionStyle returns the SAM session style configuration string If no style is
set, defaults to "STREAM"
#### func (*I2PConfig) SetSAMAddress
```go
func (f *I2PConfig) SetSAMAddress(addr string)
```
SetSAMAddress sets the SAM bridge host and port from a combined address string.
If no address is provided, it sets default values for the host and port.
#### func (*I2PConfig) SignatureType
```go
func (f *I2PConfig) SignatureType() string
```
SignatureType returns the SIGNATURE_TYPE configuration string for SAM bridges >=
3.1 Returns empty string if SAM version < 3.1 or if no signature type is set
#### func (*I2PConfig) ToPort
```go
func (f *I2PConfig) ToPort() string
```
ToPort returns the TO_PORT configuration string for SAM bridges >= 3.1 Returns
an empty string if SAM version < 3.1 or if toport is "0"
#### func (*I2PConfig) UsingCompression
```go
func (f *I2PConfig) UsingCompression() string
```
#### type Option
```go
type Option func(*SAMEmit) error
```
Option is a SAMEmit Option
#### type Options
```go
type Options map[string]string
```
options map
#### func (Options) AsList
```go
func (opts Options) AsList() (ls []string)
```
obtain sam options as list of strings
#### type SAM
```go
type SAM struct {
SAMEmit
SAMResolver
net.Conn
// Timeout for SAM connections
Timeout time.Duration
// Context for control of lifecycle
Context context.Context
}
```
Used for controlling I2Ps SAMv3.
#### func NewSAM
```go
func NewSAM(address string) (*SAM, error)
```
NewSAM creates a new SAM instance by connecting to the specified address,
performing the hello handshake, and initializing the SAM resolver. It returns a
pointer to the SAM instance or an error if any step fails. This function
combines connection establishment and hello handshake into a single step,
eliminating the need for separate helper functions. It also initializes the SAM
resolver directly after the connection is established. The SAM instance is ready
to use for further operations like session creation or name resolution.
#### func (*SAM) Close
```go
func (sam *SAM) Close() error
```
close this sam session
#### func (*SAM) EnsureKeyfile
```go
func (sam *SAM) EnsureKeyfile(fname string) (keys i2pkeys.I2PKeys, err error)
```
if keyfile fname does not exist
#### func (*SAM) Keys
```go
func (sam *SAM) Keys() (k *i2pkeys.I2PKeys)
```
#### func (*SAM) Lookup
```go
func (sam *SAM) Lookup(name string) (i2pkeys.I2PAddr, error)
```
Performs a lookup, probably this order: 1) routers known addresses, cached
addresses, 3) by asking peers in the I2P network.
#### func (SAM) NewGenericSession
```go
func (sam SAM) NewGenericSession(style, id string, keys i2pkeys.I2PKeys, extras []string) (Session, error)
```
Creates a new session with the style of either "STREAM", "DATAGRAM" or "RAW",
for a new I2P tunnel with name id, using the cypher keys specified, with the
I2CP/streaminglib-options as specified. Extra arguments can be specified by
setting extra to something else than []string{}. This sam3 instance is now a
session
#### func (SAM) NewGenericSessionWithSignature
```go
func (sam SAM) NewGenericSessionWithSignature(style, id string, keys i2pkeys.I2PKeys, sigType string, extras []string) (Session, error)
```
#### func (SAM) NewGenericSessionWithSignatureAndPorts
```go
func (sam SAM) NewGenericSessionWithSignatureAndPorts(style, id, from, to string, keys i2pkeys.I2PKeys, sigType string, extras []string) (Session, error)
```
Creates a new session with the style of either "STREAM", "DATAGRAM" or "RAW",
for a new I2P tunnel with name id, using the cypher keys specified, with the
I2CP/streaminglib-options as specified. Extra arguments can be specified by
setting extra to something else than []string{}. This sam3 instance is now a
session
#### func (*SAM) NewKeys
```go
func (sam *SAM) NewKeys(sigType ...string) (i2pkeys.I2PKeys, error)
```
Creates the I2P-equivalent of an IP address, that is unique and only the one who
has the private keys can send messages from. The public keys are the I2P
desination (the address) that anyone can send messages to.
#### func (*SAM) ReadKeys
```go
func (sam *SAM) ReadKeys(r io.Reader) (err error)
```
read public/private keys from an io.Reader
#### type SAMEmit
```go
type SAMEmit struct {
I2PConfig
}
```
#### func NewEmit
```go
func NewEmit(opts ...func(*SAMEmit) error) (*SAMEmit, error)
```
#### func (*SAMEmit) Accept
```go
func (e *SAMEmit) Accept() string
```
#### func (*SAMEmit) AcceptBytes
```go
func (e *SAMEmit) AcceptBytes() []byte
```
#### func (*SAMEmit) Connect
```go
func (e *SAMEmit) Connect(dest string) string
```
#### func (*SAMEmit) ConnectBytes
```go
func (e *SAMEmit) ConnectBytes(dest string) []byte
```
#### func (*SAMEmit) Create
```go
func (e *SAMEmit) Create() string
```
#### func (*SAMEmit) CreateBytes
```go
func (e *SAMEmit) CreateBytes() []byte
```
#### func (*SAMEmit) GenerateDestination
```go
func (e *SAMEmit) GenerateDestination() string
```
#### func (*SAMEmit) GenerateDestinationBytes
```go
func (e *SAMEmit) GenerateDestinationBytes() []byte
```
#### func (*SAMEmit) Hello
```go
func (e *SAMEmit) Hello() string
```
#### func (*SAMEmit) HelloBytes
```go
func (e *SAMEmit) HelloBytes() []byte
```
#### func (*SAMEmit) Lookup
```go
func (e *SAMEmit) Lookup(name string) string
```
#### func (*SAMEmit) LookupBytes
```go
func (e *SAMEmit) LookupBytes(name string) []byte
```
#### func (*SAMEmit) SamOptionsString
```go
func (e *SAMEmit) SamOptionsString() string
```
#### type SAMResolver
```go
type SAMResolver struct {
*SAM
}
```
#### func NewFullSAMResolver
```go
func NewFullSAMResolver(address string) (*SAMResolver, error)
```
#### func NewSAMResolver
```go
func NewSAMResolver(parent *SAM) (*SAMResolver, error)
```
#### func (*SAMResolver) Resolve
```go
func (sam *SAMResolver) Resolve(name string) (i2pkeys.I2PAddr, error)
```
Performs a lookup, probably this order: 1) routers known addresses, cached
addresses, 3) by asking peers in the I2P network.
#### type Session
```go
type Session interface {
net.Conn
ID() string
Keys() i2pkeys.I2PKeys
Close() error
}
```

20
common/README.md Normal file
View File

@@ -0,0 +1,20 @@
# go-sam-go/common
Core library for SAMv3 protocol implementation in Go, providing connection management and session configuration for I2P applications.
## Installation
Install using Go modules with the package path `github.com/go-i2p/go-sam-go/common`.
## Usage
The package handles SAM bridge connections, handshakes, and base session management. It provides configuration options for tunnel parameters, encryption settings, and I2P-specific features. The BaseSession implementation must be wrapped in specific session types (stream, datagram, or raw) for actual use.
Key components include SAM connection establishment, I2P address resolution, destination key management, and comprehensive tunnel configuration through the I2PConfig struct.
## Dependencies
- github.com/go-i2p/i2pkeys - I2P cryptographic key handling
- github.com/go-i2p/logger - Logging functionality
- github.com/sirupsen/logrus - Structured logging
- github.com/samber/oops - Enhanced error handling

View File

@@ -3,16 +3,17 @@ package common
import (
"bufio"
"bytes"
"fmt"
"io"
"net"
"os"
"strings"
"github.com/go-i2p/i2pkeys"
"github.com/samber/oops"
"github.com/sirupsen/logrus"
)
// Keys retrieves the I2P destination keys associated with this SAM instance.
// Returns a pointer to the keys used for this SAM session's I2P identity.
func (sam *SAM) Keys() (k *i2pkeys.I2PKeys) {
// TODO: copy them?
log.Debug("Retrieving SAM keys")
@@ -34,74 +35,155 @@ func (sam *SAM) ReadKeys(r io.Reader) (err error) {
return
}
// if keyfile fname does not exist
// EnsureKeyfile ensures cryptographic keys are available, either by generating transient keys
// or by loading/creating persistent keys from the specified file.
func (sam *SAM) EnsureKeyfile(fname string) (keys i2pkeys.I2PKeys, err error) {
log.WithError(err).Error("Failed to load keys")
if fname == "" {
// transient
keys, err = sam.NewKeys()
if err == nil {
sam.SAMEmit.I2PConfig.DestinationKeys = &keys
log.WithFields(logrus.Fields{
"keys": keys,
}).Debug("Generated new transient keys")
}
keys, err = sam.generateTransientKeys()
} else {
// persistent
_, err = os.Stat(fname)
if os.IsNotExist(err) {
// make the keys
keys, err = sam.NewKeys()
if err == nil {
sam.SAMEmit.I2PConfig.DestinationKeys = &keys
// save keys
var f io.WriteCloser
f, err = os.OpenFile(fname, os.O_WRONLY|os.O_CREATE, 0o600)
if err == nil {
err = i2pkeys.StoreKeysIncompat(keys, f)
f.Close()
log.Debug("Generated and saved new keys")
}
}
} else if err == nil {
// we haz key file
var f *os.File
f, err = os.Open(fname)
if err == nil {
keys, err = i2pkeys.LoadKeysIncompat(f)
if err == nil {
sam.SAMEmit.I2PConfig.DestinationKeys = &keys
log.Debug("Loaded existing keys from file")
}
}
}
keys, err = sam.ensurePersistentKeys(fname)
}
if err != nil {
log.WithError(err).Error("Failed to ensure keyfile")
}
return
}
// generateTransientKeys creates new temporary keys that are not saved to disk.
func (sam *SAM) generateTransientKeys() (i2pkeys.I2PKeys, error) {
keys, err := sam.NewKeys()
if err != nil {
return i2pkeys.I2PKeys{}, err
}
sam.SAMEmit.I2PConfig.DestinationKeys = &keys
log.WithFields(logrus.Fields{
"keys": keys,
}).Debug("Generated new transient keys")
return keys, nil
}
// ensurePersistentKeys loads existing keys from file or creates new ones if file doesn't exist.
func (sam *SAM) ensurePersistentKeys(fname string) (i2pkeys.I2PKeys, error) {
_, err := os.Stat(fname)
if os.IsNotExist(err) {
return sam.createAndSaveKeys(fname)
} else if err == nil {
return sam.loadKeysFromFile(fname)
}
return i2pkeys.I2PKeys{}, err
}
// createAndSaveKeys generates new keys and saves them to the specified file.
func (sam *SAM) createAndSaveKeys(fname string) (i2pkeys.I2PKeys, error) {
keys, err := sam.NewKeys()
if err != nil {
return i2pkeys.I2PKeys{}, err
}
sam.SAMEmit.I2PConfig.DestinationKeys = &keys
if err := sam.saveKeysToFile(keys, fname); err != nil {
return i2pkeys.I2PKeys{}, err
}
log.Debug("Generated and saved new keys")
return keys, nil
}
// loadKeysFromFile loads cryptographic keys from the specified file.
func (sam *SAM) loadKeysFromFile(fname string) (i2pkeys.I2PKeys, error) {
f, err := os.Open(fname)
if err != nil {
return i2pkeys.I2PKeys{}, err
}
defer f.Close()
keys, err := i2pkeys.LoadKeysIncompat(f)
if err != nil {
return i2pkeys.I2PKeys{}, err
}
sam.SAMEmit.I2PConfig.DestinationKeys = &keys
log.Debug("Loaded existing keys from file")
return keys, nil
}
// saveKeysToFile saves cryptographic keys to the specified file with appropriate permissions.
func (sam *SAM) saveKeysToFile(keys i2pkeys.I2PKeys, fname string) error {
f, err := os.OpenFile(fname, os.O_WRONLY|os.O_CREATE, 0o600)
if err != nil {
return err
}
defer f.Close()
return i2pkeys.StoreKeysIncompat(keys, f)
}
// Creates the I2P-equivalent of an IP address, that is unique and only the one
// who has the private keys can send messages from. The public keys are the I2P
// desination (the address) that anyone can send messages to.
func (sam *SAM) NewKeys(sigType ...string) (i2pkeys.I2PKeys, error) {
log.WithField("sigType", sigType).Debug("Generating new keys")
sigtmp := ""
sigTypeStr := sam.prepareSigType(sigType)
if err := sam.sendDestGenerateCommand(sigTypeStr); err != nil {
return i2pkeys.I2PKeys{}, err
}
response, err := sam.readKeyGenerationResponse()
if err != nil {
return i2pkeys.I2PKeys{}, err
}
pub, priv, err := sam.parseKeyResponse(response)
if err != nil {
return i2pkeys.I2PKeys{}, err
}
log.Debug("Successfully generated new keys")
return i2pkeys.NewKeys(i2pkeys.I2PAddr(pub), priv), nil
}
// prepareSigType extracts the signature type from the input parameters.
// It returns the first signature type if provided, otherwise returns an empty string.
func (sam *SAM) prepareSigType(sigType []string) string {
if len(sigType) > 0 {
sigtmp = sigType[0]
return sigType[0]
}
if _, err := sam.Conn.Write([]byte("DEST GENERATE " + sigtmp + "\n")); err != nil {
return ""
}
// sendDestGenerateCommand sends the DEST GENERATE command to the SAM connection.
// It constructs and transmits the command with the specified signature type.
func (sam *SAM) sendDestGenerateCommand(sigType string) error {
command := "DEST GENERATE " + sigType + "\n"
if _, err := sam.Conn.Write([]byte(command)); err != nil {
log.WithError(err).Error("Failed to write DEST GENERATE command")
return i2pkeys.I2PKeys{}, fmt.Errorf("error with writing in SAM: %w", err)
return oops.Errorf("error with writing in SAM: %w", err)
}
return nil
}
// readKeyGenerationResponse reads the response from the SAM connection.
// It allocates a buffer and reads the response data for key generation.
func (sam *SAM) readKeyGenerationResponse() ([]byte, error) {
buf := make([]byte, 8192)
n, err := sam.Conn.Read(buf)
if err != nil {
log.WithError(err).Error("Failed to read SAM response for key generation")
return i2pkeys.I2PKeys{}, fmt.Errorf("error with reading in SAM: %w", err)
return nil, oops.Errorf("error with reading in SAM: %w", err)
}
s := bufio.NewScanner(bytes.NewReader(buf[:n]))
return buf[:n], nil
}
// parseKeyResponse parses the SAM response to extract public and private keys.
// It scans the response tokens and extracts the PUB and PRIV key values.
func (sam *SAM) parseKeyResponse(response []byte) (string, string, error) {
s := bufio.NewScanner(bytes.NewReader(response))
s.Split(bufio.ScanWords)
var pub, priv string
@@ -117,11 +199,10 @@ func (sam *SAM) NewKeys(sigType ...string) (i2pkeys.I2PKeys, error) {
priv = text[5:]
} else {
log.Error("Failed to parse keys from SAM response")
return i2pkeys.I2PKeys{}, fmt.Errorf("Failed to parse keys.")
return "", "", oops.Errorf("Failed to parse keys.")
}
}
log.Debug("Successfully generated new keys")
return i2pkeys.NewKeys(i2pkeys.I2PAddr(pub), priv), nil
return pub, priv, nil
}
// Performs a lookup, probably this order: 1) routers known addresses, cached
@@ -131,101 +212,11 @@ func (sam *SAM) Lookup(name string) (i2pkeys.I2PAddr, error) {
return sam.SAMResolver.Resolve(name)
}
// Creates a new session with the style of either "STREAM", "DATAGRAM" or "RAW",
// for a new I2P tunnel with name id, using the cypher keys specified, with the
// I2CP/streaminglib-options as specified. Extra arguments can be specified by
// setting extra to something else than []string{}.
// This sam3 instance is now a session
func (sam *SAM) NewGenericSession(style, id string, keys i2pkeys.I2PKeys, extras []string) (net.Conn, error) {
log.WithFields(logrus.Fields{"style": style, "id": id}).Debug("Creating new generic session")
return sam.NewGenericSessionWithSignature(style, id, keys, SIG_EdDSA_SHA512_Ed25519, extras)
}
func (sam *SAM) NewGenericSessionWithSignature(style, id string, keys i2pkeys.I2PKeys, sigType string, extras []string) (net.Conn, error) {
log.WithFields(logrus.Fields{"style": style, "id": id, "sigType": sigType}).Debug("Creating new generic session with signature")
return sam.NewGenericSessionWithSignatureAndPorts(style, id, "0", "0", keys, sigType, extras)
}
// Creates a new session with the style of either "STREAM", "DATAGRAM" or "RAW",
// for a new I2P tunnel with name id, using the cypher keys specified, with the
// I2CP/streaminglib-options as specified. Extra arguments can be specified by
// setting extra to something else than []string{}.
// This sam3 instance is now a session
func (sam *SAM) NewGenericSessionWithSignatureAndPorts(style, id, from, to string, keys i2pkeys.I2PKeys, sigType string, extras []string) (net.Conn, error) {
log.WithFields(logrus.Fields{"style": style, "id": id, "from": from, "to": to, "sigType": sigType}).Debug("Creating new generic session with signature and ports")
optStr := sam.SamOptionsString()
extraStr := strings.Join(extras, " ")
conn := sam.Conn
fp := ""
tp := ""
if from != "0" {
fp = " FROM_PORT=" + from
}
if to != "0" {
tp = " TO_PORT=" + to
}
scmsg := []byte("SESSION CREATE STYLE=" + style + fp + tp + " ID=" + id + " DESTINATION=" + keys.String() + " " + optStr + extraStr + "\n")
log.WithField("message", string(scmsg)).Debug("Sending SESSION CREATE message")
for m, i := 0, 0; m != len(scmsg); i++ {
if i == 15 {
log.Error("Failed to write SESSION CREATE message after 15 attempts")
conn.Close()
return nil, fmt.Errorf("writing to SAM failed")
}
n, err := conn.Write(scmsg[m:])
if err != nil {
log.WithError(err).Error("Failed to write to SAM connection")
conn.Close()
return nil, fmt.Errorf("writing to connection failed: %w", err)
}
m += n
}
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
log.WithError(err).Error("Failed to read SAM response")
conn.Close()
return nil, fmt.Errorf("reading from connection failed: %w", err)
}
text := string(buf[:n])
log.WithField("response", text).Debug("Received SAM response")
if strings.HasPrefix(text, SESSION_OK) {
if keys.String() != text[len(SESSION_OK):len(text)-1] {
log.Error("SAM created a tunnel with different keys than requested")
conn.Close()
return nil, fmt.Errorf("SAMv3 created a tunnel with keys other than the ones we asked it for")
}
log.Debug("Successfully created new session")
return conn, nil //&StreamSession{id, conn, keys, nil, sync.RWMutex{}, nil}, nil
} else if text == SESSION_DUPLICATE_ID {
log.Error("Duplicate tunnel name")
conn.Close()
return nil, fmt.Errorf("Duplicate tunnel name")
} else if text == SESSION_DUPLICATE_DEST {
log.Error("Duplicate destination")
conn.Close()
return nil, fmt.Errorf("Duplicate destination")
} else if text == SESSION_INVALID_KEY {
log.Error("Invalid key for SAM session")
conn.Close()
return nil, fmt.Errorf("Invalid key - SAM session")
} else if strings.HasPrefix(text, SESSION_I2P_ERROR) {
log.WithField("error", text[len(SESSION_I2P_ERROR):]).Error("I2P error")
conn.Close()
return nil, fmt.Errorf("I2P error " + text[len(SESSION_I2P_ERROR):])
} else {
log.WithField("reply", text).Error("Unable to parse SAMv3 reply")
conn.Close()
return nil, fmt.Errorf("Unable to parse SAMv3 reply: " + text)
}
}
// close this sam session
func (sam *SAM) Close() error {
log.Debug("Closing SAM session")
return sam.Conn.Close()
if sam.Conn != nil {
log.Debug("Closing SAM session")
return sam.Conn.Close()
}
return nil
}

View File

@@ -2,77 +2,74 @@ package common
import (
"fmt"
"math/rand"
"net"
"strconv"
"strings"
"time"
"github.com/sirupsen/logrus"
)
const (
defaultSAMHost = "127.0.0.1"
defaultSAMPort = 7656
)
// Sam returns the SAM bridge address as a string in the format "host:port"
func (f *I2PConfig) Sam() string {
// Set default values
host := "127.0.0.1"
port := 7656
// Override defaults if config values are set
if f.SamHost != "" {
host = f.SamHost
}
if f.SamPort != 0 {
port = f.SamPort
host := f.SamHost
if host == "" {
host = defaultSAMHost
}
// Log the SAM address being constructed
log.WithFields(logrus.Fields{
"host": host,
"port": port,
}).Debug("SAM address constructed")
port := f.SamPort
if port == 0 {
port = defaultSAMPort
}
// Return formatted SAM address
return net.JoinHostPort(host, strconv.Itoa(port))
return fmt.Sprintf("%s:%d", host, port)
}
// SAMAddress returns the SAM bridge address in the format "host:port"
// This is a convenience method that uses the Sam() function to get the address.
// It is used to provide a consistent interface for retrieving the SAM address.
func (f *I2PConfig) SAMAddress() string {
// Return the SAM address in the format "host:port"
return f.Sam()
}
// SetSAMAddress sets the SAM bridge host and port from a combined address string.
// If no address is provided, it sets default values for the host and port.
func (f *I2PConfig) SetSAMAddress(addr string) {
if addr == "" {
f.SamHost = "127.0.0.1"
f.SamPort = 7656
f.SamHost = defaultSAMHost
f.SamPort = defaultSAMPort
return
}
host, port, err := net.SplitHostPort(addr)
host, portStr, err := SplitHostPort(addr)
if err != nil {
// Only set host if it looks valid
if net.ParseIP(addr) != nil || !strings.Contains(addr, ":") {
f.SamHost = addr
}
log.WithError(err).Warn("Failed to parse SAM address, using defaults")
f.SamHost = defaultSAMHost
f.SamPort = defaultSAMPort
return
}
f.SamHost = host
if p, err := strconv.Atoi(port); err == nil && p > 0 && p < 65536 {
f.SamPort = p
port, err := strconv.Atoi(portStr)
if err != nil || port < 1 || port > 65535 {
log.WithField("port", portStr).Warn("Invalid port, setting to 7656")
f.SamPort = defaultSAMPort
} else {
f.SamPort = port
}
}
// ID returns the tunnel name as a formatted string. If no tunnel name is set,
// generates a random 12-character name using lowercase letters.
func (f *I2PConfig) ID() string {
generator := rand.New(rand.NewSource(time.Now().UnixNano()))
// If no tunnel name set, generate random one
// Ensure tunnel name is set, generating if needed
if f.TunName == "" {
// Generate 12 random lowercase letters
b := make([]byte, 12)
for i := range b {
b[i] = "abcdefghijklmnopqrstuvwxyz"[generator.Intn(26)]
}
f.TunName = string(b)
// Log the generated name
f.TunName = f.generateRandomTunnelName()
log.WithField("TunName", f.TunName).Debug("Generated random tunnel name")
}
@@ -114,8 +111,9 @@ func (f *I2PConfig) LeaseSetSettings() (string, string, string) {
// FromPort returns the FROM_PORT configuration string for SAM bridges >= 3.1
// Returns an empty string if SAM version < 3.1 or if fromport is "0"
func (f *I2PConfig) FromPort() string {
minv, _ := strconv.Atoi(DEFAULT_SAM_MIN)
// Check SAM version compatibility
if f.SamMax == "" || f.samMax() < 3.1 {
if f.samMax() < minv {
log.Debug("SAM version < 3.1, FromPort not applicable")
return ""
}
@@ -133,8 +131,9 @@ func (f *I2PConfig) FromPort() string {
// ToPort returns the TO_PORT configuration string for SAM bridges >= 3.1
// Returns an empty string if SAM version < 3.1 or if toport is "0"
func (f *I2PConfig) ToPort() string {
minv, _ := strconv.Atoi(DEFAULT_SAM_MIN)
// Check SAM version compatibility
if f.samMax() < 3.1 {
if f.samMax() < minv {
log.Debug("SAM version < 3.1, ToPort not applicable")
return ""
}
@@ -165,12 +164,13 @@ func (f *I2PConfig) SessionStyle() string {
// samMax returns the maximum SAM version supported as a float64
// If parsing fails, returns default value 3.1
func (f *I2PConfig) samMax() float64 {
func (f *I2PConfig) samMax() int {
// Parse SAM max version to integer
i, err := strconv.ParseFloat(f.SamMax, 64)
i, err := strconv.Atoi(f.MaxSAM())
if err != nil {
log.WithError(err).Warn("Failed to parse SamMax, using default 3.1")
return 3.1
i, _ = strconv.Atoi(DEFAULT_SAM_MIN)
return i
}
// Log the parsed version and return
@@ -182,8 +182,8 @@ func (f *I2PConfig) samMax() float64 {
// If no minimum version is set, returns default value "3.0"
func (f *I2PConfig) MinSAM() string {
if f.SamMin == "" {
log.Debug("Using default MinSAM: 3.0")
return "3.0"
log.Debug("Using default MinSAM: " + DEFAULT_SAM_MIN)
return DEFAULT_SAM_MIN
}
log.WithField("minSAM", f.SamMin).Debug("MinSAM set")
return f.SamMin
@@ -193,8 +193,8 @@ func (f *I2PConfig) MinSAM() string {
// If no maximum version is set, returns default value "3.1"
func (f *I2PConfig) MaxSAM() string {
if f.SamMax == "" {
log.Debug("Using default MaxSAM: 3.1")
return "3.1"
log.Debug("Using default MaxSAM: " + DEFAULT_SAM_MAX)
return DEFAULT_SAM_MAX
}
log.WithField("maxSAM", f.SamMax).Debug("MaxSAM set")
return f.SamMax
@@ -218,8 +218,9 @@ func (f *I2PConfig) DestinationKey() string {
// SignatureType returns the SIGNATURE_TYPE configuration string for SAM bridges >= 3.1
// Returns empty string if SAM version < 3.1 or if no signature type is set
func (f *I2PConfig) SignatureType() string {
minv, _ := strconv.Atoi(DEFAULT_SAM_MIN)
// Check SAM version compatibility
if f.samMax() < 3.1 {
if f.samMax() < minv {
log.Debug("SAM version < 3.1, SignatureType not applicable")
return ""
}
@@ -237,9 +238,9 @@ func (f *I2PConfig) SignatureType() string {
// EncryptLease returns the lease set encryption configuration string
// Returns "i2cp.encryptLeaseSet=true" if encryption is enabled, empty string otherwise
func (f *I2PConfig) EncryptLease() string {
if f.EncryptLeaseSet == true {
if f.EncryptLeaseSet {
log.Debug("Lease set encryption enabled")
return fmt.Sprintf(" i2cp.encryptLeaseSet=true ")
return " i2cp.encryptLeaseSet=true "
}
log.Debug("Lease set encryption not enabled")
return ""
@@ -261,158 +262,171 @@ func (f *I2PConfig) Reliability() string {
// Reduce returns I2CP reduce-on-idle configuration settings as a string if enabled
func (f *I2PConfig) Reduce() string {
// If reduce idle is enabled, return formatted configuration string
if f.ReduceIdle == true {
// Log the reduce idle settings being applied
log.WithFields(logrus.Fields{
"reduceIdle": f.ReduceIdle,
"reduceIdleTime": f.ReduceIdleTime,
"reduceIdleQuantity": f.ReduceIdleQuantity,
}).Debug("Reduce idle settings applied")
// Return formatted configuration string using Sprintf
return fmt.Sprintf("i2cp.reduceOnIdle=%t"+
"i2cp.reduceIdleTime=%d"+
"i2cp.reduceQuantity=%d",
f.ReduceIdle,
f.ReduceIdleTime,
f.ReduceIdleQuantity)
// Return early if reduce idle is not enabled
if !f.ReduceIdle {
log.Debug("Reduce idle settings not applied")
return ""
}
// Log when reduce idle is not enabled
log.Debug("Reduce idle settings not applied")
return ""
// Log and return the reduce idle configuration
result := fmt.Sprintf("i2cp.reduceOnIdle=%t i2cp.reduceIdleTime=%d i2cp.reduceQuantity=%d",
f.ReduceIdle, f.ReduceIdleTime, f.ReduceIdleQuantity)
log.WithField("config", result).Debug("Reduce idle settings applied")
return result
}
// Close returns I2CP close-on-idle configuration settings as a string if enabled
func (f *I2PConfig) Close() string {
// If close idle is enabled, return formatted configuration string
if f.CloseIdle == true {
// Log the close idle settings being applied
log.WithFields(logrus.Fields{
"closeIdle": f.CloseIdle,
"closeIdleTime": f.CloseIdleTime,
}).Debug("Close idle settings applied")
// Return formatted configuration string using Sprintf
return fmt.Sprintf("i2cp.closeOnIdle=%t"+
"i2cp.closeIdleTime=%d",
f.CloseIdle,
f.CloseIdleTime)
// Return early if close idle is not enabled
if !f.CloseIdle {
log.Debug("Close idle settings not applied")
return ""
}
// Log when close idle is not enabled
log.Debug("Close idle settings not applied")
return ""
// Log and return the close idle configuration
result := fmt.Sprintf("i2cp.closeOnIdle=%t i2cp.closeIdleTime=%d",
f.CloseIdle, f.CloseIdleTime)
log.WithField("config", result).Debug("Close idle settings applied")
return result
}
// DoZero returns the zero hop and fast receive configuration string settings
func (f *I2PConfig) DoZero() string {
// Build settings using slices for cleaner concatenation
var settings []string
// Add inbound zero hop setting if enabled
if f.InAllowZeroHop == true {
if f.InAllowZeroHop {
settings = append(settings, fmt.Sprintf("inbound.allowZeroHop=%t", f.InAllowZeroHop))
}
// Add outbound zero hop setting if enabled
if f.OutAllowZeroHop == true {
if f.OutAllowZeroHop {
settings = append(settings, fmt.Sprintf("outbound.allowZeroHop=%t", f.OutAllowZeroHop))
}
// Add fast receive setting if enabled
if f.FastRecieve == true {
if f.FastRecieve {
settings = append(settings, fmt.Sprintf("i2cp.fastRecieve=%t", f.FastRecieve))
}
// Join all settings with spaces
result := strings.Join(settings, " ")
// Log the final settings
log.WithField("zeroHopSettings", result).Debug("Zero hop settings applied")
return result
}
// formatConfigPair creates a configuration string for inbound/outbound pairs
func (f *I2PConfig) formatConfigPair(direction, property string, value interface{}) string {
var valueStr string
switch v := value.(type) {
case int:
if v == 0 {
return "" // Skip zero values to avoid duplicates
}
valueStr = strconv.Itoa(v)
case string:
if v == "" {
return ""
}
valueStr = v
case bool:
valueStr = strconv.FormatBool(v)
default:
return ""
}
return fmt.Sprintf("%s.%s=%s", direction, property, valueStr)
}
// InboundLength returns the inbound tunnel length configuration string.
// Specifies the desired length of the inbound tunnel (number of hops).
func (f *I2PConfig) InboundLength() string {
return fmt.Sprintf("inbound.length=%d", f.InLength)
return f.formatConfigPair("inbound", "length", f.InLength)
}
// OutboundLength returns the outbound tunnel length configuration string.
// Specifies the desired length of the outbound tunnel (number of hops).
func (f *I2PConfig) OutboundLength() string {
return fmt.Sprintf("outbound.length=%d", f.OutLength)
return f.formatConfigPair("outbound", "length", f.OutLength)
}
// InboundLengthVariance returns the inbound tunnel length variance configuration string.
// Controls the randomness in inbound tunnel hop count for improved anonymity.
func (f *I2PConfig) InboundLengthVariance() string {
return fmt.Sprintf("inbound.lengthVariance=%d", f.InVariance)
return f.formatConfigPair("inbound", "lengthVariance", f.InVariance)
}
// OutboundLengthVariance returns the outbound tunnel length variance configuration string.
// Controls the randomness in outbound tunnel hop count for improved anonymity.
func (f *I2PConfig) OutboundLengthVariance() string {
return fmt.Sprintf("outbound.lengthVariance=%d", f.OutVariance)
return f.formatConfigPair("outbound", "lengthVariance", f.OutVariance)
}
// InboundBackupQuantity returns the inbound tunnel backup quantity configuration string.
// Specifies the number of backup tunnels to maintain for inbound connections.
func (f *I2PConfig) InboundBackupQuantity() string {
return fmt.Sprintf("inbound.backupQuantity=%d", f.InBackupQuantity)
return f.formatConfigPair("inbound", "backupQuantity", f.InBackupQuantity)
}
// OutboundBackupQuantity returns the outbound tunnel backup quantity configuration string.
// Specifies the number of backup tunnels to maintain for outbound connections.
func (f *I2PConfig) OutboundBackupQuantity() string {
return fmt.Sprintf("outbound.backupQuantity=%d", f.OutBackupQuantity)
return f.formatConfigPair("outbound", "backupQuantity", f.OutBackupQuantity)
}
// InboundQuantity returns the inbound tunnel quantity configuration string.
// Specifies the number of parallel inbound tunnels to maintain for load balancing.
func (f *I2PConfig) InboundQuantity() string {
return fmt.Sprintf("inbound.quantity=%d", f.InQuantity)
return f.formatConfigPair("inbound", "quantity", f.InQuantity)
}
// OutboundQuantity returns the outbound tunnel quantity configuration string.
// Specifies the number of parallel outbound tunnels to maintain for load balancing.
func (f *I2PConfig) OutboundQuantity() string {
return fmt.Sprintf("outbound.quantity=%d", f.OutQuantity)
return f.formatConfigPair("outbound", "quantity", f.OutQuantity)
}
// UsingCompression returns the compression configuration string for I2P streams.
// Enables or disables data compression to reduce bandwidth usage at the cost of CPU overhead.
func (f *I2PConfig) UsingCompression() string {
return fmt.Sprintf("i2cp.gzip=%t", f.UseCompression)
return f.formatConfigPair("i2cp", "useCompression", f.UseCompression)
}
// Print returns a slice of strings containing all the I2P configuration settings
func (f *I2PConfig) Print() []string {
// Get lease set settings
lsk, lspk, lspsk := f.LeaseSetSettings()
var configs []string
// Build the configuration settings slice
settings := []string{
f.InboundLength(),
f.OutboundLength(),
f.InboundLengthVariance(),
f.OutboundLengthVariance(),
f.InboundBackupQuantity(),
f.OutboundBackupQuantity(),
f.InboundQuantity(),
f.OutboundQuantity(),
f.UsingCompression(),
f.DoZero(), // Zero hop settings
f.Reduce(), // Reduce idle settings
f.Close(), // Close idle settings
f.Reliability(), // Message reliability
f.EncryptLease(), // Lease encryption
lsk, lspk, lspsk, // Lease set keys
f.Accesslisttype(), // Access list type
f.Accesslist(), // Access list
f.LeaseSetEncryptionType(), // Lease set encryption type
// Helper function to add non-empty strings to the config slice
addConfig := func(config string) {
if strings.TrimSpace(config) != "" {
configs = append(configs, strings.TrimSpace(config))
}
}
return settings
// Collect all configuration settings, filtering out empty strings
allSettings := [][]string{
f.collectTunnelSettings(),
f.collectConnectionSettings(),
f.collectLeaseSetSettings(),
f.collectAccessSettings(),
}
for _, settingsGroup := range allSettings {
for _, setting := range settingsGroup {
addConfig(setting)
}
}
log.WithField("configs", configs).Debug("Configuration strings collected")
return configs
}
// Accesslisttype returns the I2CP access list configuration string based on the AccessListType setting
func (f *I2PConfig) Accesslisttype() string {
switch f.AccessListType {
case ACCESS_TYPE_WHITELIST:
log.Debug("Access list type set to whitelist")
return fmt.Sprintf("i2cp.enableAccessList=true")
log.Debug("Access list type set to allowlist")
return "i2cp.enableAccessList=true"
case ACCESS_TYPE_BLACKLIST:
log.Debug("Access list type set to blacklist")
return fmt.Sprintf("i2cp.enableBlackList=true")
case ACCESS_TYPE_NONE:
log.Debug("Access list type set to none")
return ""
log.Debug("Access list type set to blocklist")
return "i2cp.enableBlackList=true"
default:
log.Debug("Access list type not set")
return ""
@@ -425,10 +439,8 @@ func (f *I2PConfig) Accesslist() string {
if f.AccessListType != "" && len(f.AccessList) > 0 {
// Join access list entries with commas
accessList := strings.Join(f.AccessList, ",")
// Log the generated access list
log.WithField("accessList", accessList).Debug("Access list generated")
// Return formatted access list configuration
return fmt.Sprintf("i2cp.accessList=%s", accessList)
}
@@ -442,56 +454,50 @@ func (f *I2PConfig) Accesslist() string {
// If no encryption type is set, returns default value "4,0".
// Validates that all encryption types are valid integers.
func (f *I2PConfig) LeaseSetEncryptionType() string {
// Use default encryption type if none specified
// Use default if not specified
if f.LeaseSetEncryption == "" {
f.LeaseSetEncryption = "4,0" // Set default ECIES-X25519 and ElGamal compatibility
log.Debug("Using default lease set encryption type: 4,0")
return "i2cp.leaseSetEncType=4,0"
}
// Validate each encryption type is a valid integer
for _, s := range strings.Split(f.LeaseSetEncryption, ",") {
if _, err := strconv.Atoi(s); err != nil {
log.WithField("invalidType", s).Panic("Invalid encrypted leaseSet type")
// panic("Invalid encrypted leaseSet type: " + s)
}
// Validate all encryption types are integers
if err := f.validateEncryptionTypes(f.LeaseSetEncryption); err != nil {
log.WithError(err).Warn("Invalid encryption types, using default")
f.LeaseSetEncryption = "4,0"
}
// Log and return the configured encryption type
log.WithField("leaseSetEncType", f.LeaseSetEncryption).Debug("Lease set encryption type set")
return fmt.Sprintf("i2cp.leaseSetEncType=%s", f.LeaseSetEncryption)
return f.formatLeaseSetEncryptionType(f.LeaseSetEncryption)
}
// NewConfig creates a new I2PConfig instance with default values and applies functional options.
// Returns a configured instance ready for use in session creation or an error if any option fails.
// Example: config, err := NewConfig(SetInLength(4), SetOutLength(4))
func NewConfig(opts ...func(*I2PConfig) error) (*I2PConfig, error) {
var config I2PConfig
config.SamHost = "127.0.0.1"
config.SamPort = 7656
config.SamMin = DEFAULT_SAM_MIN
config.SamMax = DEFAULT_SAM_MAX
config.TunName = ""
config.TunType = "server"
config.Style = "STREAM"
config.InLength = 3
config.OutLength = 3
config.InQuantity = 2
config.OutQuantity = 2
config.InVariance = 1
config.OutVariance = 1
config.InBackupQuantity = 3
config.OutBackupQuantity = 3
config.InAllowZeroHop = false
config.OutAllowZeroHop = false
config.EncryptLeaseSet = false
config.LeaseSetKey = ""
config.LeaseSetPrivateKey = ""
config.LeaseSetPrivateSigningKey = ""
config.FastRecieve = false
config.UseCompression = true
config.ReduceIdle = false
config.ReduceIdleTime = 15
config.ReduceIdleQuantity = 4
config.CloseIdle = false
config.CloseIdleTime = 300000
config.MessageReliability = "none"
// Initialize with struct literal containing only non-zero defaults
// Go automatically zero-initializes all other fields
config := I2PConfig{
SamHost: "127.0.0.1",
SamPort: 7656,
SamMin: DEFAULT_SAM_MIN,
SamMax: DEFAULT_SAM_MAX,
TunType: "server",
Style: SESSION_STYLE_STREAM,
InLength: 3,
OutLength: 3,
InQuantity: 2,
OutQuantity: 2,
InVariance: 1,
OutVariance: 1,
InBackupQuantity: 3,
OutBackupQuantity: 3,
UseCompression: true,
ReduceIdleTime: 15,
ReduceIdleQuantity: 4,
CloseIdleTime: 300000,
MessageReliability: "none",
}
// Apply functional options
for _, opt := range opts {
if err := opt(&config); err != nil {
return nil, err

View File

@@ -1,6 +1,7 @@
package common
import (
"strings"
"testing"
)
@@ -27,13 +28,13 @@ func TestSetSAMAddress_Cases(t *testing.T) {
name: "invalid port uses default",
addr: "localhost:99999",
wantHost: "localhost",
wantPort: 0,
wantPort: 7656, // Default port
},
{
name: "just IP address",
addr: "192.168.1.1",
wantHost: "192.168.1.1",
wantPort: 0,
wantPort: 7656,
},
}
@@ -158,3 +159,44 @@ func TestLeaseSetSettings_Formatting(t *testing.T) {
})
}
}
func TestTunnelConfigNoDuplicates(t *testing.T) {
cfg := &I2PConfig{
InLength: 3,
OutLength: 3,
InQuantity: 2,
OutQuantity: 2,
}
params := cfg.Print()
// Verify no duplicate parameters
seen := make(map[string]bool)
for _, param := range params {
if seen[param] {
t.Errorf("Duplicate parameter found: %s", param)
}
seen[param] = true
}
// Verify expected parameters present
expectedParams := []string{
"inbound.length=3",
"outbound.length=3",
"inbound.quantity=2",
"outbound.quantity=2",
}
for _, expected := range expectedParams {
found := false
for _, param := range params {
if strings.Contains(param, expected) {
found = true
}
}
if !found {
t.Errorf("Expected parameter not found: %s", expected)
}
}
t.Logf("Tunnel configuration parameters: %v", params)
}

View File

@@ -1,10 +1,19 @@
package common
// DEFAULT_SAM_MIN specifies the minimum supported SAM protocol version.
// This constant is used during SAM bridge handshake to negotiate protocol compatibility.
const (
DEFAULT_SAM_MIN = "3.1"
// DEFAULT_SAM_MAX specifies the maximum supported SAM protocol version.
// This allows the library to work with newer SAM protocol features when available.
DEFAULT_SAM_MAX = "3.3"
)
// SESSION_OK indicates successful session creation with destination key.
// SESSION_DUPLICATE_ID indicates session creation failed due to duplicate session ID.
// SESSION_DUPLICATE_DEST indicates session creation failed due to duplicate destination.
// SESSION_INVALID_KEY indicates session creation failed due to invalid destination key.
// SESSION_I2P_ERROR indicates session creation failed due to I2P router error.
const (
SESSION_OK = "SESSION STATUS RESULT=OK DESTINATION="
SESSION_DUPLICATE_ID = "SESSION STATUS RESULT=DUPLICATED_ID\n"
@@ -13,6 +22,13 @@ const (
SESSION_I2P_ERROR = "SESSION STATUS RESULT=I2P_ERROR MESSAGE="
)
// SIG_NONE is deprecated, use SIG_DEFAULT instead for secure signatures.
// SIG_DSA_SHA1 specifies DSA with SHA1 signature type (legacy, not recommended).
// SIG_ECDSA_SHA256_P256 specifies ECDSA with SHA256 on P256 curve signature type.
// SIG_ECDSA_SHA384_P384 specifies ECDSA with SHA384 on P384 curve signature type.
// SIG_ECDSA_SHA512_P521 specifies ECDSA with SHA512 on P521 curve signature type.
// SIG_EdDSA_SHA512_Ed25519 specifies EdDSA with SHA512 on Ed25519 curve signature type.
// SIG_DEFAULT points to the recommended secure signature type for new applications.
const (
SIG_NONE = "SIGNATURE_TYPE=EdDSA_SHA512_Ed25519"
SIG_DSA_SHA1 = "SIGNATURE_TYPE=DSA_SHA1"
@@ -20,25 +36,38 @@ const (
SIG_ECDSA_SHA384_P384 = "SIGNATURE_TYPE=ECDSA_SHA384_P384"
SIG_ECDSA_SHA512_P521 = "SIGNATURE_TYPE=ECDSA_SHA512_P521"
SIG_EdDSA_SHA512_Ed25519 = "SIGNATURE_TYPE=EdDSA_SHA512_Ed25519"
// Add a default constant that points to the recommended secure signature type
SIG_DEFAULT = SIG_EdDSA_SHA512_Ed25519
)
// SAM_RESULT_OK indicates successful SAM operation completion.
// SAM_RESULT_INVALID_KEY indicates SAM operation failed due to invalid key format.
// SAM_RESULT_KEY_NOT_FOUND indicates SAM operation failed due to missing key.
const (
SAM_RESULT_OK = "RESULT=OK"
SAM_RESULT_INVALID_KEY = "RESULT=INVALID_KEY"
SAM_RESULT_KEY_NOT_FOUND = "RESULT=KEY_NOT_FOUND"
)
// HELLO_REPLY_OK indicates successful SAM handshake completion.
// HELLO_REPLY_NOVERSION indicates SAM handshake failed due to unsupported protocol version.
const (
HELLO_REPLY_OK = "HELLO REPLY RESULT=OK"
HELLO_REPLY_NOVERSION = "HELLO REPLY RESULT=NOVERSION\n"
)
// SESSION_STYLE_STREAM creates TCP-like reliable connection sessions.
// SESSION_STYLE_DATAGRAM creates UDP-like message-based sessions.
// SESSION_STYLE_RAW creates low-level packet transmission sessions.
const (
SESSION_STYLE_STREAM = "STREAM"
SESSION_STYLE_DATAGRAM = "DATAGRAM"
SESSION_STYLE_RAW = "RAW"
)
// ACCESS_TYPE_WHITELIST allows only specified destinations in access list.
// ACCESS_TYPE_BLACKLIST blocks specified destinations in access list.
// ACCESS_TYPE_NONE disables access list filtering entirely.
const (
ACCESS_TYPE_WHITELIST = "whitelist"
ACCESS_TYPE_BLACKLIST = "blacklist"

View File

@@ -1,10 +1,10 @@
package common
import (
"fmt"
"strconv"
"strings"
"github.com/samber/oops"
"github.com/sirupsen/logrus"
)
@@ -14,21 +14,15 @@ type Option func(*SAMEmit) error
// SetType sets the type of the forwarder server
func SetType(s string) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if s == SESSION_STYLE_STREAM {
c.Style = s
log.WithField("style", s).Debug("Set session style")
return nil
} else if s == SESSION_STYLE_DATAGRAM {
c.Style = s
log.WithField("style", s).Debug("Set session style")
return nil
} else if s == SESSION_STYLE_RAW {
if s == SESSION_STYLE_STREAM ||
s == SESSION_STYLE_DATAGRAM ||
s == SESSION_STYLE_RAW {
c.Style = s
log.WithField("style", s).Debug("Set session style")
return nil
}
log.WithField("style", s).Error("Invalid session style")
return fmt.Errorf("Invalid session STYLE=%s, must be STREAM, DATAGRAM, or RAW", s)
return oops.Errorf("Invalid session STYLE=%s, must be STREAM, DATAGRAM, or RAW", s)
}
}
@@ -38,14 +32,14 @@ func SetSAMAddress(s string) func(*SAMEmit) error {
sp := strings.Split(s, ":")
if len(sp) > 2 {
log.WithField("address", s).Error("Invalid SAM address")
return fmt.Errorf("Invalid address string: %s", sp)
return oops.Errorf("Invalid address string: %s", sp)
}
if len(sp) == 2 {
var err error
c.I2PConfig.SamPort, err = strconv.Atoi(sp[1])
if err != nil {
log.WithField("port", sp[1]).Error("Invalid SAM port")
return fmt.Errorf("Invalid SAM Port %s; non-number", sp[1])
return oops.Errorf("Invalid SAM Port %s; non-number", sp[1])
}
}
c.I2PConfig.SamHost = sp[0]
@@ -72,7 +66,7 @@ func SetSAMPort(s string) func(*SAMEmit) error {
port, err := strconv.Atoi(s)
if err != nil {
log.WithField("port", s).Error("Invalid SAM port: non-number")
return fmt.Errorf("Invalid SAM Port %s; non-number", s)
return oops.Errorf("Invalid SAM Port %s; non-number", s)
}
if port < 65536 && port > -1 {
c.I2PConfig.SamPort = port
@@ -80,7 +74,7 @@ func SetSAMPort(s string) func(*SAMEmit) error {
return nil
}
log.WithField("port", port).Error("Invalid SAM port")
return fmt.Errorf("Invalid port")
return oops.Errorf("Invalid port")
}
}
@@ -102,7 +96,7 @@ func SetInLength(u int) func(*SAMEmit) error {
return nil
}
log.WithField("inLength", u).Error("Invalid inbound tunnel length")
return fmt.Errorf("Invalid inbound tunnel length")
return oops.Errorf("Invalid inbound tunnel length")
}
}
@@ -115,7 +109,7 @@ func SetOutLength(u int) func(*SAMEmit) error {
return nil
}
log.WithField("outLength", u).Error("Invalid outbound tunnel length")
return fmt.Errorf("Invalid outbound tunnel length")
return oops.Errorf("Invalid outbound tunnel length")
}
}
@@ -128,7 +122,7 @@ func SetInVariance(i int) func(*SAMEmit) error {
return nil
}
log.WithField("inVariance", i).Error("Invalid inbound tunnel variance")
return fmt.Errorf("Invalid inbound tunnel length")
return oops.Errorf("Invalid inbound tunnel length")
}
}
@@ -141,7 +135,7 @@ func SetOutVariance(i int) func(*SAMEmit) error {
return nil
}
log.WithField("outVariance", i).Error("Invalid outbound tunnel variance")
return fmt.Errorf("Invalid outbound tunnel variance")
return oops.Errorf("Invalid outbound tunnel variance")
}
}
@@ -154,7 +148,7 @@ func SetInQuantity(u int) func(*SAMEmit) error {
return nil
}
log.WithField("inQuantity", u).Error("Invalid inbound tunnel quantity")
return fmt.Errorf("Invalid inbound tunnel quantity")
return oops.Errorf("Invalid inbound tunnel quantity")
}
}
@@ -167,7 +161,7 @@ func SetOutQuantity(u int) func(*SAMEmit) error {
return nil
}
log.WithField("outQuantity", u).Error("Invalid outbound tunnel quantity")
return fmt.Errorf("Invalid outbound tunnel quantity")
return oops.Errorf("Invalid outbound tunnel quantity")
}
}
@@ -180,7 +174,7 @@ func SetInBackups(u int) func(*SAMEmit) error {
return nil
}
log.WithField("inBackups", u).Error("Invalid inbound tunnel backup quantity")
return fmt.Errorf("Invalid inbound tunnel backup quantity")
return oops.Errorf("Invalid inbound tunnel backup quantity")
}
}
@@ -193,7 +187,7 @@ func SetOutBackups(u int) func(*SAMEmit) error {
return nil
}
log.WithField("outBackups", u).Error("Invalid outbound tunnel backup quantity")
return fmt.Errorf("Invalid outbound tunnel backup quantity")
return oops.Errorf("Invalid outbound tunnel backup quantity")
}
}
@@ -285,6 +279,8 @@ func SetCompress(b bool) func(*SAMEmit) error {
}
}
// SetFastRecieve enables or disables fast receive mode for improved performance.
// When enabled, allows bypassing some protocol overhead for faster data transmission.
// SetFastRecieve tells clients to use compression
func SetFastRecieve(b bool) func(*SAMEmit) error {
return func(c *SAMEmit) error {
@@ -322,7 +318,7 @@ func SetReduceIdleTime(u int) func(*SAMEmit) error {
return nil
}
log.WithField("minutes", u).Error("Invalid reduce idle timeout")
return fmt.Errorf("Invalid reduce idle timeout(Measured in minutes) %v", u)
return oops.Errorf("Invalid reduce idle timeout(Measured in minutes) %v", u)
}
}
@@ -336,7 +332,7 @@ func SetReduceIdleTimeMs(u int) func(*SAMEmit) error {
return nil
}
log.WithField("milliseconds", u).Error("Invalid reduce idle timeout")
return fmt.Errorf("Invalid reduce idle timeout(Measured in milliseconds) %v", u)
return oops.Errorf("Invalid reduce idle timeout(Measured in milliseconds) %v", u)
}
}
@@ -349,7 +345,7 @@ func SetReduceIdleQuantity(u int) func(*SAMEmit) error {
return nil
}
log.WithField("quantity", u).Error("Invalid reduce tunnel quantity")
return fmt.Errorf("Invalid reduce tunnel quantity")
return oops.Errorf("Invalid reduce tunnel quantity")
}
}
@@ -379,7 +375,7 @@ func SetCloseIdleTime(u int) func(*SAMEmit) error {
return nil
}
log.WithField("minutes", u).Error("Invalid close idle timeout")
return fmt.Errorf("Invalid close idle timeout(Measured in minutes) %v", u)
return oops.Errorf("Invalid close idle timeout(Measured in minutes) %v", u)
}
}
@@ -392,7 +388,7 @@ func SetCloseIdleTimeMs(u int) func(*SAMEmit) error {
log.WithField("closeIdleTimeMs", u).Debug("Set close idle time in milliseconds")
return nil
}
return fmt.Errorf("Invalid close idle timeout(Measured in milliseconds) %v", u)
return oops.Errorf("Invalid close idle timeout(Measured in milliseconds) %v", u)
}
}
@@ -412,7 +408,7 @@ func SetAccessListType(s string) func(*SAMEmit) error {
log.Debug("Set access list type to none")
return nil
}
return fmt.Errorf("Invalid Access list type (whitelist, blacklist, none)")
return oops.Errorf("Invalid Access list type (whitelist, blacklist, none)")
}
}
@@ -420,9 +416,7 @@ func SetAccessListType(s string) func(*SAMEmit) error {
func SetAccessList(s []string) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if len(s) > 0 {
for _, a := range s {
c.I2PConfig.AccessList = append(c.I2PConfig.AccessList, a)
}
c.I2PConfig.AccessList = append(c.I2PConfig.AccessList, s...)
log.WithField("accessList", s).Debug("Set access list")
return nil
}

View File

@@ -20,10 +20,11 @@ func TestSetInQuantity(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
emit := &SAMEmit{I2PConfig: I2PConfig{}}
err := SetInQuantity(tt.input)(emit)
if (err != nil) != tt.wantErr {
t.Errorf("SetInQuantity() error = %v, wantErr %v", err, tt.wantErr)
return
if err != nil {
if !tt.wantErr {
t.Errorf("SetInQuantity() error = %v, wantErr %v", err, tt.wantErr)
return
}
}
if !tt.wantErr && emit.I2PConfig.InQuantity != tt.input {
@@ -51,10 +52,11 @@ func TestSetOutQuantity(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
emit := &SAMEmit{I2PConfig: I2PConfig{}}
err := SetOutQuantity(tt.input)(emit)
if (err != nil) != tt.wantErr {
t.Errorf("SetOutQuantity() error = %v, wantErr %v", err, tt.wantErr)
return
if err != nil {
if !tt.wantErr {
t.Errorf("SetOutQuantity() error = %v, wantErr %v", err, tt.wantErr)
return
}
}
if !tt.wantErr && emit.I2PConfig.OutQuantity != tt.input {

View File

@@ -5,42 +5,58 @@ import (
"strings"
)
// SamOptionsString generates a space-separated string of all I2P configuration options.
// Used internally to construct SAM protocol messages with tunnel and session parameters.
func (e *SAMEmit) SamOptionsString() string {
optStr := strings.Join(e.I2PConfig.Print(), " ")
log.WithField("optStr", optStr).Debug("Generated option string")
return optStr
}
// Hello generates the SAM protocol HELLO command for initial handshake.
// Includes minimum and maximum supported SAM protocol versions for negotiation.
func (e *SAMEmit) Hello() string {
hello := fmt.Sprintf("HELLO VERSION MIN=%s MAX=%s \n", e.I2PConfig.MinSAM(), e.I2PConfig.MaxSAM())
log.WithField("hello", hello).Debug("Generated HELLO command")
return hello
}
// HelloBytes returns the HELLO command as a byte slice for network transmission.
// Convenience method for sending the handshake command over network connections.
func (e *SAMEmit) HelloBytes() []byte {
return []byte(e.Hello())
}
// GenerateDestination creates a SAM DEST GENERATE command for key generation.
// Uses the configured signature type to request new I2P destination keys from the router.
func (e *SAMEmit) GenerateDestination() string {
dest := fmt.Sprintf("DEST GENERATE %s \n", e.I2PConfig.SignatureType())
log.WithField("destination", dest).Debug("Generated DEST GENERATE command")
return dest
}
// GenerateDestinationBytes returns the DEST GENERATE command as bytes.
// Convenience method for network transmission of key generation requests.
func (e *SAMEmit) GenerateDestinationBytes() []byte {
return []byte(e.GenerateDestination())
}
// Lookup generates a SAM NAMING LOOKUP command for address resolution.
// Takes a human-readable name and creates a command to resolve it to an I2P destination.
func (e *SAMEmit) Lookup(name string) string {
lookup := fmt.Sprintf("NAMING LOOKUP NAME=%s \n", name)
log.WithField("lookup", lookup).Debug("Generated NAMING LOOKUP command")
return lookup
}
// LookupBytes returns the NAMING LOOKUP command as bytes for transmission.
// Convenience method for sending address resolution requests over network connections.
func (e *SAMEmit) LookupBytes(name string) []byte {
return []byte(e.Lookup(name))
}
// Create generates a SAM SESSION CREATE command for establishing new sessions.
// Combines session style, ports, ID, destination, signature type, and options into a single command.
func (e *SAMEmit) Create() string {
create := fmt.Sprintf(
// //1 2 3 4 5 6 7
@@ -57,11 +73,15 @@ func (e *SAMEmit) Create() string {
return create
}
// CreateBytes returns the SESSION CREATE command as bytes for network transmission.
// Includes debug output of the command for troubleshooting session creation issues.
func (e *SAMEmit) CreateBytes() []byte {
fmt.Println("sam command: " + e.Create())
return []byte(e.Create())
}
// Connect generates a SAM STREAM CONNECT command for establishing connections.
// Takes a destination address and creates a command to connect to that I2P destination.
func (e *SAMEmit) Connect(dest string) string {
connect := fmt.Sprintf(
"STREAM CONNECT ID=%s %s %s DESTINATION=%s \n",
@@ -74,10 +94,14 @@ func (e *SAMEmit) Connect(dest string) string {
return connect
}
// ConnectBytes returns the STREAM CONNECT command as bytes for transmission.
// Convenience method for sending connection requests over network connections.
func (e *SAMEmit) ConnectBytes(dest string) []byte {
return []byte(e.Connect(dest))
}
// Accept generates a SAM STREAM ACCEPT command for accepting incoming connections.
// Creates a command to listen for and accept connections on the configured session.
func (e *SAMEmit) Accept() string {
accept := fmt.Sprintf(
"STREAM ACCEPT ID=%s %s %s",
@@ -89,10 +113,15 @@ func (e *SAMEmit) Accept() string {
return accept
}
// AcceptBytes returns the STREAM ACCEPT command as bytes for transmission.
// Convenience method for sending accept requests over network connections.
func (e *SAMEmit) AcceptBytes() []byte {
return []byte(e.Accept())
}
// NewEmit creates a new SAMEmit instance with the specified configuration options.
// Applies functional options to configure the emitter with custom settings.
// Returns an error if any option fails to apply correctly.
func NewEmit(opts ...func(*SAMEmit) error) (*SAMEmit, error) {
var emit SAMEmit
for _, o := range opts {

View File

@@ -1,10 +1,7 @@
package common
import logger "github.com/go-i2p/go-sam-go/logger"
import (
"github.com/go-i2p/logger"
)
var log = logger.GetSAM3Logger()
func init() {
logger.InitializeSAM3Logger()
log = logger.GetSAM3Logger()
}
var log = logger.GetGoI2PLogger()

View File

@@ -1,81 +1,50 @@
package common
import (
"fmt"
"net"
"strings"
"github.com/samber/oops"
)
// Creates a new controller for the I2P routers SAM bridge.
func OldNewSAM(address string) (*SAM, error) {
log.WithField("address", address).Debug("Creating new SAM instance")
var s SAM
// TODO: clean this up by refactoring the connection setup and error handling logic
conn, err := net.Dial("tcp", address)
if err != nil {
log.WithError(err).Error("Failed to dial SAM address")
return nil, fmt.Errorf("error dialing to address '%s': %w", address, err)
}
if _, err := conn.Write(s.SAMEmit.HelloBytes()); err != nil {
log.WithError(err).Error("Failed to write hello message")
conn.Close()
return nil, fmt.Errorf("error writing to address '%s': %w", address, err)
}
buf := make([]byte, 256)
n, err := conn.Read(buf)
if err != nil {
log.WithError(err).Error("Failed to read SAM response")
conn.Close()
return nil, fmt.Errorf("error reading onto buffer: %w", err)
}
if strings.Contains(string(buf[:n]), HELLO_REPLY_OK) {
log.Debug("SAM hello successful")
s.SAMEmit.I2PConfig.SetSAMAddress(address)
s.Conn = conn
s.SAMResolver, err = NewSAMResolver(&s)
if err != nil {
log.WithError(err).Error("Failed to create SAM resolver")
return nil, fmt.Errorf("error creating resolver: %w", err)
}
return &s, nil
} else if string(buf[:n]) == HELLO_REPLY_NOVERSION {
log.Error("SAM bridge does not support SAMv3")
conn.Close()
return nil, fmt.Errorf("That SAM bridge does not support SAMv3.")
} else {
log.WithField("response", string(buf[:n])).Error("Unexpected SAM response")
conn.Close()
return nil, fmt.Errorf("%s", string(buf[:n]))
}
}
// NewSAM creates a new SAM instance by connecting to the specified address,
// performing the hello handshake, and initializing the SAM resolver.
// It returns a pointer to the SAM instance or an error if any step fails.
// This function combines connection establishment and hello handshake into a single step,
// eliminating the need for separate helper functions.
// It also initializes the SAM resolver directly after the connection is established.
// The SAM instance is ready to use for further operations like session creation or name resolution.
func NewSAM(address string) (*SAM, error) {
logger := log.WithField("address", address)
logger.Debug("Creating new SAM instance")
// Use existing helper function for connection establishment
conn, err := connectToSAM(address)
if err != nil {
return nil, err
logger.WithError(err).Error("Failed to connect to SAM bridge")
return nil, err // connectToSAM already wraps the error appropriately
}
defer func() {
if err != nil {
conn.Close()
}
}()
s := &SAM{
Conn: conn,
}
if err = sendHelloAndValidate(conn, s); err != nil {
return nil, err
// Use existing helper function for hello handshake with proper cleanup
if err := sendHelloAndValidate(conn, s); err != nil {
logger.WithError(err).Error("Failed to complete SAM handshake")
conn.Close()
return nil, err // sendHelloAndValidate already wraps the error appropriately
}
// Configure SAM instance with address
s.SAMEmit.I2PConfig.SetSAMAddress(address)
if s.SAMResolver, err = NewSAMResolver(s); err != nil {
return nil, fmt.Errorf("failed to create SAM resolver: %w", err)
// Initialize resolver
resolver, err := NewSAMResolver(s)
if err != nil {
logger.WithError(err).Error("Failed to create SAM resolver")
conn.Close()
return nil, oops.Errorf("failed to create SAM resolver: %w", err)
}
s.SAMResolver = *resolver
logger.Debug("Successfully created new SAM instance")
return s, nil
}

View File

@@ -9,6 +9,9 @@ import (
"github.com/go-i2p/i2pkeys"
)
// NewSAMResolver creates a new SAMResolver using an existing SAM instance.
// This allows sharing a single SAM connection for both session management and address resolution.
// Returns a configured resolver ready for performing I2P address lookups.
func NewSAMResolver(parent *SAM) (*SAMResolver, error) {
log.Debug("Creating new SAMResolver from existing SAM instance")
var s SAMResolver
@@ -16,11 +19,15 @@ func NewSAMResolver(parent *SAM) (*SAMResolver, error) {
return &s, nil
}
// NewFullSAMResolver creates a complete SAMResolver with its own SAM connection.
// Establishes a new connection to the specified SAM bridge address for address resolution.
// Returns a fully configured resolver or an error if connection fails.
func NewFullSAMResolver(address string) (*SAMResolver, error) {
log.WithField("address", address).Debug("Creating new full SAMResolver")
var s SAMResolver
var err error
s.SAM, err = NewSAM(address)
// var err error
sam, err := NewSAM(address)
s.SAM = sam
if err != nil {
log.WithError(err).Error("Failed to create new SAM instance")
return nil, err
@@ -33,30 +40,68 @@ func NewFullSAMResolver(address string) (*SAMResolver, error) {
func (sam *SAMResolver) Resolve(name string) (i2pkeys.I2PAddr, error) {
log.WithField("name", name).Debug("Resolving name")
if err := sam.sendLookupRequest(name); err != nil {
return i2pkeys.I2PAddr(""), err
}
response, err := sam.readLookupResponse()
if err != nil {
return i2pkeys.I2PAddr(""), err
}
scanner, err := sam.prepareLookupScanner(response)
if err != nil {
return i2pkeys.I2PAddr(""), err
}
return sam.processLookupResponse(scanner, name)
}
// sendLookupRequest sends a NAMING LOOKUP request to the SAM connection.
// It writes the lookup command and handles any connection errors.
func (sam *SAMResolver) sendLookupRequest(name string) error {
if _, err := sam.Conn.Write([]byte("NAMING LOOKUP NAME=" + name + "\r\n")); err != nil {
log.WithError(err).Error("Failed to write to SAM connection")
sam.Close()
return i2pkeys.I2PAddr(""), err
return err
}
return nil
}
// readLookupResponse reads the response from the SAM connection.
// It handles reading errors and connection cleanup on failure.
func (sam *SAMResolver) readLookupResponse() ([]byte, error) {
buf := make([]byte, 4096)
n, err := sam.Conn.Read(buf)
if err != nil {
log.WithError(err).Error("Failed to read from SAM connection")
sam.Close()
return i2pkeys.I2PAddr(""), err
return nil, err
}
if n <= 13 || !strings.HasPrefix(string(buf[:n]), "NAMING REPLY ") {
log.Error("Failed to parse SAM response")
return i2pkeys.I2PAddr(""), errors.New("Failed to parse.")
}
s := bufio.NewScanner(bytes.NewReader(buf[13:n]))
s.Split(bufio.ScanWords)
return buf[:n], nil
}
// prepareLookupScanner validates the response format and creates a scanner.
// It ensures the response has the correct "NAMING REPLY" prefix and length.
func (sam *SAMResolver) prepareLookupScanner(response []byte) (*bufio.Scanner, error) {
if len(response) <= 13 || !strings.HasPrefix(string(response), "NAMING REPLY ") {
log.Error("Failed to parse SAM response")
return nil, errors.New("failed to parse SAM response")
}
scanner := bufio.NewScanner(bytes.NewReader(response[13:]))
scanner.Split(bufio.ScanWords)
return scanner, nil
}
// processLookupResponse processes the scanner tokens and returns the resolved address.
// It handles different response types and accumulates error messages.
func (sam *SAMResolver) processLookupResponse(scanner *bufio.Scanner, name string) (i2pkeys.I2PAddr, error) {
errStr := ""
for s.Scan() {
text := s.Text()
for scanner.Scan() {
text := scanner.Text()
log.WithField("text", text).Debug("Parsing SAM response token")
// log.Println("SAM3", text)
if text == SAM_RESULT_OK {
continue
} else if text == SAM_RESULT_INVALID_KEY {

View File

@@ -1,28 +1,35 @@
package common
import (
"fmt"
"net"
"strings"
"github.com/samber/oops"
)
// connectToSAM establishes a TCP connection to the SAM bridge at the specified address.
// This is an internal helper function used during SAM instance initialization.
// Returns the established connection or an error if the connection fails.
func connectToSAM(address string) (net.Conn, error) {
conn, err := net.Dial("tcp", address)
if err != nil {
return nil, fmt.Errorf("failed to connect to SAM bridge at %s: %w", address, err)
return nil, oops.Errorf("failed to connect to SAM bridge at %s: %w", address, err)
}
return conn, nil
}
// sendHelloAndValidate performs the SAM protocol handshake and validates the response.
// This internal function sends the HELLO message and ensures the SAM bridge supports the protocol.
// Returns an error if the handshake fails or the protocol version is unsupported.
func sendHelloAndValidate(conn net.Conn, s *SAM) error {
if _, err := conn.Write(s.SAMEmit.HelloBytes()); err != nil {
return fmt.Errorf("failed to send hello message: %w", err)
return oops.Errorf("failed to send hello message: %w", err)
}
buf := make([]byte, 256)
n, err := conn.Read(buf)
if err != nil {
return fmt.Errorf("failed to read SAM response: %w", err)
return oops.Errorf("failed to read SAM response: %w", err)
}
response := string(buf[:n])
@@ -31,8 +38,8 @@ func sendHelloAndValidate(conn net.Conn, s *SAM) error {
log.Debug("SAM hello successful")
return nil
case response == HELLO_REPLY_NOVERSION:
return fmt.Errorf("SAM bridge does not support SAMv3")
return oops.Errorf("SAM bridge does not support SAMv3")
default:
return fmt.Errorf("unexpected SAM response: %s", response)
return oops.Errorf("unexpected SAM response: %s", response)
}
}

157
common/session.go Normal file
View File

@@ -0,0 +1,157 @@
package common
import (
"strings"
"github.com/go-i2p/i2pkeys"
"github.com/samber/oops"
"github.com/sirupsen/logrus"
)
// Creates a new session with the style of either "STREAM", "DATAGRAM" or "RAW",
// for a new I2P tunnel with name id, using the cypher keys specified, with the
// I2CP/streaminglib-options as specified. Extra arguments can be specified by
// setting extra to something else than []string{}.
// This sam3 instance is now a session
func (sam SAM) NewGenericSession(style, id string, keys i2pkeys.I2PKeys, extras []string) (Session, error) {
log.WithFields(logrus.Fields{"style": style, "id": id}).Debug("Creating new generic session")
return sam.NewGenericSessionWithSignature(style, id, keys, SIG_EdDSA_SHA512_Ed25519, extras)
}
// Creates a new session with the style of either "STREAM", "DATAGRAM" or "RAW",
// for a new I2P tunnel with name id, using the cypher keys specified, with the
// I2CP/streaminglib-options as specified. Extra arguments can be specified by
// setting extra to something else than []string{}.
// This sam3 instance is now a session
func (sam SAM) NewGenericSessionWithSignature(style, id string, keys i2pkeys.I2PKeys, sigType string, extras []string) (Session, error) {
log.WithFields(logrus.Fields{"style": style, "id": id, "sigType": sigType}).Debug("Creating new generic session with signature")
return sam.NewGenericSessionWithSignatureAndPorts(style, id, "0", "0", keys, sigType, extras)
}
// Creates a new session with the style of either "STREAM", "DATAGRAM" or "RAW",
// for a new I2P tunnel with name id, using the cypher keys specified, with the
// I2CP/streaminglib-options as specified. Extra arguments can be specified by
// setting extra to something else than []string{}.
// This sam3 instance is now a session
func (sam SAM) NewGenericSessionWithSignatureAndPorts(style, id, from, to string, keys i2pkeys.I2PKeys, sigType string, extras []string) (Session, error) {
log.WithFields(logrus.Fields{"style": style, "id": id, "from": from, "to": to, "sigType": sigType}).Debug("Creating new generic session with signature and ports")
if err := sam.configureSessionParameters(style, id, from, to, keys, sigType); err != nil {
return nil, err
}
message, err := sam.buildSessionCreateMessage(extras)
if err != nil {
return nil, err
}
if err := sam.transmitSessionMessage(message); err != nil {
return nil, err
}
response, err := sam.readSessionResponse()
if err != nil {
return nil, err
}
return sam.parseSessionResponse(response, id, keys)
}
// configureSessionParameters sets up the SAMEmit configuration with session parameters.
func (sam *SAM) configureSessionParameters(style, id, from, to string, keys i2pkeys.I2PKeys, sigType string) error {
sam.SAMEmit.I2PConfig.Style = style
sam.SAMEmit.I2PConfig.TunName = id
sam.SAMEmit.I2PConfig.DestinationKeys = &keys
sam.SAMEmit.I2PConfig.SigType = sigType
sam.SAMEmit.I2PConfig.Fromport = from
sam.SAMEmit.I2PConfig.Toport = to
return nil
}
// buildSessionCreateMessage constructs the SESSION CREATE message with optional extras.
func (sam *SAM) buildSessionCreateMessage(extras []string) ([]byte, error) {
baseMsg := strings.TrimSuffix(sam.SAMEmit.Create(), " \n")
extraStr := strings.Join(extras, " ")
if extraStr != "" {
baseMsg += " " + extraStr
}
message := []byte(baseMsg + "\n")
log.WithField("message", string(message)).Debug("Sending SESSION CREATE message " + string(message))
return message, nil
}
// transmitSessionMessage sends the SESSION CREATE message to the SAM connection.
func (sam *SAM) transmitSessionMessage(message []byte) error {
conn := sam.Conn
n, err := conn.Write(message)
if err != nil {
log.WithError(err).Error("Failed to write to SAM connection")
conn.Close()
return oops.Errorf("writing to connection failed: %w", err)
}
if n != len(message) {
log.WithFields(logrus.Fields{
"written": n,
"total": len(message),
}).Error("Incomplete write to SAM connection")
conn.Close()
return oops.Errorf("incomplete write to connection: wrote %d bytes, expected %d bytes", n, len(message))
}
return nil
}
// readSessionResponse reads the response from the SAM connection.
func (sam *SAM) readSessionResponse() (string, error) {
buf := make([]byte, 4096)
n, err := sam.Conn.Read(buf)
if err != nil {
log.WithError(err).Error("Failed to read SAM response")
sam.Conn.Close()
return "", oops.Errorf("reading from connection failed: %w", err)
}
response := string(buf[:n])
log.WithField("response", response).Debug("Received SAM response")
return response, nil
}
// parseSessionResponse parses the SAM response and returns the appropriate session or error.
func (sam *SAM) parseSessionResponse(response, id string, keys i2pkeys.I2PKeys) (Session, error) {
conn := sam.Conn
if strings.HasPrefix(response, SESSION_OK) {
if keys.String() != response[len(SESSION_OK):len(response)-1] {
log.Error("SAM created a tunnel with different keys than requested")
conn.Close()
return nil, oops.Errorf("SAMv3 created a tunnel with keys other than the ones we asked it for")
}
log.Debug("Successfully created new session")
return &BaseSession{
id: id,
conn: conn,
keys: keys,
SAM: *sam,
}, nil
} else if response == SESSION_DUPLICATE_ID {
log.Error("Duplicate tunnel name")
conn.Close()
return nil, oops.Errorf("Duplicate tunnel name")
} else if response == SESSION_DUPLICATE_DEST {
log.Error("Duplicate destination")
conn.Close()
return nil, oops.Errorf("Duplicate destination")
} else if response == SESSION_INVALID_KEY {
log.Error("Invalid key for SAM session")
conn.Close()
return nil, oops.Errorf("Invalid key - SAM session")
} else if strings.HasPrefix(response, SESSION_I2P_ERROR) {
log.WithField("error", response[len(SESSION_I2P_ERROR):]).Error("I2P error")
conn.Close()
return nil, oops.Errorf("I2P error " + response[len(SESSION_I2P_ERROR):])
} else {
log.WithField("reply", response).Error("Unable to parse SAMv3 reply")
conn.Close()
return nil, oops.Errorf("Unable to parse SAMv3 reply: " + response)
}
}

402
common/session_test.go Normal file
View File

@@ -0,0 +1,402 @@
package common
import (
"strings"
"testing"
"github.com/go-i2p/i2pkeys"
)
func TestNewGenericSession(t *testing.T) {
// Create SAM connection
sam, err := NewSAM("127.0.0.1:7656")
if err != nil {
t.Skipf("Failed to connect to SAM bridge: %v", err)
}
defer sam.Close()
// Generate keys for testing
_, err = sam.NewKeys()
if err != nil {
t.Fatalf("Failed to generate keys: %v", err)
}
tests := []struct {
name string
style string
id string
extras []string
wantErr bool
}{
{
name: "stream session",
style: SESSION_STYLE_STREAM,
id: "test-stream-session",
extras: []string{},
wantErr: false,
},
{
name: "datagram session",
style: SESSION_STYLE_DATAGRAM,
id: "test-datagram-session",
extras: []string{},
wantErr: false,
},
{
name: "raw session",
style: SESSION_STYLE_RAW,
id: "test-raw-session",
extras: []string{},
wantErr: false,
},
{
name: "invalid style",
style: "INVALID",
id: "test-invalid-session",
extras: []string{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create new SAM connection for each test
testSam, err := NewSAM("127.0.0.1:7656")
if err != nil {
t.Skipf("Failed to connect to SAM bridge: %v", err)
}
defer testSam.Close()
// Generate unique keys for each test
testKeys, err := testSam.NewKeys()
if err != nil {
t.Fatalf("Failed to generate keys: %v", err)
}
session, err := testSam.NewGenericSession(tt.style, tt.id, testKeys, tt.extras)
if (err != nil) != tt.wantErr {
t.Errorf("NewGenericSession() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
if session == nil {
t.Error("NewGenericSession() returned nil session")
return
}
// Verify session properties
if session.ID() != tt.id {
t.Errorf("Session ID = %v, want %v", session.ID(), tt.id)
}
if session.Keys().String() != testKeys.String() {
t.Error("Session keys don't match expected keys")
}
// Clean up session
session.Close()
}
})
}
}
func TestNewGenericSessionWithSignature(t *testing.T) {
// Create SAM connection
sam, err := NewSAM("127.0.0.1:7656")
if err != nil {
t.Skipf("Failed to connect to SAM bridge: %v", err)
}
defer sam.Close()
tests := []struct {
name string
style string
id string
sigType string
extras []string
wantErr bool
}{
{
name: "ed25519 signature",
style: SESSION_STYLE_STREAM,
id: "test-ed25519-session",
sigType: SIG_EdDSA_SHA512_Ed25519,
extras: []string{},
wantErr: false,
},
{
name: "dsa signature",
style: SESSION_STYLE_STREAM,
id: "test-dsa-session",
sigType: SIG_DSA_SHA1,
extras: []string{},
wantErr: false,
},
{
name: "ecdsa p256 signature",
style: SESSION_STYLE_STREAM,
id: "test-ecdsa-p256-session",
sigType: SIG_ECDSA_SHA256_P256,
extras: []string{},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create new SAM connection for each test
testSam, err := NewSAM("127.0.0.1:7656")
if err != nil {
t.Skipf("Failed to connect to SAM bridge: %v", err)
}
defer testSam.Close()
// Generate keys with specific signature type
var testKeys i2pkeys.I2PKeys
if strings.Contains(tt.sigType, "EdDSA") {
testKeys, err = testSam.NewKeys("EdDSA_SHA512_Ed25519")
} else if strings.Contains(tt.sigType, "DSA") {
testKeys, err = testSam.NewKeys("DSA_SHA1")
} else if strings.Contains(tt.sigType, "ECDSA_SHA256_P256") {
testKeys, err = testSam.NewKeys("ECDSA_SHA256_P256")
} else {
testKeys, err = testSam.NewKeys()
}
if err != nil {
t.Fatalf("Failed to generate keys: %v", err)
}
session, err := testSam.NewGenericSessionWithSignature(tt.style, tt.id, testKeys, tt.sigType, tt.extras)
if (err != nil) != tt.wantErr {
t.Errorf("NewGenericSessionWithSignature() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
if session == nil {
t.Error("NewGenericSessionWithSignature() returned nil session")
return
}
// Verify session properties
if session.ID() != tt.id {
t.Errorf("Session ID = %v, want %v", session.ID(), tt.id)
}
// Clean up session
session.Close()
}
})
}
}
func TestNewGenericSessionWithSignatureAndPorts(t *testing.T) {
// Create SAM connection
sam, err := NewSAM("127.0.0.1:7656")
if err != nil {
t.Skipf("Failed to connect to SAM bridge: %v", err)
}
defer sam.Close()
tests := getSessionWithPortsTestCases()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
testSam, testKeys := setupSessionTest(t)
defer testSam.Close()
session, err := testSam.NewGenericSessionWithSignatureAndPorts(tt.style, tt.id, tt.from, tt.to, testKeys, tt.sigType, tt.extras)
if (err != nil) != tt.wantErr {
t.Errorf("NewGenericSessionWithSignatureAndPorts() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
validateSessionWithPorts(t, session, tt, testKeys)
session.Close()
}
})
}
}
// getSessionWithPortsTestCases returns test cases for session creation with ports and signature.
func getSessionWithPortsTestCases() []struct {
name string
style string
id string
from string
to string
sigType string
extras []string
wantErr bool
} {
return []struct {
name string
style string
id string
from string
to string
sigType string
extras []string
wantErr bool
}{
{
name: "with ports",
style: SESSION_STYLE_STREAM,
id: "test-ports-session",
from: "8080",
to: "9090",
sigType: SIG_EdDSA_SHA512_Ed25519,
extras: []string{},
wantErr: false,
},
{
name: "default ports",
style: SESSION_STYLE_STREAM,
id: "test-default-ports-session",
from: "0",
to: "0",
sigType: SIG_EdDSA_SHA512_Ed25519,
extras: []string{},
wantErr: false,
},
{
name: "with extras",
style: SESSION_STYLE_STREAM,
id: "test-extras-session",
from: "0",
to: "0",
sigType: SIG_EdDSA_SHA512_Ed25519,
extras: []string{"inbound.length=2", "outbound.length=2"},
wantErr: false,
},
}
}
// setupSessionTest creates a new SAM connection and generates test keys.
func setupSessionTest(t *testing.T) (*SAM, i2pkeys.I2PKeys) {
testSam, err := NewSAM("127.0.0.1:7656")
if err != nil {
t.Skipf("Failed to connect to SAM bridge: %v", err)
}
testKeys, err := testSam.NewKeys()
if err != nil {
t.Fatalf("Failed to generate keys: %v", err)
}
return testSam, testKeys
}
// validateSessionWithPorts verifies that a session was created correctly with the expected properties.
func validateSessionWithPorts(t *testing.T, session Session, testCase struct {
name string
style string
id string
from string
to string
sigType string
extras []string
wantErr bool
}, testKeys i2pkeys.I2PKeys) {
if session == nil {
t.Error("NewGenericSessionWithSignatureAndPorts() returned nil session")
return
}
validateSessionID(t, session, testCase.id)
validateSessionKeys(t, session, testKeys)
validateSessionPorts(t, session, testCase.from, testCase.to)
}
// validateSessionID checks if the session ID matches the expected value.
func validateSessionID(t *testing.T, session Session, expectedID string) {
if session.ID() != expectedID {
t.Errorf("Session ID = %v, want %v", session.ID(), expectedID)
}
}
// validateSessionKeys checks if the session keys match the expected keys.
func validateSessionKeys(t *testing.T, session Session, expectedKeys i2pkeys.I2PKeys) {
if session.Keys().String() != expectedKeys.String() {
t.Error("Session keys don't match expected keys")
}
}
// validateSessionPorts verifies that the session ports are set correctly.
func validateSessionPorts(t *testing.T, session Session, expectedFrom, expectedTo string) {
baseSession, ok := session.(*BaseSession)
if !ok {
return
}
if expectedFrom != "0" && baseSession.From() != expectedFrom {
t.Errorf("Session FromPort = %v, want %v", baseSession.From(), expectedFrom)
}
if expectedTo != "0" && baseSession.To() != expectedTo {
t.Errorf("Session ToPort = %v, want %v", baseSession.To(), expectedTo)
}
}
func TestSessionCreationErrors(t *testing.T) {
// Create SAM connection
sam, err := NewSAM("127.0.0.1:7656")
if err != nil {
t.Skipf("Failed to connect to SAM bridge: %v", err)
}
defer sam.Close()
// Generate keys for testing
keys, err := sam.NewKeys()
if err != nil {
t.Fatalf("Failed to generate keys: %v", err)
}
t.Run("duplicate session ID", func(t *testing.T) {
// Create first session
testSam1, err := NewSAM("127.0.0.1:7656")
if err != nil {
t.Skipf("Failed to connect to SAM bridge: %v", err)
}
defer testSam1.Close()
session1, err := testSam1.NewGenericSession(SESSION_STYLE_STREAM, "duplicate-id", keys, []string{})
if err != nil {
t.Fatalf("Failed to create first session: %v", err)
}
defer session1.Close()
// Try to create second session with same ID
testSam2, err := NewSAM("127.0.0.1:7656")
if err != nil {
t.Skipf("Failed to connect to SAM bridge: %v", err)
}
defer testSam2.Close()
_, err = testSam2.NewGenericSession(SESSION_STYLE_STREAM, "duplicate-id", keys, []string{})
if err == nil {
t.Error("Expected error for duplicate session ID")
}
if !strings.Contains(err.Error(), "Duplicate") {
t.Errorf("Expected duplicate error, got: %v", err)
}
})
t.Run("invalid keys", func(t *testing.T) {
testSam, err := NewSAM("127.0.0.1:7656")
if err != nil {
t.Skipf("Failed to connect to SAM bridge: %v", err)
}
defer testSam.Close()
// Create invalid keys
invalidKeys := i2pkeys.NewKeys(i2pkeys.I2PAddr("invalid"), "invalid")
_, err = testSam.NewGenericSession(SESSION_STYLE_STREAM, "invalid-keys-session", invalidKeys, []string{})
if err == nil {
t.Error("Expected error for invalid keys")
}
})
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net"
"sync"
"time"
"github.com/go-i2p/i2pkeys"
@@ -57,6 +58,8 @@ type I2PConfig struct {
AccessList []string
}
// SAMEmit handles SAM protocol message generation and configuration.
// It embeds I2PConfig to provide access to all tunnel and session configuration options.
type SAMEmit struct {
I2PConfig
}
@@ -64,7 +67,7 @@ type SAMEmit struct {
// Used for controlling I2Ps SAMv3.
type SAM struct {
SAMEmit
*SAMResolver
SAMResolver
net.Conn
// Timeout for SAM connections
@@ -73,6 +76,8 @@ type SAM struct {
Context context.Context
}
// SAMResolver provides I2P address resolution services through SAM protocol.
// It maintains a connection to the SAM bridge for performing address lookups.
type SAMResolver struct {
*SAM
}
@@ -87,3 +92,117 @@ func (opts Options) AsList() (ls []string) {
}
return
}
// Session represents a generic I2P session interface for different connection types.
// It extends net.Conn with I2P-specific functionality for session identification and key management.
// All session implementations (stream, datagram, raw) must implement this interface.
type Session interface {
net.Conn
ID() string
Keys() i2pkeys.I2PKeys
Close() error
// Add other session methods as needed
}
// BaseSession provides common functionality for all I2P session types.
// It implements the Session interface and serves as the foundation for stream, datagram, and raw sessions.
// Contains connection management, key handling, and basic I/O operations.
type BaseSession struct {
id string
conn net.Conn
keys i2pkeys.I2PKeys
SAM SAM
mu sync.RWMutex
}
// Conn returns the underlying network connection for the session.
// This provides access to the raw connection for advanced operations.
func (bs *BaseSession) Conn() net.Conn {
bs.mu.RLock()
defer bs.mu.RUnlock()
return bs.conn
}
// ID returns the unique session identifier used by the SAM bridge.
// This identifier is used to distinguish between multiple sessions on the same connection.
func (bs *BaseSession) ID() string { return bs.id }
// Keys returns the I2P cryptographic keys associated with this session.
// These keys define the session's I2P destination and identity.
func (bs *BaseSession) Keys() i2pkeys.I2PKeys { return bs.keys }
// Read reads data from the session connection into the provided buffer.
// Implements the io.Reader interface for standard Go I/O operations.
func (bs *BaseSession) Read(b []byte) (int, error) {
bs.mu.RLock()
defer bs.mu.RUnlock()
return bs.conn.Read(b)
}
// Write writes data from the buffer to the session connection.
// Implements the io.Writer interface for standard Go I/O operations.
func (bs *BaseSession) Write(b []byte) (int, error) {
bs.mu.RLock()
defer bs.mu.RUnlock()
return bs.conn.Write(b)
}
// Close closes the session connection and releases associated resources.
// Implements the io.Closer interface for proper resource cleanup.
func (bs *BaseSession) Close() error {
bs.mu.Lock()
defer bs.mu.Unlock()
return bs.conn.Close()
}
// LocalAddr returns the local network address of the session connection.
// Implements the net.Conn interface for network address information.
func (bs *BaseSession) LocalAddr() net.Addr {
bs.mu.RLock()
defer bs.mu.RUnlock()
return bs.conn.LocalAddr()
}
// RemoteAddr returns the remote network address of the session connection.
// Implements the net.Conn interface for network address information.
func (bs *BaseSession) RemoteAddr() net.Addr {
bs.mu.RLock()
defer bs.mu.RUnlock()
return bs.conn.RemoteAddr()
}
// SetDeadline sets read and write deadlines for the session connection.
// Implements the net.Conn interface for timeout control.
func (bs *BaseSession) SetDeadline(t time.Time) error {
bs.mu.RLock()
defer bs.mu.RUnlock()
return bs.conn.SetDeadline(t)
}
// SetReadDeadline sets the read deadline for the session connection.
// Implements the net.Conn interface for read timeout control.
func (bs *BaseSession) SetReadDeadline(t time.Time) error {
bs.mu.RLock()
defer bs.mu.RUnlock()
return bs.conn.SetReadDeadline(t)
}
// SetWriteDeadline sets the write deadline for the session connection.
// Implements the net.Conn interface for write timeout control.
func (bs *BaseSession) SetWriteDeadline(t time.Time) error {
bs.mu.RLock()
defer bs.mu.RUnlock()
return bs.conn.SetWriteDeadline(t)
}
// From returns the configured source port for the session.
// Used in port-based session configurations for service identification.
func (bs *BaseSession) From() string {
return bs.SAM.SAMEmit.I2PConfig.Fromport
}
// To returns the configured destination port for the session.
// Used in port-based session configurations for service identification.
func (bs *BaseSession) To() string {
return bs.SAM.SAMEmit.I2PConfig.Toport
}

View File

@@ -1,15 +1,22 @@
package common
import (
cryptoRand "crypto/rand"
"encoding/binary"
"fmt"
"math/rand"
"net"
"strconv"
"strings"
"time"
"github.com/samber/oops"
"github.com/sirupsen/logrus"
)
// IgnorePortError filters out "missing port in address" errors for convenience.
// This function is used when parsing addresses that may not include port numbers.
// Returns nil if the error is about missing port, otherwise returns the original error.
func IgnorePortError(err error) error {
if err == nil {
return nil
@@ -21,6 +28,9 @@ func IgnorePortError(err error) error {
return err
}
// SplitHostPort separates host and port from a combined address string.
// Unlike net.SplitHostPort, this function handles addresses without ports gracefully.
// Returns host, port as strings, and error. Port defaults to "0" if not specified.
func SplitHostPort(hostport string) (string, string, error) {
host, port, err := net.SplitHostPort(hostport)
if err != nil {
@@ -37,6 +47,9 @@ func SplitHostPort(hostport string) (string, string, error) {
return host, port, nil
}
// ExtractPairString extracts the value from a key=value pair in a space-separated string.
// Searches for the specified key prefix and returns the associated value.
// Returns empty string if the key is not found or has no value.
func ExtractPairString(input, value string) string {
log.WithFields(logrus.Fields{"input": input, "value": value}).Debug("ExtractPairString called")
parts := strings.Split(input, " ")
@@ -54,6 +67,9 @@ func ExtractPairString(input, value string) string {
return ""
}
// ExtractPairInt extracts an integer value from a key=value pair in a space-separated string.
// Uses ExtractPairString internally and converts the result to integer.
// Returns 0 if the key is not found or the value cannot be converted to integer.
func ExtractPairInt(input, value string) int {
rv, err := strconv.Atoi(ExtractPairString(input, value))
if err != nil {
@@ -64,32 +80,153 @@ func ExtractPairInt(input, value string) int {
return rv
}
// ExtractDest extracts the destination address from a SAM protocol response.
// Takes the first space-separated token from the input string as the destination.
// Used for parsing SAM session creation responses and connection messages.
func ExtractDest(input string) string {
log.WithField("input", input).Debug("ExtractDest called")
dest := strings.Split(input, " ")[0]
log.WithField("dest", dest).Debug("Destination extracted")
return strings.Split(input, " ")[0]
return dest
}
var (
randSource = rand.NewSource(time.Now().UnixNano())
randGen = rand.New(randSource)
)
// RandPort generates a random available port number for local testing.
// Attempts to find a port that is available for both TCP and UDP connections.
// Returns the port as a string or an error if no available port is found after 30 attempts.
func RandPort() (portNumber string, err error) {
maxAttempts := 30
for range maxAttempts {
port, err := generateRandomPort()
if err != nil {
return "", err
}
func RandPort() string {
for {
p := randGen.Intn(55534) + 10000
port := strconv.Itoa(p)
if l, e := net.Listen("tcp", net.JoinHostPort("localhost", port)); e != nil {
continue
} else {
defer l.Close()
if l, e := net.Listen("udp", net.JoinHostPort("localhost", port)); e != nil {
continue
} else {
defer l.Close()
return strconv.Itoa(l.Addr().(*net.UDPAddr).Port)
}
if isPortAvailable(port) {
return port, nil
}
}
return "", oops.Errorf("unable to find a pair of available tcp and udp ports in %v attempts", maxAttempts)
}
// generateRandomPort creates a random port number in the range 10000-65534.
// Uses crypto/rand for thread-safe random generation.
func generateRandomPort() (string, error) {
var buf [4]byte
if _, err := cryptoRand.Read(buf[:]); err != nil {
return "", oops.Wrapf(err, "failed to generate random bytes")
}
// Convert to uint32 and scale to port range (10000-65534)
p := int(binary.BigEndian.Uint32(buf[:]))%55534 + 10000
return strconv.Itoa(p), nil
}
// isPortAvailable checks if a port is available for both TCP and UDP connections.
// Returns true if the port can be bound on both protocols, false otherwise.
func isPortAvailable(port string) bool {
return isTCPPortAvailable(port) && isUDPPortAvailable(port)
}
// isTCPPortAvailable checks if a TCP port is available for binding.
// Returns true if the port can be bound, false otherwise.
func isTCPPortAvailable(port string) bool {
l, err := net.Listen("tcp", net.JoinHostPort("localhost", port))
if err != nil {
return false
}
defer l.Close()
return true
}
// isUDPPortAvailable checks if a UDP port is available for binding.
// Returns true if the port can be bound, false otherwise.
func isUDPPortAvailable(port string) bool {
l, err := net.Listen("udp", net.JoinHostPort("localhost", port))
if err != nil {
return false
}
defer l.Close()
return true
}
// generateRandomTunnelName creates a random 12-character tunnel name using lowercase letters.
// This function is deterministic for testing when a fixed seed is used.
func (f *I2PConfig) generateRandomTunnelName() string {
const (
nameLength = 12
letters = "abcdefghijklmnopqrstuvwxyz"
)
generator := rand.New(rand.NewSource(time.Now().UnixNano()))
name := make([]byte, nameLength)
for i := range name {
name[i] = letters[generator.Intn(len(letters))]
}
return string(name)
}
// validateEncryptionTypes checks that all comma-separated values are valid integers
func (f *I2PConfig) validateEncryptionTypes(encTypes string) error {
for _, s := range strings.Split(encTypes, ",") {
trimmed := strings.TrimSpace(s)
if trimmed == "" {
return fmt.Errorf("empty encryption type")
}
if _, err := strconv.Atoi(trimmed); err != nil {
return fmt.Errorf("invalid encryption type '%s': %w", trimmed, err)
}
}
return nil
}
// formatLeaseSetEncryptionType creates the formatted configuration string
func (f *I2PConfig) formatLeaseSetEncryptionType(encType string) string {
log.WithField("leaseSetEncType", encType).Debug("Lease set encryption type set")
return fmt.Sprintf("i2cp.leaseSetEncType=%s", encType)
}
// collectTunnelSettings returns all tunnel-related configuration strings
func (f *I2PConfig) collectTunnelSettings() []string {
return []string{
f.InboundLength(),
f.OutboundLength(),
f.InboundLengthVariance(),
f.OutboundLengthVariance(),
f.InboundBackupQuantity(),
f.OutboundBackupQuantity(),
f.InboundQuantity(),
f.OutboundQuantity(),
}
}
// collectConnectionSettings returns all connection behavior configuration strings
func (f *I2PConfig) collectConnectionSettings() []string {
return []string{
f.UsingCompression(),
f.DoZero(), // Zero hop settings
f.Reduce(), // Reduce idle settings
f.Close(), // Close idle settings
f.Reliability(), // Message reliability
}
}
// collectLeaseSetSettings returns all lease set configuration strings
func (f *I2PConfig) collectLeaseSetSettings() []string {
lsk, lspk, lspsk := f.LeaseSetSettings()
return []string{
f.EncryptLease(), // Lease encryption
lsk, lspk, lspsk, // Lease set keys
f.LeaseSetEncryptionType(), // Lease set encryption type
}
}
// collectAccessSettings returns all access control configuration strings
func (f *I2PConfig) collectAccessSettings() []string {
return []string{
f.Accesslisttype(), // Access list type
f.Accesslist(), // Access list
}
}

View File

@@ -1,63 +0,0 @@
// package sam3 wraps the original sam3 API from github.com/go-i2p/sam3
package sam3
import (
"fmt"
"github.com/go-i2p/go-sam-go/common"
)
// I2PConfig is a struct which manages I2P configuration options
type I2PConfig struct {
common.I2PConfig
}
func NewConfig(opts ...func(*I2PConfig) error) (*I2PConfig, error) {
var config I2PConfig
config.SamHost = "127.0.0.1"
config.SamPort = 7656
config.SamMin = "3.0"
config.SamMax = "3.2"
config.TunName = ""
config.TunType = "server"
config.Style = "STREAM"
config.InLength = 3
config.OutLength = 3
config.InQuantity = 2
config.OutQuantity = 2
config.InVariance = 1
config.OutVariance = 1
config.InBackupQuantity = 3
config.OutBackupQuantity = 3
config.InAllowZeroHop = false
config.OutAllowZeroHop = false
config.EncryptLeaseSet = false
config.LeaseSetKey = ""
config.LeaseSetPrivateKey = ""
config.LeaseSetPrivateSigningKey = ""
config.FastRecieve = false
config.UseCompression = true
config.ReduceIdle = false
config.ReduceIdleTime = 15
config.ReduceIdleQuantity = 4
config.CloseIdle = false
config.CloseIdleTime = 300000
config.MessageReliability = "none"
for _, o := range opts {
if err := o(&config); err != nil {
return nil, err
}
}
return &config, nil
}
// options map
type Options map[string]string
// obtain sam options as list of strings
func (opts Options) AsList() (ls []string) {
for k, v := range opts {
ls = append(ls, fmt.Sprintf("%s=%s", k, v))
}
return
}

View File

@@ -1,14 +0,0 @@
// package sam3 wraps the original sam3 API from github.com/go-i2p/sam3
package sam3
import (
"github.com/go-i2p/go-sam-go/datagram"
)
// The DatagramSession implements net.PacketConn. It works almost like ordinary
// UDP, except that datagrams may be at most 31kB large. These datagrams are
// also end-to-end encrypted, signed and includes replay-protection. And they
// are also built to be surveillance-resistant (yey!).
type DatagramSession struct {
datagram.DatagramSession
}

353
datagram/DOC.md Normal file
View File

@@ -0,0 +1,353 @@
# datagram
--
import "github.com/go-i2p/go-sam-go/datagram"
## Usage
#### type Datagram
```go
type Datagram struct {
Data []byte
Source i2pkeys.I2PAddr
Local i2pkeys.I2PAddr
}
```
Datagram represents an I2P datagram message
#### type DatagramAddr
```go
type DatagramAddr struct {
}
```
DatagramAddr implements net.Addr for I2P datagram addresses
#### func (*DatagramAddr) Network
```go
func (a *DatagramAddr) Network() string
```
Network returns the network type
#### func (*DatagramAddr) String
```go
func (a *DatagramAddr) String() string
```
String returns the string representation of the address
#### type DatagramConn
```go
type DatagramConn struct {
}
```
DatagramConn implements net.PacketConn for I2P datagrams
#### func (*DatagramConn) Close
```go
func (c *DatagramConn) Close() error
```
Close closes the datagram connection
#### func (*DatagramConn) LocalAddr
```go
func (c *DatagramConn) LocalAddr() net.Addr
```
LocalAddr returns the local address
#### func (*DatagramConn) Read
```go
func (c *DatagramConn) Read(b []byte) (n int, err error)
```
Read implements net.Conn by wrapping ReadFrom. 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. Note: This is not a typical use case for
datagrams, as they are connectionless. However, for compatibility with net.Conn,
we implement it this way.
#### func (*DatagramConn) ReadFrom
```go
func (c *DatagramConn) ReadFrom(p []byte) (n int, addr net.Addr, err error)
```
ReadFrom reads a datagram from the connection
#### func (*DatagramConn) RemoteAddr
```go
func (c *DatagramConn) RemoteAddr() net.Addr
```
RemoteAddr returns the remote address of the connection. For datagram
connections, this returns nil as there is no single remote address.
#### func (*DatagramConn) SetDeadline
```go
func (c *DatagramConn) SetDeadline(t time.Time) error
```
SetDeadline sets the read and write deadlines
#### func (*DatagramConn) SetReadDeadline
```go
func (c *DatagramConn) SetReadDeadline(t time.Time) error
```
SetReadDeadline sets the deadline for future ReadFrom calls
#### func (*DatagramConn) SetWriteDeadline
```go
func (c *DatagramConn) SetWriteDeadline(t time.Time) error
```
SetWriteDeadline sets the deadline for future WriteTo calls
#### func (*DatagramConn) Write
```go
func (c *DatagramConn) Write(b []byte) (n int, err error)
```
Write implements net.Conn by wrapping WriteTo. It writes data to the remote
address and returns the number of bytes written. It uses the remote address set
by the last Read operation. If no remote address is set, it returns an error.
Note: This is not a typical use case for datagrams, as they are connectionless.
However, for compatibility with net.Conn, we implement it this way.
#### func (*DatagramConn) WriteTo
```go
func (c *DatagramConn) WriteTo(p []byte, addr net.Addr) (n int, err error)
```
WriteTo writes a datagram to the specified address
#### type DatagramListener
```go
type DatagramListener struct {
}
```
DatagramListener implements net.DatagramListener for I2P datagram connections
#### func (*DatagramListener) Accept
```go
func (l *DatagramListener) Accept() (net.Conn, error)
```
Accept waits for and returns the next packet connection to the listener
#### func (*DatagramListener) Addr
```go
func (l *DatagramListener) Addr() net.Addr
```
Addr returns the listener's network address
#### func (*DatagramListener) Close
```go
func (l *DatagramListener) Close() error
```
Close closes the packet listener
#### type DatagramReader
```go
type DatagramReader struct {
}
```
DatagramReader handles incoming datagram reception
#### func (*DatagramReader) Close
```go
func (r *DatagramReader) Close() error
```
#### func (*DatagramReader) ReceiveDatagram
```go
func (r *DatagramReader) ReceiveDatagram() (*Datagram, error)
```
ReceiveDatagram receives a datagram from any source
#### type DatagramSession
```go
type DatagramSession struct {
*common.BaseSession
}
```
DatagramSession represents a datagram session that can send and receive
datagrams
#### func NewDatagramSession
```go
func NewDatagramSession(sam *common.SAM, id string, keys i2pkeys.I2PKeys, options []string) (*DatagramSession, error)
```
NewDatagramSession creates a new datagram session
#### func (*DatagramSession) Addr
```go
func (s *DatagramSession) Addr() i2pkeys.I2PAddr
```
Addr returns the I2P address of this session
#### func (*DatagramSession) Close
```go
func (s *DatagramSession) Close() error
```
Close closes the datagram session and all associated resources
#### func (*DatagramSession) Dial
```go
func (ds *DatagramSession) Dial(destination string) (net.PacketConn, error)
```
Dial establishes a datagram connection to the specified destination
#### func (*DatagramSession) DialContext
```go
func (ds *DatagramSession) DialContext(ctx context.Context, destination string) (net.PacketConn, error)
```
DialContext establishes a datagram connection with context support
#### func (*DatagramSession) DialI2P
```go
func (ds *DatagramSession) DialI2P(addr i2pkeys.I2PAddr) (net.PacketConn, error)
```
DialI2P establishes a datagram connection to an I2P address
#### func (*DatagramSession) DialI2PContext
```go
func (ds *DatagramSession) DialI2PContext(ctx context.Context, addr i2pkeys.I2PAddr) (net.PacketConn, error)
```
DialI2PContext establishes a datagram connection to an I2P address with context
support
#### func (*DatagramSession) DialI2PTimeout
```go
func (ds *DatagramSession) DialI2PTimeout(addr i2pkeys.I2PAddr, timeout time.Duration) (net.PacketConn, error)
```
DialI2PTimeout establishes a datagram connection to an I2P address with timeout
#### func (*DatagramSession) DialTimeout
```go
func (ds *DatagramSession) DialTimeout(destination string, timeout time.Duration) (net.PacketConn, error)
```
DialTimeout establishes a datagram connection with a timeout
#### func (*DatagramSession) Listen
```go
func (s *DatagramSession) Listen() (*DatagramListener, error)
```
#### func (*DatagramSession) NewReader
```go
func (s *DatagramSession) NewReader() *DatagramReader
```
NewReader creates a DatagramReader for receiving datagrams
#### func (*DatagramSession) NewWriter
```go
func (s *DatagramSession) NewWriter() *DatagramWriter
```
NewWriter creates a DatagramWriter for sending datagrams
#### func (*DatagramSession) PacketConn
```go
func (s *DatagramSession) PacketConn() net.PacketConn
```
PacketConn returns a net.PacketConn interface for this session
#### func (*DatagramSession) ReceiveDatagram
```go
func (s *DatagramSession) ReceiveDatagram() (*Datagram, error)
```
ReceiveDatagram receives a datagram from any source
#### func (*DatagramSession) SendDatagram
```go
func (s *DatagramSession) SendDatagram(data []byte, dest i2pkeys.I2PAddr) error
```
SendDatagram sends a datagram to the specified destination
#### type DatagramWriter
```go
type DatagramWriter struct {
}
```
DatagramWriter handles outgoing datagram transmission
#### func (*DatagramWriter) SendDatagram
```go
func (w *DatagramWriter) SendDatagram(data []byte, dest i2pkeys.I2PAddr) error
```
SendDatagram sends a datagram to the specified destination
#### func (*DatagramWriter) SetTimeout
```go
func (w *DatagramWriter) SetTimeout(timeout time.Duration) *DatagramWriter
```
SetTimeout sets the timeout for datagram operations
#### type SAM
```go
type SAM struct {
*common.SAM
}
```
SAM wraps common.SAM to provide datagram-specific functionality
#### func (*SAM) NewDatagramSession
```go
func (s *SAM) NewDatagramSession(id string, keys i2pkeys.I2PKeys, options []string) (*DatagramSession, error)
```
NewDatagramSession creates a new datagram session with the SAM bridge
#### func (*SAM) NewDatagramSessionWithPorts
```go
func (s *SAM) NewDatagramSessionWithPorts(id, fromPort, toPort string, keys i2pkeys.I2PKeys, options []string) (*DatagramSession, error)
```
NewDatagramSessionWithPorts creates a new datagram session with port
specifications
#### func (*SAM) NewDatagramSessionWithSignature
```go
func (s *SAM) NewDatagramSessionWithSignature(id string, keys i2pkeys.I2PKeys, options []string, sigType string) (*DatagramSession, error)
```
NewDatagramSessionWithSignature creates a new datagram session with custom
signature type

23
datagram/README.md Normal file
View File

@@ -0,0 +1,23 @@
package datagram
High-level datagram library for UDP-like message delivery over I2P using the SAMv3 protocol.
## Installation
Install using Go modules with the package path `github.com/go-i2p/go-sam-go/datagram`.
## 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.
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.
## 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/sirupsen/logrus - Structured logging
- github.com/samber/oops - Enhanced error handling

111
datagram/SAM.go Normal file
View File

@@ -0,0 +1,111 @@
package datagram
import (
"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 datagram-specific functionality for I2P messaging.
// This type extends the base SAM functionality with methods specifically designed for
// datagram communication, including session creation with various configuration options
// and signature types for enhanced security and routing control.
// Example usage: sam := &SAM{SAM: baseSAM}; session, err := sam.NewDatagramSession(id, keys, options)
type SAM struct {
*common.SAM
}
// NewDatagramSession creates a new datagram session with the SAM bridge using default settings.
// This method establishes a new datagram session for UDP-like messaging over I2P with the specified
// session ID, cryptographic keys, and configuration options. It uses default signature settings
// and provides a simple interface for basic datagram communication needs.
// Example usage: session, err := sam.NewDatagramSession("my-session", keys, []string{"inbound.length=1"})
func (s *SAM) NewDatagramSession(id string, keys i2pkeys.I2PKeys, options []string) (*DatagramSession, error) {
// Delegate to the package-level function for session creation
// This provides consistency with the package API design
return NewDatagramSession(s.SAM, id, keys, options)
}
// NewDatagramSessionWithSignature creates a new datagram session with custom signature type.
// This method allows specifying a custom cryptographic signature type for the session,
// enabling advanced security configurations beyond the default signature algorithm.
// Different signature types provide various security levels and compatibility options.
// Example usage: session, err := sam.NewDatagramSessionWithSignature(id, keys, options, "EdDSA_SHA512_Ed25519")
func (s *SAM) NewDatagramSessionWithSignature(id string, keys i2pkeys.I2PKeys, options []string, sigType string) (*DatagramSession, error) {
// Log session creation with signature type for debugging
logger := log.WithFields(logrus.Fields{
"id": id,
"options": options,
"sigType": sigType,
})
logger.Debug("Creating new DatagramSession with signature")
// Create the base session using the common package with custom signature
// This enables advanced cryptographic configuration for enhanced security
session, err := s.SAM.NewGenericSessionWithSignature("DATAGRAM", 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 datagram session: %w", err)
}
// Ensure the session is of the correct type for datagram 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 datagram session with the base session and configuration
ds := &DatagramSession{
BaseSession: baseSession,
sam: s.SAM,
options: options,
}
logger.Debug("Successfully created DatagramSession with signature")
return ds, nil
}
// 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)
func (s *SAM) NewDatagramSessionWithPorts(id, fromPort, toPort string, keys i2pkeys.I2PKeys, options []string) (*DatagramSession, 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 DatagramSession with ports")
// Create the base session using the common package with port configuration
// This enables advanced port management for specific networking requirements
session, err := s.SAM.NewGenericSessionWithSignatureAndPorts("DATAGRAM", id, fromPort, toPort, keys, common.SIG_EdDSA_SHA512_Ed25519, options)
if err != nil {
logger.WithError(err).Error("Failed to create generic session with ports")
return nil, oops.Errorf("failed to create datagram session: %w", err)
}
// Ensure the session is of the correct type for datagram 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 datagram session with the base session and configuration
ds := &DatagramSession{
BaseSession: baseSession,
sam: s.SAM,
options: options,
}
logger.Debug("Successfully created DatagramSession with ports")
return ds, nil
}

View File

@@ -1,78 +0,0 @@
package datagram
import (
"errors"
"net"
"strconv"
"github.com/go-i2p/go-sam-go/common"
"github.com/go-i2p/i2pkeys"
"github.com/sirupsen/logrus"
)
// Creates a new datagram session. udpPort is the UDP port SAM is listening on,
// and if you set it to zero, it will use SAMs standard UDP port.
func (s *SAM) NewDatagramSession(id string, keys i2pkeys.I2PKeys, options []string, udpPort int) (*DatagramSession, error) {
log.WithFields(logrus.Fields{
"id": id,
"udpPort": udpPort,
}).Debug("Creating new DatagramSession")
if udpPort > 65335 || udpPort < 0 {
log.WithField("udpPort", udpPort).Error("Invalid UDP port")
return nil, errors.New("udpPort needs to be in the intervall 0-65335")
}
if udpPort == 0 {
udpPort = 7655
log.Debug("Using default UDP port 7655")
}
lhost, _, err := common.SplitHostPort(s.LocalAddr().String())
if err != nil {
log.WithError(err).Error("Failed to split local host port")
s.Close()
return nil, err
}
lUDPAddr, err := net.ResolveUDPAddr("udp4", lhost+":0")
if err != nil {
log.WithError(err).Error("Failed to resolve local UDP address")
return nil, err
}
udpconn, err := net.ListenUDP("udp4", lUDPAddr)
if err != nil {
log.WithError(err).Error("Failed to listen on UDP")
return nil, err
}
rhost, _, err := common.SplitHostPort(s.RemoteAddr().String())
if err != nil {
log.WithError(err).Error("Failed to split remote host port")
s.Close()
return nil, err
}
rUDPAddr, err := net.ResolveUDPAddr("udp4", rhost+":"+strconv.Itoa(udpPort))
if err != nil {
log.WithError(err).Error("Failed to resolve remote UDP address")
return nil, err
}
_, lport, err := net.SplitHostPort(udpconn.LocalAddr().String())
if err != nil {
log.WithError(err).Error("Failed to get local port")
s.Close()
return nil, err
}
conn, err := s.NewGenericSession("DATAGRAM", id, keys, []string{" PORT=" + lport})
if err != nil {
log.WithError(err).Error("Failed to create generic session")
return nil, err
}
log.WithField("id", id).Info("DatagramSession created successfully")
datagramSession := &DatagramSession{
SAM: s,
UDPConn: udpconn,
SAMUDPAddress: rUDPAddr,
RemoteI2PAddr: nil,
}
datagramSession.Conn = conn
return datagramSession, nil
// return &DatagramSession{s.address, id, conn, udpconn, keys, rUDPAddr, nil}, nil
}

160
datagram/dial.go Normal file
View File

@@ -0,0 +1,160 @@
package datagram
import (
"context"
"net"
"time"
"github.com/go-i2p/i2pkeys"
"github.com/samber/oops"
"github.com/sirupsen/logrus"
)
// Dial establishes a datagram connection to the specified I2P destination.
// This method creates a net.PacketConn interface for sending and receiving datagrams
// with the specified destination. It uses a default timeout of 30 seconds for connection
// establishment and provides UDP-like communication over I2P networks.
// Example usage: conn, err := session.Dial("destination.b32.i2p")
func (ds *DatagramSession) Dial(destination string) (net.PacketConn, error) {
// Use the timeout variant with default 30-second timeout
// This provides reasonable default behavior for most applications
return ds.DialTimeout(destination, 30*time.Second)
}
// DialTimeout establishes a datagram connection with specified timeout duration.
// This method creates a net.PacketConn interface with timeout support for connection
// establishment. Zero or negative timeout values disable the timeout mechanism.
// The timeout only applies to the initial connection setup, not to subsequent operations.
// Example usage: conn, err := session.DialTimeout("destination.b32.i2p", 60*time.Second)
func (ds *DatagramSession) DialTimeout(destination string, timeout time.Duration) (net.PacketConn, error) {
// Handle zero or negative timeout by disabling timeout completely
if timeout <= 0 {
return ds.DialContext(context.Background(), destination)
}
// Create a context with the specified timeout for connection establishment
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return ds.DialContext(ctx, destination)
}
// DialContext establishes a datagram connection with context support for cancellation.
// This method provides the core dialing functionality with context-based cancellation support,
// allowing for proper resource cleanup and operation cancellation through the provided context.
// It validates the destination and session state before attempting connection establishment.
// Example usage: conn, err := session.DialContext(ctx, "destination.b32.i2p")
func (ds *DatagramSession) DialContext(ctx context.Context, destination string) (net.PacketConn, error) {
// Check if the context is already cancelled before starting
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
// Validate that the session is not closed before attempting connection
ds.mu.RLock()
if ds.closed {
ds.mu.RUnlock()
return nil, oops.Errorf("session is closed")
}
ds.mu.RUnlock()
// Validate that the destination is not empty
if destination == "" {
return nil, oops.Errorf("destination cannot be empty")
}
// Create logging context for debugging connection establishment
logger := log.WithFields(logrus.Fields{
"destination": destination,
"session_id": ds.ID(),
})
logger.Debug("Dialing datagram destination")
// Create a datagram connection with integrated reader and writer
// This provides the net.PacketConn interface for standard Go networking compatibility
conn := &DatagramConn{
session: ds,
reader: ds.NewReader(),
writer: ds.NewWriter(),
}
// Start the reader's receive loop for continuous datagram processing
if conn.reader != nil {
go conn.reader.receiveLoop()
}
logger.Debug("Successfully created datagram connection")
return conn, nil
}
// DialI2P establishes a datagram connection to an I2P address using native addressing.
// This method creates a net.PacketConn interface for communicating with the specified I2P
// address using the native i2pkeys.I2PAddr type. It uses a default timeout of 30 seconds
// and provides type-safe addressing for I2P destinations.
// Example usage: conn, err := session.DialI2P(i2pAddress)
func (ds *DatagramSession) DialI2P(addr i2pkeys.I2PAddr) (net.PacketConn, error) {
// Use the timeout variant with default 30-second timeout
return ds.DialI2PTimeout(addr, 30*time.Second)
}
// DialI2PTimeout establishes a datagram connection to an I2P address with timeout.
// This method provides time-bounded connection establishment using native I2P addressing.
// Zero or negative timeout values disable the timeout mechanism. The timeout only applies
// to the initial connection setup, not to subsequent datagram operations.
// Example usage: conn, err := session.DialI2PTimeout(i2pAddress, 60*time.Second)
func (ds *DatagramSession) DialI2PTimeout(addr i2pkeys.I2PAddr, timeout time.Duration) (net.PacketConn, error) {
// Handle zero or negative timeout by disabling timeout completely
if timeout <= 0 {
return ds.DialI2PContext(context.Background(), addr)
}
// Create a context with the specified timeout for connection establishment
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return ds.DialI2PContext(ctx, addr)
}
// DialI2PContext establishes a datagram connection to an I2P address with context support.
// This method provides the core I2P dialing functionality with context-based cancellation,
// allowing for proper resource cleanup and operation cancellation through the provided context.
// It validates the session state and creates a connection with integrated reader and writer.
// Example usage: conn, err := session.DialI2PContext(ctx, i2pAddress)
func (ds *DatagramSession) DialI2PContext(ctx context.Context, addr i2pkeys.I2PAddr) (net.PacketConn, error) {
// Check if the context is already cancelled before starting
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
// Validate that the session is not closed before attempting connection
ds.mu.RLock()
if ds.closed {
ds.mu.RUnlock()
return nil, oops.Errorf("session is closed")
}
ds.mu.RUnlock()
// Create logging context for debugging I2P connection establishment
logger := log.WithFields(logrus.Fields{
"destination": addr.Base32(),
"session_id": ds.ID(),
})
logger.Debug("Dialing I2P datagram destination")
// Create a datagram connection with integrated reader and writer
conn := &DatagramConn{
session: ds,
reader: ds.NewReader(),
writer: ds.NewWriter(),
}
// Start the reader's receive loop for continuous datagram processing
if conn.reader != nil {
go conn.reader.receiveLoop()
}
logger.Debug("Successfully created I2P datagram connection")
return conn, nil
}

216
datagram/dial_test.go Normal file
View File

@@ -0,0 +1,216 @@
package datagram
import (
"context"
"errors"
"testing"
"time"
)
func TestDatagramSession_Dial(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
// Create two sessions - one for listener, one for dialer
sam1, keys1 := setupTestSAM(t)
defer sam1.Close()
sam2, keys2 := setupTestSAM(t)
defer sam2.Close()
// Create listener session
listenerSession, err := NewDatagramSession(sam1, "test_dial_listener", keys1, []string{
"inbound.length=0", "outbound.length=0",
})
if err != nil {
t.Fatalf("Failed to create listener session: %v", err)
}
defer listenerSession.Close()
listener, err := listenerSession.Listen()
if err != nil {
t.Fatalf("Failed to create listener: %v", err)
}
defer listener.Close()
// Create dialer session
dialerSession, err := NewDatagramSession(sam2, "test_dial_dialer", keys2, []string{
"inbound.length=0", "outbound.length=0",
})
if err != nil {
t.Fatalf("Failed to create dialer session: %v", err)
}
defer dialerSession.Close()
// Test dial
dest, err := dialerSession.sam.Lookup(listener.Addr().String())
if err != nil {
t.Fatalf("Failed to lookup listener address: %v", err)
}
conn, err := dialerSession.Dial(dest.Base64())
if err != nil {
t.Fatalf("Failed to dial: %v", err)
}
defer conn.Close()
// Verify connection properties
if conn == nil {
t.Fatal("Dial returned nil connection")
}
if conn.LocalAddr().String() != dialerSession.Addr().String() {
t.Errorf("Local address mismatch: got %s, want %s",
conn.LocalAddr().String(), dialerSession.Addr().String())
}
}
func TestDatagramSession_DialContext(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
// Create two sessions
sam1, keys1 := setupTestSAM(t)
defer sam1.Close()
sam2, keys2 := setupTestSAM(t)
defer sam2.Close()
// Create listener session
listenerSession, err := NewDatagramSession(sam1, "test_dialctx_listener", keys1, nil)
if err != nil {
t.Fatalf("Failed to create listener session: %v", err)
}
defer listenerSession.Close()
listener, err := listenerSession.Listen()
if err != nil {
t.Fatalf("Failed to create listener: %v", err)
}
defer listener.Close()
// Create dialer session
dialerSession, err := NewDatagramSession(sam2, "test_dialctx_dialer", keys2, nil)
if err != nil {
t.Fatalf("Failed to create dialer session: %v", err)
}
defer dialerSession.Close()
// Test dial with context
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
addr, err := dialerSession.sam.Lookup(listener.Addr().String())
if err != nil {
t.Fatalf("Failed to lookup listener address: %v", err)
}
conn, err := dialerSession.DialContext(ctx, addr.Base64())
if err != nil {
t.Fatalf("Failed to dial with context: %v", err)
}
defer conn.Close()
if conn == nil {
t.Fatal("DialContext returned nil connection")
}
}
func TestDatagramSession_DialContext_Timeout(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewDatagramSession(sam, "test_dialctx_timeout", keys, nil)
if err != nil {
t.Fatalf("Failed to create session: %v", err)
}
defer session.Close()
// Use very short timeout to force timeout
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Microsecond)
defer cancel()
addr, err := session.sam.Lookup("idk.i2p")
if err != nil {
t.Fatalf("Failed to lookup address: %v", err)
}
// Try to dial with short timeout
conn, err := session.DialContext(ctx, addr.Base64())
// Should get context deadline exceeded error
if err == nil {
if conn != nil {
conn.Close()
}
t.Fatal("Expected timeout error")
}
// Should be a context deadline exceeded error
if !errors.Is(err, context.DeadlineExceeded) {
t.Errorf("Expected context.DeadlineExceeded, got: %v", err)
}
if conn != nil {
t.Error("Expected nil connection on timeout")
}
}
func TestDatagramSession_Dial_ClosedSession(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewDatagramSession(sam, "test_dial_closed", keys, nil)
if err != nil {
t.Fatalf("Failed to create session: %v", err)
}
// Close the session first
session.Close()
// Try to dial on closed session
conn, err := session.Dial("test.b32.i2p")
if err == nil {
if conn != nil {
conn.Close()
}
t.Error("Expected error when dialing on closed session")
}
if conn != nil {
t.Error("Expected nil connection when session is closed")
}
}
func TestDatagramSession_NewDialer(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewDatagramSession(sam, "test_newdialer", keys, nil)
if err != nil {
t.Fatalf("Failed to create session: %v", err)
}
defer session.Close()
// Test that session can dial successfully (since NewDialer method doesn't exist)
// This test now verifies the basic dialing functionality
conn, err := session.Dial("test.b32.i2p")
if err != nil {
// Expected to fail with invalid address, but should not panic
t.Logf("Dial failed as expected with invalid address: %v", err)
} else if conn != nil {
conn.Close()
}
}

38
datagram/listen.go Normal file
View File

@@ -0,0 +1,38 @@
package datagram
import (
"net"
"github.com/samber/oops"
)
// Listen creates a new DatagramListener for accepting incoming connections.
// This method creates a listener that can accept multiple concurrent datagram
// connections on the same session. Each accepted connection will be a separate
// DatagramConn that shares the underlying session but has its own reader/writer.
// The listener starts an accept loop in a goroutine to handle incoming connections.
func (s *DatagramSession) Listen() (*DatagramListener, error) {
s.mu.RLock()
defer s.mu.RUnlock()
if s.closed {
return nil, oops.Errorf("session is closed")
}
logger := log.WithField("id", s.ID())
logger.Debug("Creating PacketListener")
listener := &DatagramListener{
session: s,
reader: s.NewReader(),
acceptChan: make(chan net.Conn, 10), // Buffer for incoming connections
errorChan: make(chan error, 1),
closeChan: make(chan struct{}),
}
// Start accepting packet connections in a goroutine
go listener.acceptLoop()
logger.Debug("Successfully created PacketListener")
return listener, nil
}

84
datagram/listen_test.go Normal file
View File

@@ -0,0 +1,84 @@
package datagram
import (
"testing"
)
func TestDatagramSession_Listen(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewDatagramSession(sam, "test_listen", keys, []string{
"inbound.length=0", "outbound.length=0",
})
if err != nil {
t.Fatalf("Failed to create session: %v", err)
}
defer session.Close()
listener, err := session.Listen()
if err != nil {
t.Fatalf("Failed to create listener: %v", err)
}
defer listener.Close()
// Verify listener properties
if listener.Addr().String() != session.Addr().String() {
t.Error("Listener address doesn't match session address")
}
// Verify listener is not nil and has expected fields
if listener.session != session {
t.Error("Listener session doesn't match created session")
}
if listener.reader == nil {
t.Error("Listener reader is nil")
}
if listener.acceptChan == nil {
t.Error("Listener acceptChan is nil")
}
if listener.errorChan == nil {
t.Error("Listener errorChan is nil")
}
if listener.closeChan == nil {
t.Error("Listener closeChan is nil")
}
}
func TestDatagramSession_Listen_ClosedSession(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewDatagramSession(sam, "test_listen_closed", keys, nil)
if err != nil {
t.Fatalf("Failed to create session: %v", err)
}
// Close the session first
session.Close()
// Try to create listener on closed session
listener, err := session.Listen()
if err == nil {
if listener != nil {
listener.Close()
}
t.Fatal("Expected error when creating listener on closed session")
}
if listener != nil {
t.Error("Expected nil listener when session is closed")
}
}

View File

@@ -1,10 +1,7 @@
package datagram
import logger "github.com/go-i2p/go-sam-go/logger"
import (
"github.com/go-i2p/logger"
)
var log = logger.GetSAM3Logger()
func init() {
logger.InitializeSAM3Logger()
log = logger.GetSAM3Logger()
}
var log = logger.GetGoI2PLogger()

169
datagram/packetconn.go Normal file
View File

@@ -0,0 +1,169 @@
package datagram
import (
"net"
"time"
"github.com/go-i2p/i2pkeys"
"github.com/samber/oops"
)
// ReadFrom reads a datagram from the connection and returns the number of bytes read,
// the source address, and any error encountered. 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 source address is returned as a DatagramAddr.
func (c *DatagramConn) 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 = &DatagramAddr{addr: datagram.Source}
return n, addr, nil
}
// WriteTo writes a datagram to the specified address and returns the number of bytes written
// and any error encountered. This method implements the net.PacketConn interface.
// The address must be a DatagramAddr containing a valid I2P destination.
// The entire byte slice p is sent as a single datagram message.
func (c *DatagramConn) 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.(*DatagramAddr)
if !ok {
return 0, oops.Errorf("address must be a DatagramAddr")
}
err = c.writer.SendDatagram(p, i2pAddr.addr)
if err != nil {
return 0, err
}
return len(p), nil
}
// Close closes the datagram 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 *DatagramConn) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.closed {
return nil
}
logger := log.WithField("session_id", c.session.ID())
logger.Debug("Closing DatagramConn")
c.closed = true
// 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 DatagramConn")
return nil
}
// LocalAddr returns the local network address as a DatagramAddr 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 *DatagramConn) LocalAddr() net.Addr {
return &DatagramAddr{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 *DatagramConn) 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 datagram connections,
// this is currently a placeholder implementation that always returns nil.
// Timeout handling is managed differently for datagram operations.
func (c *DatagramConn) 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 *DatagramConn) 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 *DatagramConn) 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 datagram connections,
// this returns the address of the last peer that sent data (set by Read),
// or nil if no data has been received yet.
func (c *DatagramConn) RemoteAddr() net.Addr {
if c.remoteAddr != nil {
return &DatagramAddr{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 *DatagramConn) 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 := &DatagramAddr{addr: *c.remoteAddr}
return c.WriteTo(b, addr)
}

142
datagram/packetlistener.go Normal file
View File

@@ -0,0 +1,142 @@
package datagram
import (
"net"
"sync"
"github.com/samber/oops"
)
// DatagramListener implements net.Listener for I2P datagram connections.
// It provides a way to accept incoming datagram connections in a stream-like manner,
// where each accepted connection represents a new DatagramConn that can be used
// for bidirectional communication with remote I2P destinations.
type DatagramListener struct {
session *DatagramSession
reader *DatagramReader
acceptChan chan net.Conn
errorChan chan error
closeChan chan struct{}
closed bool
mu sync.RWMutex
}
// Accept waits for and returns the next datagram connection to the listener.
// This method implements the net.Listener interface. It blocks until a new
// connection is available or an error occurs. Each accepted connection is a
// new DatagramConn that shares the underlying session but has its own reader/writer.
func (l *DatagramListener) Accept() (net.Conn, error) {
l.mu.RLock()
if l.closed {
l.mu.RUnlock()
return nil, oops.Errorf("listener is closed")
}
l.mu.RUnlock()
select {
case conn := <-l.acceptChan:
return conn, nil
case err := <-l.errorChan:
return nil, err
case <-l.closeChan:
return nil, oops.Errorf("listener is closed")
}
}
// Close closes the datagram listener and releases associated resources.
// This method implements the net.Listener interface. It stops accepting new
// connections and closes the reader. The underlying session is not closed
// as it may be shared by other components. Multiple calls to Close are safe.
func (l *DatagramListener) Close() error {
l.mu.Lock()
defer l.mu.Unlock()
if l.closed {
return nil
}
logger := log.WithField("session_id", l.session.ID())
logger.Debug("Closing PacketListener")
l.closed = true
close(l.closeChan)
// Close the reader
if l.reader != nil {
l.reader.Close()
}
logger.Debug("Successfully closed PacketListener")
return nil
}
// Addr returns the listener's network address as a DatagramAddr.
// This method implements the net.Listener interface and provides access
// to the I2P destination address that this listener is bound to.
func (l *DatagramListener) Addr() net.Addr {
return &DatagramAddr{addr: l.session.Addr()}
}
// acceptLoop continuously accepts incoming datagram connections.
// This method runs in a separate goroutine and creates new DatagramConn instances
// for each incoming connection, sending them through the acceptChan. It handles
// errors by sending them through errorChan and terminates when closeChan is closed.
func (l *DatagramListener) acceptLoop() {
logger := log.WithField("session_id", l.session.ID())
logger.Debug("Starting packet accept loop")
for {
select {
case <-l.closeChan:
logger.Debug("Packet accept loop terminated - listener closed")
return
default:
conn, err := l.acceptPacketConnection()
if err != nil {
l.mu.RLock()
closed := l.closed
l.mu.RUnlock()
if !closed {
logger.WithError(err).Error("Failed to accept packet connection")
select {
case l.errorChan <- err:
case <-l.closeChan:
return
}
}
continue
}
select {
case l.acceptChan <- conn:
logger.Debug("Successfully accepted new packet connection")
case <-l.closeChan:
conn.Close()
return
}
}
}
}
// acceptPacketConnection creates a new DatagramConn for incoming datagrams.
// This method creates a new connection that shares the session but has its own
// reader and writer components. It starts the reader loop for the new connection
// and returns it ready for use.
func (l *DatagramListener) acceptPacketConnection() (net.Conn, error) {
logger := log.WithField("session_id", l.session.ID())
logger.Debug("Creating new packet connection")
// For datagram sessions, we create a new DatagramConn that shares the session
// but has its own reader/writer for handling the specific connection
conn := &DatagramConn{
session: l.session,
reader: l.session.NewReader(),
writer: l.session.NewWriter(),
}
// Start the reader loop for this connection
go conn.reader.receiveLoop()
return conn, nil
}

272
datagram/read.go Normal file
View File

@@ -0,0 +1,272 @@
package datagram
import (
"bufio"
"encoding/base64"
"strings"
"sync/atomic"
"time"
"github.com/go-i2p/i2pkeys"
"github.com/go-i2p/logger"
"github.com/samber/oops"
)
// ReceiveDatagram receives a single datagram from the I2P network.
// This method blocks until a datagram is received or an error occurs, returning
// the received datagram with its data and addressing information. It handles
// concurrent access safely and provides proper error handling for network issues.
// Example usage: datagram, err := reader.ReceiveDatagram()
func (r *DatagramReader) ReceiveDatagram() (*Datagram, error) {
// Check if the reader is closed before attempting to receive
// This prevents operations on invalid readers and provides clear error messages
r.mu.RLock()
if r.closed {
r.mu.RUnlock()
return nil, oops.Errorf("reader is closed")
}
r.mu.RUnlock()
// Use select to handle multiple channel operations atomically
// This ensures proper handling of datagrams, errors, and close signals
select {
case datagram := <-r.recvChan:
// Successfully received a 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 DatagramReader 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 *DatagramReader) 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.mu.Lock()
defer r.mu.Unlock()
if r.closed {
return
}
// Log reader closure for debugging and monitoring
sessionID := "unknown"
if r.session != nil && r.session.BaseSession != nil {
sessionID = r.session.ID()
}
logger := log.WithField("session_id", sessionID)
logger.Debug("Closing DatagramReader")
r.closed = true
// Set atomic flag to indicate we're closing
atomic.StoreInt32(&r.closing, 1)
// Signal the receive loop to terminate
// This prevents the background goroutine from continuing to run
close(r.closeChan)
// 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")
}
// Clean up channels to prevent resource leaks
// Close channels that are safe to close
close(r.recvChan)
close(r.errorChan)
logger.Debug("Successfully closed DatagramReader")
})
return nil
}
// receiveLoop continuously receives incoming datagrams in a separate goroutine.
// This method handles the SAM protocol communication for datagram reception, parsing
// DATAGRAM RECEIVED responses and forwarding datagrams to the appropriate channels.
// It runs until the reader is closed and provides error handling for network issues.
func (r *DatagramReader) receiveLoop() {
logger := r.initializeReceiveLoop()
defer r.signalReceiveLoopCompletion()
if err := r.validateSessionState(logger); err != nil {
return
}
r.runReceiveLoop(logger)
}
// initializeReceiveLoop sets up logging context and returns a logger for the receive loop.
func (r *DatagramReader) 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 datagram receive loop")
return logger
}
// signalReceiveLoopCompletion signals that the receive loop has completed execution.
func (r *DatagramReader) 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 *DatagramReader) 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 *DatagramReader) 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 datagram, returning false if the loop should terminate.
func (r *DatagramReader) processIncomingDatagram(logger *logger.Entry) bool {
// Check atomic flag first to avoid race condition
if atomic.LoadInt32(&r.closing) == 1 {
return false
}
datagram, err := r.receiveDatagram()
if err != nil {
logger.WithError(err).Debug("Error receiving datagram")
select {
case r.errorChan <- err:
case <-r.closeChan:
return false
}
return true
}
// Check atomic flag again before sending to avoid race condition
if atomic.LoadInt32(&r.closing) == 1 {
return false
}
select {
case r.recvChan <- datagram:
// Successfully forwarded the datagram
case <-r.closeChan:
return false
}
return true
}
// receiveDatagram performs the actual datagram reception from the SAM bridge.
// This method handles the low-level SAM protocol communication, parsing DATAGRAM RECEIVED
// responses and extracting the datagram data and addressing information. It provides
// the core functionality for the receive loop and handles protocol-specific details.
// receiveDatagram performs the actual datagram reception from the SAM bridge
func (r *DatagramReader) receiveDatagram() (*Datagram, error) {
// Read data from the SAM connection
// This blocks until data is available or an error occurs
conn := r.session.Conn()
buffer := make([]byte, 4096)
n, err := conn.Read(buffer)
if err != nil {
return nil, oops.Errorf("failed to read from SAM connection: %w", err)
}
// Parse the received data as a SAM protocol message
// The message format follows SAMv3 specifications for datagram reception
response := string(buffer[:n])
log.WithField("response", response).Debug("Received SAM response")
// Parse the response to extract datagram information
// This involves parsing the SAM protocol format and extracting the payload
if !strings.Contains(response, "DATAGRAM RECEIVED") {
return nil, oops.Errorf("unexpected response format: %s", response)
}
// Parse the DATAGRAM RECEIVED response
scanner := bufio.NewScanner(strings.NewReader(response))
scanner.Split(bufio.ScanWords)
var source, data string
for scanner.Scan() {
word := scanner.Text()
switch {
case word == "DATAGRAM":
continue
case word == "RECEIVED":
continue
case strings.HasPrefix(word, "DESTINATION="):
source = word[12:]
case strings.HasPrefix(word, "SIZE="):
continue // We'll get the actual data size from the payload
default:
// Remaining data is the base64-encoded payload
if data == "" {
data = word
} else {
data += " " + word
}
}
}
if source == "" {
return nil, oops.Errorf("no source in datagram")
}
if data == "" {
return nil, oops.Errorf("no data in datagram")
}
// Parse the source destination
sourceAddr, err := i2pkeys.NewI2PAddrFromString(source)
if err != nil {
return nil, oops.Errorf("failed to parse source address: %w", err)
}
// Decode the base64 data
decodedData, err := base64.StdEncoding.DecodeString(data)
if err != nil {
return nil, oops.Errorf("failed to decode datagram data: %w", err)
}
// Create the datagram
datagram := &Datagram{
Data: decodedData,
Source: sourceAddr,
Local: r.session.Addr(),
}
return datagram, nil
}

87
datagram/read_test.go Normal file
View File

@@ -0,0 +1,87 @@
package datagram
import (
"testing"
"time"
)
func TestDatagramSession_ConcurrentOperations(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
// Add overall test timeout
timeout := time.After(30 * time.Second)
done := make(chan bool)
go func() {
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewDatagramSession(sam, "test_concurrent", keys, nil)
if err != nil {
t.Errorf("Failed to create session: %v", err)
done <- false
return
}
defer session.Close()
// Test concurrent reader creation
readerDone := make(chan bool, 10)
for i := 0; i < 10; i++ {
go func() {
reader := session.NewReader()
if reader == nil {
t.Error("NewReader returned nil")
}
// Immediately close reader to prevent resource leaks
reader.Close()
readerDone <- true
}()
}
// Test concurrent writer creation
writerDone := make(chan bool, 10)
for i := 0; i < 10; i++ {
go func() {
writer := session.NewWriter()
if writer == nil {
t.Error("NewWriter returned nil")
}
writerDone <- true
}()
}
// Wait for all goroutines with timeout
for i := 0; i < 10; i++ {
select {
case <-readerDone:
case <-time.After(5 * time.Second):
t.Error("Timeout waiting for reader creation")
done <- false
return
}
}
for i := 0; i < 10; i++ {
select {
case <-writerDone:
case <-time.After(2 * time.Second):
t.Error("Timeout waiting for writer creation")
done <- false
return
}
}
done <- true
}()
select {
case success := <-done:
if !success {
t.Fatal("Test failed")
}
case <-timeout:
t.Fatal("Test timeout - likely goroutine leak or blocking operation")
}
}

View File

@@ -1,209 +1,177 @@
package datagram
import (
"bytes"
"errors"
"net"
"time"
"sync"
"github.com/go-i2p/go-sam-go/common"
"github.com/go-i2p/i2pkeys"
"github.com/samber/oops"
"github.com/sirupsen/logrus"
)
func (s *DatagramSession) B32() string {
b32 := s.DestinationKeys.Addr().Base32()
log.WithField("b32", b32).Debug("Generated B32 address")
return b32
}
// 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(sam, "my-session", keys, []string{"inbound.length=1"})
func NewDatagramSession(sam *common.SAM, id string, keys i2pkeys.I2PKeys, options []string) (*DatagramSession, error) {
// Log session creation with detailed parameters for debugging
logger := log.WithFields(logrus.Fields{
"id": id,
"options": options,
})
logger.Debug("Creating new DatagramSession")
func (s *DatagramSession) Dial(net, addr string) (*DatagramSession, error) {
log.WithFields(logrus.Fields{
"net": net,
"addr": addr,
}).Debug("Dialing address")
netaddr, err := s.Lookup(addr)
// 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("DATAGRAM", id, keys, options)
if err != nil {
log.WithError(err).Error("Lookup failed")
return nil, err
logger.WithError(err).Error("Failed to create generic session")
return nil, oops.Errorf("failed to create datagram session: %w", err)
}
return s.DialI2PRemote(net, netaddr)
// Ensure the session is of the correct type for datagram 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 datagram session with the base session and configuration
ds := &DatagramSession{
BaseSession: baseSession,
sam: sam,
options: options,
}
logger.Debug("Successfully created DatagramSession")
return ds, nil
}
func (s *DatagramSession) DialRemote(net, addr string) (net.PacketConn, error) {
log.WithFields(logrus.Fields{
"net": net,
"addr": addr,
}).Debug("Dialing remote address")
netaddr, err := s.Lookup(addr)
if err != nil {
log.WithError(err).Error("Lookup failed")
return nil, err
}
return s.DialI2PRemote(net, netaddr)
}
func (s *DatagramSession) DialI2PRemote(net string, addr net.Addr) (*DatagramSession, error) {
log.WithFields(logrus.Fields{
"net": net,
"addr": addr,
}).Debug("Dialing I2P remote address")
switch addr.(type) {
case *i2pkeys.I2PAddr:
s.RemoteI2PAddr = addr.(*i2pkeys.I2PAddr)
case i2pkeys.I2PAddr:
i2paddr := addr.(i2pkeys.I2PAddr)
s.RemoteI2PAddr = &i2paddr
}
return s, nil
}
func (s *DatagramSession) RemoteAddr() net.Addr {
log.WithField("remoteAddr", s.RemoteI2PAddr).Debug("Getting remote address")
return s.RemoteI2PAddr
}
// Reads one datagram sent to the destination of the DatagramSession. Returns
// the number of bytes read, from what address it was sent, or an error.
// implements net.PacketConn
func (s *DatagramSession) ReadFrom(b []byte) (n int, addr net.Addr, err error) {
log.Debug("Reading datagram")
// extra bytes to read the remote address of incomming datagram
buf := make([]byte, len(b)+4096)
for {
// very basic protection: only accept incomming UDP messages from the IP of the SAM bridge
var saddr *net.UDPAddr
n, saddr, err = s.UDPConn.ReadFromUDP(buf)
if err != nil {
log.WithError(err).Error("Failed to read from UDP")
return 0, i2pkeys.I2PAddr(""), err
}
if bytes.Equal(saddr.IP, s.SAMUDPAddress.IP) {
continue
}
break
}
i := bytes.IndexByte(buf, byte('\n'))
if i > 4096 || i > n {
log.Error("Could not parse incoming message remote address")
return 0, i2pkeys.I2PAddr(""), errors.New("Could not parse incomming message remote address.")
}
raddr, err := i2pkeys.NewI2PAddrFromString(string(buf[:i]))
if err != nil {
log.WithError(err).Error("Could not parse incoming message remote address")
return 0, i2pkeys.I2PAddr(""), errors.New("Could not parse incomming message remote address: " + err.Error())
}
// shift out the incomming address to contain only the data received
if (n - i + 1) > len(b) {
copy(b, buf[i+1:i+1+len(b)])
return n - (i + 1), raddr, errors.New("Datagram did not fit into your buffer.")
} else {
copy(b, buf[i+1:n])
log.WithField("bytesRead", n-(i+1)).Debug("Datagram read successfully")
return n - (i + 1), raddr, nil
// NewReader creates a DatagramReader for receiving datagrams from any source.
// 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 *DatagramSession) NewReader() *DatagramReader {
// Create reader with buffered channels for non-blocking operation
// The buffer size of 10 prevents blocking when multiple datagrams arrive rapidly
return &DatagramReader{
session: s,
recvChan: make(chan *Datagram, 10), // Buffer for incoming datagrams
errorChan: make(chan error, 1),
closeChan: make(chan struct{}),
doneChan: make(chan struct{}, 1),
closed: false,
mu: sync.RWMutex{},
closeOnce: sync.Once{},
}
}
func (s *DatagramSession) Accept() (net.Conn, error) {
log.Debug("Accept called on DatagramSession")
return s, nil
}
func (s *DatagramSession) Read(b []byte) (n int, err error) {
log.Debug("Reading from DatagramSession")
rint, _, rerr := s.ReadFrom(b)
return rint, rerr
}
// Sends one signed datagram to the destination specified. At the time of
// writing, maximum size is 31 kilobyte, but this may change in the future.
// Implements net.PacketConn.
func (s *DatagramSession) WriteTo(b []byte, addr net.Addr) (n int, err error) {
log.WithFields(logrus.Fields{
"addr": addr,
"datagramLen": len(b),
}).Debug("Writing datagram")
header := []byte("3.1 " + s.ID() + " " + addr.String() + "\n")
msg := append(header, b...)
n, err = s.UDPConn.WriteToUDP(msg, s.SAMUDPAddress)
if err != nil {
log.WithError(err).Error("Failed to write to UDP")
} else {
log.WithField("bytesWritten", n).Debug("Datagram written successfully")
// NewWriter creates a DatagramWriter for sending datagrams to specific 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 *DatagramSession) NewWriter() *DatagramWriter {
// Initialize writer with default timeout for send operations
// The timeout prevents indefinite blocking on send operations
return &DatagramWriter{
session: s,
timeout: 30, // Default timeout in seconds
}
return n, err
}
func (s *DatagramSession) Write(b []byte) (int, error) {
log.WithField("dataLen", len(b)).Debug("Writing to DatagramSession")
return s.WriteTo(b, s.RemoteI2PAddr)
// PacketConn returns a net.PacketConn interface for this session.
// This method provides compatibility with standard Go networking code by wrapping
// the datagram session in a connection that implements the PacketConn interface.
// Example usage: conn := session.PacketConn(); n, addr, err := conn.ReadFrom(buffer)
func (s *DatagramSession) PacketConn() net.PacketConn {
// Create a PacketConn wrapper with integrated reader and writer
// This provides standard Go networking interface compliance
return &DatagramConn{
session: s,
reader: s.NewReader(),
writer: s.NewWriter(),
}
}
// Closes the DatagramSession. Implements net.PacketConn
// SendDatagram sends a 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([]byte("hello"), destinationAddr)
func (s *DatagramSession) 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 datagram from any source.
// This is a convenience method that creates a temporary reader, starts the receive loop,
// gets one datagram, and cleans up resources automatically. For continuous reception,
// use NewReader() and manage the reader lifecycle manually.
// Example usage: datagram, err := session.ReceiveDatagram()
func (s *DatagramSession) ReceiveDatagram() (*Datagram, error) {
// Create temporary reader for one-time receive operations
reader := s.NewReader()
// Start the receive loop for datagram processing
go reader.receiveLoop()
return reader.ReceiveDatagram()
}
// 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()
func (s *DatagramSession) Close() error {
log.Debug("Closing DatagramSession")
err := s.Conn.Close()
err2 := s.UDPConn.Close()
if err != nil {
log.WithError(err).Error("Failed to close connection")
return err
s.mu.Lock()
defer s.mu.Unlock()
if s.closed {
return nil
}
if err2 != nil {
log.WithError(err2).Error("Failed to close UDP connection")
// Log session closure for debugging and monitoring
logger := log.WithField("id", s.ID())
logger.Debug("Closing DatagramSession")
s.closed = true
// 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 datagram session: %w", err)
}
return err2
logger.Debug("Successfully closed DatagramSession")
return nil
}
// Returns the I2P destination of the DatagramSession.
func (s *DatagramSession) LocalI2PAddr() i2pkeys.I2PAddr {
addr := s.DestinationKeys.Addr()
log.WithField("localI2PAddr", addr).Debug("Getting local I2P address")
return addr
// Addr returns the I2P address of this session.
// This address represents the session's identity on the I2P network and can be
// used by other nodes to send datagrams 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 *DatagramSession) Addr() i2pkeys.I2PAddr {
// Return the I2P address derived from the session's cryptographic keys
return s.Keys().Addr()
}
// Implements net.PacketConn
func (s *DatagramSession) LocalAddr() net.Addr {
return s.LocalI2PAddr()
// Network returns the network type for this address.
// This method implements the net.Addr interface and always returns "i2p-datagram"
// to identify this as an I2P datagram address type for networking compatibility.
// Example usage: network := addr.Network() // returns "i2p-datagram"
func (a *DatagramAddr) Network() string {
// Return the network type identifier for I2P datagram addresses
return "i2p-datagram"
}
func (s *DatagramSession) Addr() net.Addr {
return s.LocalI2PAddr()
}
func (s *DatagramSession) Lookup(name string) (a net.Addr, err error) {
log.WithField("name", name).Debug("Looking up address")
var sam *common.SAM
sam, err = common.NewSAM(s.Sam())
if err == nil {
defer sam.Close()
a, err = sam.Lookup(name)
}
log.WithField("address", a).Debug("Lookup successful")
return
}
// Sets read and write deadlines for the DatagramSession. Implements
// net.PacketConn and does the same thing. Setting write deadlines for datagrams
// is seldom done.
func (s *DatagramSession) SetDeadline(t time.Time) error {
log.WithField("deadline", t).Debug("Setting deadline")
return s.UDPConn.SetDeadline(t)
}
// Sets read deadline for the DatagramSession. Implements net.PacketConn
func (s *DatagramSession) SetReadDeadline(t time.Time) error {
log.WithField("readDeadline", t).Debug("Setting read deadline")
return s.UDPConn.SetReadDeadline(t)
}
// Sets the write deadline for the DatagramSession. Implements net.Packetconn.
func (s *DatagramSession) SetWriteDeadline(t time.Time) error {
log.WithField("writeDeadline", t).Debug("Setting write deadline")
return s.UDPConn.SetWriteDeadline(t)
}
func (s *DatagramSession) SetWriteBuffer(bytes int) error {
log.WithField("bytes", bytes).Debug("Setting write buffer")
return s.UDPConn.SetWriteBuffer(bytes)
// 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 and logging.
// Example usage: addrStr := addr.String() // returns "abcd1234...xyz.b32.i2p"
func (a *DatagramAddr) String() string {
// Return the Base32 encoded I2P address for human-readable representation
return a.addr.Base32()
}

271
datagram/session_test.go Normal file
View File

@@ -0,0 +1,271 @@
package datagram
import (
"testing"
"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
}
func TestNewDatagramSession(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
tests := []struct {
name string
id string
options []string
wantErr bool
}{
{
name: "basic session creation",
id: "test_datagram_session",
options: nil,
wantErr: false,
},
{
name: "session with options",
id: "test_datagram_with_opts",
options: []string{"inbound.length=1", "outbound.length=1"},
wantErr: false,
},
{
name: "session with small tunnel config",
id: "test_datagram_small",
options: []string{
"inbound.length=0",
"outbound.length=0",
"inbound.lengthVariance=0",
"outbound.lengthVariance=0",
"inbound.quantity=1",
"outbound.quantity=1",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewDatagramSession(sam, tt.id, keys, tt.options)
if (err != nil) != tt.wantErr {
t.Errorf("NewDatagramSession() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil {
// Verify session properties
if session.ID() != tt.id {
t.Errorf("Session ID = %v, want %v", session.ID(), tt.id)
}
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")
}
// Clean up
if err := session.Close(); err != nil {
t.Errorf("Failed to close session: %v", err)
}
}
})
}
}
func TestDatagramSession_Close(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewDatagramSession(sam, "test_close", 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
err = session.Close()
if err != nil {
t.Errorf("Second Close() error = %v", err)
}
}
func TestDatagramSession_Addr(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewDatagramSession(sam, "test_addr", 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())
}
}
func TestDatagramSession_NewReader(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewDatagramSession(sam, "test_reader", 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")
}
}
func TestDatagramSession_NewWriter(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewDatagramSession(sam, "test_writer", 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)
}
}
func TestDatagramSession_PacketConn(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewDatagramSession(sam, "test_packetconn", 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")
}
datagramConn, ok := conn.(*DatagramConn)
if !ok {
t.Error("PacketConn() did not return a DatagramConn")
}
if datagramConn.session != session {
t.Error("DatagramConn session reference is incorrect")
}
if datagramConn.reader == nil {
t.Error("DatagramConn reader is nil")
}
if datagramConn.writer == nil {
t.Error("DatagramConn writer is nil")
}
}
func TestDatagramAddr_Network(t *testing.T) {
addr := &DatagramAddr{}
if addr.Network() != "i2p-datagram" {
t.Errorf("Network() = %v, want i2p-datagram", addr.Network())
}
}
func TestDatagramAddr_String(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
sam, keys := setupTestSAM(t)
defer sam.Close()
addr := &DatagramAddr{addr: keys.Addr()}
expected := keys.Addr().Base32()
if addr.String() != expected {
t.Errorf("String() = %v, want %v", addr.String(), expected)
}
}

View File

@@ -1,21 +1,83 @@
package datagram
import (
"net"
"sync"
"time"
"github.com/go-i2p/go-sam-go/common"
"github.com/go-i2p/i2pkeys"
)
type SAM common.SAM
// The DatagramSession implements net.PacketConn. It works almost like ordinary
// UDP, except that datagrams may be at most 31kB large. These datagrams are
// also end-to-end encrypted, signed and includes replay-protection. And they
// are also built to be surveillance-resistant (yey!).
// DatagramSession represents a datagram session that can send and receive 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)
type DatagramSession struct {
*SAM
UDPConn *net.UDPConn // used to deliver datagrams
SAMUDPAddress *net.UDPAddr // the SAM bridge UDP-port
RemoteI2PAddr *i2pkeys.I2PAddr // optional remote I2P address
*common.BaseSession
sam *common.SAM
options []string
mu sync.RWMutex
closed bool
}
// DatagramReader handles incoming datagram 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.
// Example usage: reader := session.NewReader(); datagram, err := reader.ReceiveDatagram()
type DatagramReader struct {
session *DatagramSession
recvChan chan *Datagram
errorChan chan error
closeChan chan struct{}
doneChan chan struct{}
closed bool
closing int32 // atomic flag to coordinate channel closure
mu sync.RWMutex
closeOnce sync.Once
}
// DatagramWriter handles outgoing datagram 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.
// Example usage: writer := session.NewWriter().SetTimeout(30*time.Second); err := writer.SendDatagram(data, dest)
type DatagramWriter struct {
session *DatagramSession
timeout time.Duration
}
// Datagram represents an I2P datagram message containing data and address information.
// It encapsulates the payload data along with source and destination addressing details,
// providing all necessary information for processing received datagrams or preparing outgoing ones.
// The structure includes both the raw data bytes and I2P address information for routing.
// Example usage: if datagram.Source.Base32() == expectedSender { processData(datagram.Data) }
type Datagram struct {
Data []byte
Source i2pkeys.I2PAddr
Local i2pkeys.I2PAddr
}
// DatagramAddr implements net.Addr interface for I2P datagram 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 := &DatagramAddr{addr: destination}; fmt.Println(addr.Network(), addr.String())
type DatagramAddr struct {
addr i2pkeys.I2PAddr
}
// DatagramConn implements net.PacketConn interface for I2P datagram communication.
// This type provides compatibility with standard Go networking patterns by wrapping
// datagram session functionality in a familiar PacketConn interface. It manages
// internal readers and writers while providing standard connection operations.
// Example usage: conn := session.PacketConn(); n, addr, err := conn.ReadFrom(buffer)
type DatagramConn struct {
session *DatagramSession
reader *DatagramReader
writer *DatagramWriter
remoteAddr *i2pkeys.I2PAddr
mu sync.RWMutex
closed bool
}

14
datagram/types_test.go Normal file
View File

@@ -0,0 +1,14 @@
package datagram
import (
"net"
"github.com/go-i2p/go-sam-go/common"
)
var (
ds common.Session = &DatagramSession{}
dl net.Listener = &DatagramListener{}
dc net.PacketConn = &DatagramConn{}
dcc net.Conn = &DatagramConn{}
)

116
datagram/write.go Normal file
View File

@@ -0,0 +1,116 @@
package datagram
import (
"encoding/base64"
"fmt"
"strings"
"time"
"github.com/go-i2p/i2pkeys"
"github.com/samber/oops"
"github.com/sirupsen/logrus"
)
// SetTimeout sets the timeout for datagram 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 *DatagramWriter) SetTimeout(timeout time.Duration) *DatagramWriter {
// Configure the timeout for send operations to prevent indefinite blocking
w.timeout = timeout
return w
}
// SendDatagram sends a datagram to the specified I2P destination address.
// This method handles the complete datagram transmission process including data encoding,
// SAM protocol communication, and response validation. It blocks until the datagram
// is sent or an error occurs, respecting the configured timeout duration.
// Example usage: err := writer.SendDatagram([]byte("hello world"), destinationAddr)
func (w *DatagramWriter) SendDatagram(data []byte, dest i2pkeys.I2PAddr) error {
// Check if the session is closed before attempting to send
// This prevents operations on invalid sessions
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),
})
logger.Debug("Sending datagram")
// Encode the datagram data as base64 for SAM protocol transmission
// The SAM protocol requires base64 encoding for binary data transfer
encodedData := base64.StdEncoding.EncodeToString(data)
// Create the DATAGRAM SEND command following SAMv3 protocol format
// This command includes session ID, destination address, size, and encoded data
sendCmd := fmt.Sprintf("DATAGRAM SEND ID=%s DESTINATION=%s SIZE=%d\n%s\n",
w.session.ID(), dest.Base64(), len(data), encodedData)
// Log the command being sent for debugging protocol communication
logger.WithField("command", strings.Split(sendCmd, "\n")[0]).Debug("Sending DATAGRAM SEND")
// Send the command to the SAM bridge and read the response
// This handles the underlying protocol communication with error handling
conn := w.session.Conn()
if _, err := conn.Write([]byte(sendCmd)); err != nil {
logger.WithError(err).Error("Failed to send datagram command")
return oops.Errorf("failed to send datagram command: %w", err)
}
// Read the response from the SAM bridge to check for errors
// The response indicates whether the datagram was successfully queued for transmission
response := make([]byte, 1024)
n, err := conn.Read(response)
if err != nil {
logger.WithError(err).Error("Failed to read datagram response")
return oops.Errorf("failed to read datagram response: %w", err)
}
// Parse the response to check for transmission errors
// The SAM bridge returns status information about the send operation
if err := w.parseSendResponse(string(response[:n])); err != nil {
logger.WithError(err).Error("Datagram send failed")
return oops.Errorf("datagram send failed: %w", err)
}
logger.Debug("Successfully sent datagram")
return nil
}
// parseSendResponse parses the DATAGRAM STATUS response from the SAM bridge.
// This method analyzes the response from a datagram send operation to determine
// if the transmission was successful or if any errors occurred during the process.
// It provides detailed error information for debugging and error handling.
// parseSendResponse parses the DATAGRAM STATUS response from the SAM bridge
func (w *DatagramWriter) parseSendResponse(response string) error {
// Parse the response to extract status information
// The response format follows SAMv3 protocol specifications
if strings.Contains(response, "RESULT=OK") {
log.Debug("Datagram send successful")
return nil
}
// Handle various error conditions that can occur during send operations
// Different errors provide specific information about transmission failures
if strings.Contains(response, "RESULT=INVALID_KEY") {
return oops.Errorf("invalid destination key")
}
if strings.Contains(response, "RESULT=KEY_NOT_FOUND") {
return oops.Errorf("destination key not found")
}
if strings.Contains(response, "RESULT=INVALID_ID") {
return oops.Errorf("invalid session ID")
}
// Log the unexpected response for debugging protocol issues
log.WithField("response", response).Error("Unexpected datagram send response")
return oops.Errorf("unexpected datagram send response: %s", response)
}

6
datagram2/DOC.md Normal file
View File

@@ -0,0 +1,6 @@
# datagram2
--
import "github.com/go-i2p/go-sam-go/datagram2"
## Usage

8
datagram2/doc.go Normal file
View File

@@ -0,0 +1,8 @@
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.
*/

6
datagram3/DOC.md Normal file
View File

@@ -0,0 +1,6 @@
# datagram3
--
import "github.com/go-i2p/go-sam-go/datagram3"
## Usage

8
datagram3/doc.go Normal file
View File

@@ -0,0 +1,8 @@
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.
*/

View File

@@ -1,183 +0,0 @@
// package sam3 wraps the original sam3 API from github.com/go-i2p/sam3
package sam3
import (
"fmt"
"testing"
"time"
)
func Test_DatagramServerClient(t *testing.T) {
if testing.Short() {
return
}
fmt.Println("Test_DatagramServerClient")
sam, err := NewSAM(yoursam)
if err != nil {
t.Fail()
return
}
defer sam.Close()
keys, err := sam.NewKeys()
if err != nil {
t.Fail()
return
}
// fmt.Println("\tServer: My address: " + keys.Addr().Base32())
fmt.Println("\tServer: Creating tunnel")
ds, err := sam.NewDatagramSession("DGserverTun", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}, 0)
if err != nil {
fmt.Println("Server: Failed to create tunnel: " + err.Error())
t.Fail()
return
}
c, w := make(chan bool), make(chan bool)
go func(c, w chan (bool)) {
sam2, err := NewSAM(yoursam)
if err != nil {
c <- false
return
}
defer sam2.Close()
keys, err := sam2.NewKeys()
if err != nil {
c <- false
return
}
fmt.Println("\tClient: Creating tunnel")
ds2, err := sam2.NewDatagramSession("DGclientTun", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}, 0)
if err != nil {
c <- false
return
}
defer ds2.Close()
// fmt.Println("\tClient: Servers address: " + ds.LocalAddr().Base32())
// fmt.Println("\tClient: Clients address: " + ds2.LocalAddr().Base32())
fmt.Println("\tClient: Tries to send datagram to server")
for {
select {
default:
_, err = ds2.WriteTo([]byte("Hello datagram-world! <3 <3 <3 <3 <3 <3"), ds.LocalAddr())
if err != nil {
fmt.Println("\tClient: Failed to send datagram: " + err.Error())
c <- false
return
}
time.Sleep(5 * time.Second)
case <-w:
fmt.Println("\tClient: Sent datagram, quitting.")
return
}
}
c <- true
}(c, w)
buf := make([]byte, 512)
fmt.Println("\tServer: ReadFrom() waiting...")
n, _, err := ds.ReadFrom(buf)
w <- true
if err != nil {
fmt.Println("\tServer: Failed to ReadFrom(): " + err.Error())
t.Fail()
return
}
fmt.Println("\tServer: Received datagram: " + string(buf[:n]))
// fmt.Println("\tServer: Senders address was: " + saddr.Base32())
}
func ExampleDatagramSession() {
// Creates a new DatagramSession, which behaves just like a net.PacketConn.
const samBridge = "127.0.0.1:7656"
sam, err := NewSAM(samBridge)
if err != nil {
fmt.Println(err.Error())
return
}
keys, err := sam.NewKeys()
if err != nil {
fmt.Println(err.Error())
return
}
myself := keys.Addr()
// See the example Option_* variables.
dg, err := sam.NewDatagramSession("DGTUN", keys, Options_Small, 0)
if err != nil {
fmt.Println(err.Error())
return
}
someone, err := sam.Lookup("zzz.i2p")
if err != nil {
fmt.Println(err.Error())
return
}
dg.WriteTo([]byte("Hello stranger!"), someone)
dg.WriteTo([]byte("Hello myself!"), myself)
buf := make([]byte, 31*1024)
n, _, err := dg.ReadFrom(buf)
if err != nil {
fmt.Println(err.Error())
return
}
fmt.Println("Got message: '" + string(buf[:n]) + "'")
fmt.Println("Got message: " + string(buf[:n]))
return
// Output:
// Got message: Hello myself!
}
func ExampleMiniDatagramSession() {
// Creates a new DatagramSession, which behaves just like a net.PacketConn.
const samBridge = "127.0.0.1:7656"
sam, err := NewSAM(samBridge)
if err != nil {
fmt.Println(err.Error())
return
}
keys, err := sam.NewKeys()
if err != nil {
fmt.Println(err.Error())
return
}
myself := keys.Addr()
// See the example Option_* variables.
dg, err := sam.NewDatagramSession("MINIDGTUN", keys, Options_Small, 0)
if err != nil {
fmt.Println(err.Error())
return
}
someone, err := sam.Lookup("zzz.i2p")
if err != nil {
fmt.Println(err.Error())
return
}
err = dg.SetWriteBuffer(14 * 1024)
if err != nil {
fmt.Println(err.Error())
return
}
dg.WriteTo([]byte("Hello stranger!"), someone)
dg.WriteTo([]byte("Hello myself!"), myself)
buf := make([]byte, 31*1024)
n, _, err := dg.ReadFrom(buf)
if err != nil {
fmt.Println(err.Error())
return
}
log.Println("Got message: '" + string(buf[:n]) + "'")
fmt.Println("Got message: " + string(buf[:n]))
return
// Output:
// Got message: Hello myself!
}

View File

@@ -1,437 +0,0 @@
// package sam3 wraps the original sam3 API from github.com/go-i2p/sam3
package sam3
import (
"fmt"
"strconv"
"strings"
"github.com/sirupsen/logrus"
)
// Option is a SAMEmit Option
type Option func(*SAMEmit) error
// SetType sets the type of the forwarder server
func SetType(s string) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if s == "STREAM" {
c.Style = s
log.WithField("style", s).Debug("Set session style")
return nil
} else if s == "DATAGRAM" {
c.Style = s
log.WithField("style", s).Debug("Set session style")
return nil
} else if s == "RAW" {
c.Style = s
log.WithField("style", s).Debug("Set session style")
return nil
}
log.WithField("style", s).Error("Invalid session style")
return fmt.Errorf("Invalid session STYLE=%s, must be STREAM, DATAGRAM, or RAW", s)
}
}
// SetSAMAddress sets the SAM address all-at-once
func SetSAMAddress(s string) func(*SAMEmit) error {
return func(c *SAMEmit) error {
sp := strings.Split(s, ":")
if len(sp) > 2 {
log.WithField("address", s).Error("Invalid SAM address")
return fmt.Errorf("Invalid address string: %s", s)
}
if len(sp) == 2 {
port, err := strconv.Atoi(sp[1])
if err != nil {
log.WithField("port", sp[1]).Error("Invalid SAM port: non-number")
return fmt.Errorf("Invalid SAM port %s; non-number", sp[1])
}
c.I2PConfig.SamPort = port
}
c.I2PConfig.SamHost = sp[0]
log.WithFields(logrus.Fields{
"host": c.I2PConfig.SamHost,
"port": c.I2PConfig.SamPort,
}).Debug("Set SAM address")
return nil
}
}
// SetSAMHost sets the host of the SAMEmit's SAM bridge
func SetSAMHost(s string) func(*SAMEmit) error {
return func(c *SAMEmit) error {
c.I2PConfig.SamHost = s
log.WithField("host", s).Debug("Set SAM host")
return nil
}
}
// SetSAMPort sets the port of the SAMEmit's SAM bridge using a string
func SetSAMPort(s string) func(*SAMEmit) error {
return func(c *SAMEmit) error {
port, err := strconv.Atoi(s)
if err != nil {
log.WithField("port", s).Error("Invalid SAM port: non-number")
return fmt.Errorf("Invalid SAM port %s; non-number", s)
}
if port < 65536 && port > -1 {
c.I2PConfig.SamPort = port
log.WithField("port", s).Debug("Set SAM port")
return nil
}
log.WithField("port", port).Error("Invalid SAM port")
return fmt.Errorf("Invalid SAM port: out of range")
}
}
// SetName sets the host of the SAMEmit's SAM bridge
func SetName(s string) func(*SAMEmit) error {
return func(c *SAMEmit) error {
c.I2PConfig.TunName = s
log.WithField("name", s).Debug("Set tunnel name")
return nil
}
}
// SetInLength sets the number of hops inbound
func SetInLength(u int) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if u < 7 && u >= 0 {
c.I2PConfig.InLength = u
log.WithField("inLength", u).Debug("Set inbound tunnel length")
return nil
}
log.WithField("inLength", u).Error("Invalid inbound tunnel length")
return fmt.Errorf("Invalid inbound tunnel length: out of range")
}
}
// SetOutLength sets the number of hops outbound
func SetOutLength(u int) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if u < 7 && u >= 0 {
c.I2PConfig.OutLength = u
log.WithField("outLength", u).Debug("Set outbound tunnel length")
return nil
}
log.WithField("outLength", u).Error("Invalid outbound tunnel length")
return fmt.Errorf("Invalid outbound tunnel length: out of range")
}
}
// SetInVariance sets the variance of a number of hops inbound
func SetInVariance(i int) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if i < 7 && i > -7 {
c.I2PConfig.InVariance = i
log.WithField("inVariance", i).Debug("Set inbound tunnel variance")
return nil
}
log.WithField("inVariance", i).Error("Invalid inbound tunnel variance")
return fmt.Errorf("Invalid inbound tunnel variance: out of range")
}
}
// SetOutVariance sets the variance of a number of hops outbound
func SetOutVariance(i int) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if i < 7 && i > -7 {
c.I2PConfig.OutVariance = i
log.WithField("outVariance", i).Debug("Set outbound tunnel variance")
return nil
}
log.WithField("outVariance", i).Error("Invalid outbound tunnel variance")
return fmt.Errorf("Invalid outbound tunnel variance: out of range")
}
}
// SetInQuantity sets the inbound tunnel quantity
func SetInQuantity(u int) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if u <= 16 && u > 0 {
c.I2PConfig.InQuantity = u
log.WithField("inQuantity", u).Debug("Set inbound tunnel quantity")
return nil
}
log.WithField("inQuantity", u).Error("Invalid inbound tunnel quantity")
return fmt.Errorf("Invalid inbound tunnel quantity: out of range")
}
}
// SetOutQuantity sets the outbound tunnel quantity
func SetOutQuantity(u int) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if u <= 16 && u > 0 {
c.I2PConfig.OutQuantity = u
log.WithField("outQuantity", u).Debug("Set outbound tunnel quantity")
return nil
}
log.WithField("outQuantity", u).Error("Invalid outbound tunnel quantity")
return fmt.Errorf("Invalid outbound tunnel quantity: out of range")
}
}
// SetInBackups sets the inbound tunnel backups
func SetInBackups(u int) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if u < 6 && u >= 0 {
c.I2PConfig.InBackupQuantity = u
log.WithField("inBackups", u).Debug("Set inbound tunnel backups")
return nil
}
log.WithField("inBackups", u).Error("Invalid inbound tunnel backup quantity")
return fmt.Errorf("Invalid inbound tunnel backup quantity: out of range")
}
}
// SetOutBackups sets the inbound tunnel backups
func SetOutBackups(u int) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if u < 6 && u >= 0 {
c.I2PConfig.OutBackupQuantity = u
log.WithField("outBackups", u).Debug("Set outbound tunnel backups")
return nil
}
log.WithField("outBackups", u).Error("Invalid outbound tunnel backup quantity")
return fmt.Errorf("Invalid outbound tunnel backup quantity: out of range")
}
}
// SetEncrypt tells the router to use an encrypted leaseset
func SetEncrypt(b bool) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if b {
c.I2PConfig.EncryptLeaseSet = true
return nil
}
c.I2PConfig.EncryptLeaseSet = false
log.WithField("encrypt", b).Debug("Set lease set encryption")
return nil
}
}
// SetLeaseSetKey sets the host of the SAMEmit's SAM bridge
func SetLeaseSetKey(s string) func(*SAMEmit) error {
return func(c *SAMEmit) error {
c.I2PConfig.LeaseSetKey = s
log.WithField("leaseSetKey", s).Debug("Set lease set key")
return nil
}
}
// SetLeaseSetPrivateKey sets the host of the SAMEmit's SAM bridge
func SetLeaseSetPrivateKey(s string) func(*SAMEmit) error {
return func(c *SAMEmit) error {
c.I2PConfig.LeaseSetPrivateKey = s
log.WithField("leaseSetPrivateKey", s).Debug("Set lease set private key")
return nil
}
}
// SetLeaseSetPrivateSigningKey sets the host of the SAMEmit's SAM bridge
func SetLeaseSetPrivateSigningKey(s string) func(*SAMEmit) error {
return func(c *SAMEmit) error {
c.I2PConfig.LeaseSetPrivateSigningKey = s
log.WithField("leaseSetPrivateSigningKey", s).Debug("Set lease set private signing key")
return nil
}
}
// SetMessageReliability sets the host of the SAMEmit's SAM bridge
func SetMessageReliability(s string) func(*SAMEmit) error {
return func(c *SAMEmit) error {
c.I2PConfig.MessageReliability = s
log.WithField("messageReliability", s).Debug("Set message reliability")
return nil
}
}
// SetAllowZeroIn tells the tunnel to accept zero-hop peers
func SetAllowZeroIn(b bool) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if b {
c.I2PConfig.InAllowZeroHop = true
return nil
}
c.I2PConfig.InAllowZeroHop = false
log.WithField("allowZeroIn", b).Debug("Set allow zero-hop inbound")
return nil
}
}
// SetAllowZeroOut tells the tunnel to accept zero-hop peers
func SetAllowZeroOut(b bool) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if b {
c.I2PConfig.OutAllowZeroHop = true
return nil
}
c.I2PConfig.OutAllowZeroHop = false
log.WithField("allowZeroOut", b).Debug("Set allow zero-hop outbound")
return nil
}
}
// SetCompress tells clients to use compression
func SetCompress(b bool) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if b {
c.I2PConfig.UseCompression = true
return nil
}
c.I2PConfig.UseCompression = false
log.WithField("compress", b).Debug("Set compression")
return nil
}
}
// SetFastRecieve tells clients to use compression
func SetFastRecieve(b bool) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if b {
c.I2PConfig.FastRecieve = true
return nil
}
c.I2PConfig.FastRecieve = false
log.WithField("fastReceive", b).Debug("Set fast receive")
return nil
}
}
// SetReduceIdle tells the connection to reduce it's tunnels during extended idle time.
func SetReduceIdle(b bool) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if b {
c.I2PConfig.ReduceIdle = true
return nil
}
c.I2PConfig.ReduceIdle = false
log.WithField("reduceIdle", b).Debug("Set reduce idle")
return nil
}
}
// SetReduceIdleTime sets the time to wait before reducing tunnels to idle levels
func SetReduceIdleTime(u int) func(*SAMEmit) error {
return func(c *SAMEmit) error {
c.I2PConfig.ReduceIdleTime = 300000
if u >= 6 {
idleTime := (u * 60) * 1000
c.I2PConfig.ReduceIdleTime = idleTime
log.WithField("reduceIdleTime", idleTime).Debug("Set reduce idle time")
return nil
}
log.WithField("minutes", u).Error("Invalid reduce idle timeout")
return fmt.Errorf("Invalid reduce idle timeout (Measured in minutes) %v", u)
}
}
// SetReduceIdleTimeMs sets the time to wait before reducing tunnels to idle levels in milliseconds
func SetReduceIdleTimeMs(u int) func(*SAMEmit) error {
return func(c *SAMEmit) error {
c.I2PConfig.ReduceIdleTime = 300000
if u >= 300000 {
c.I2PConfig.ReduceIdleTime = u
log.WithField("reduceIdleTimeMs", u).Debug("Set reduce idle time in milliseconds")
return nil
}
log.WithField("milliseconds", u).Error("Invalid reduce idle timeout")
return fmt.Errorf("Invalid reduce idle timeout (Measured in milliseconds) %v", u)
}
}
// SetReduceIdleQuantity sets minimum number of tunnels to reduce to during idle time
func SetReduceIdleQuantity(u int) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if u < 5 {
c.I2PConfig.ReduceIdleQuantity = u
log.WithField("reduceIdleQuantity", u).Debug("Set reduce idle quantity")
return nil
}
log.WithField("quantity", u).Error("Invalid reduce tunnel quantity")
return fmt.Errorf("Invalid reduce idle tunnel quantity: out of range")
}
}
// SetCloseIdle tells the connection to close it's tunnels during extended idle time.
func SetCloseIdle(b bool) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if b {
c.I2PConfig.CloseIdle = true
return nil
}
c.I2PConfig.CloseIdle = false
return nil
}
}
// SetCloseIdleTime sets the time to wait before closing tunnels to idle levels
func SetCloseIdleTime(u int) func(*SAMEmit) error {
return func(c *SAMEmit) error {
c.I2PConfig.CloseIdleTime = 300000
if u >= 6 {
idleTime := (u * 60) * 1000
c.I2PConfig.CloseIdleTime = idleTime
log.WithFields(logrus.Fields{
"minutes": u,
"milliseconds": idleTime,
}).Debug("Set close idle time")
return nil
}
log.WithField("minutes", u).Error("Invalid close idle timeout")
return fmt.Errorf("Invalid close idle timeout (Measured in minutes) %v", u)
}
}
// SetCloseIdleTimeMs sets the time to wait before closing tunnels to idle levels in milliseconds
func SetCloseIdleTimeMs(u int) func(*SAMEmit) error {
return func(c *SAMEmit) error {
c.I2PConfig.CloseIdleTime = 300000
if u >= 300000 {
c.I2PConfig.CloseIdleTime = u
log.WithField("closeIdleTimeMs", u).Debug("Set close idle time in milliseconds")
return nil
}
return fmt.Errorf("Invalid close idle timeout (Measured in milliseconds) %v", u)
}
}
// SetAccessListType tells the system to treat the AccessList as a whitelist
func SetAccessListType(s string) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if s == "whitelist" {
c.I2PConfig.AccessListType = "whitelist"
log.Debug("Set access list type to whitelist")
return nil
} else if s == "blacklist" {
c.I2PConfig.AccessListType = "blacklist"
log.Debug("Set access list type to blacklist")
return nil
} else if s == "none" {
c.I2PConfig.AccessListType = ""
log.Debug("Set access list type to none")
return nil
} else if s == "" {
c.I2PConfig.AccessListType = ""
log.Debug("Set access list type to none")
return nil
}
return fmt.Errorf("Invalid Access list type (whitelist, blacklist, none)")
}
}
// SetAccessList tells the system to treat the AccessList as a whitelist
func SetAccessList(s []string) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if len(s) > 0 {
for _, a := range s {
c.I2PConfig.AccessList = append(c.I2PConfig.AccessList, a)
}
log.WithField("accessList", s).Debug("Set access list")
return nil
}
log.Debug("No access list set (empty list provided)")
return nil
}
}

142
emit.go
View File

@@ -1,142 +0,0 @@
// package sam3 wraps the original sam3 API from github.com/go-i2p/sam3
package sam3
import (
"fmt"
"net"
"strings"
"github.com/go-i2p/go-sam-go/common"
"github.com/sirupsen/logrus"
)
type SAMEmit struct {
common.SAMEmit
}
func (e *SAMEmit) SamOptionsString() string {
optStr := strings.Join(e.I2PConfig.Print(), " ")
log.WithField("optStr", optStr).Debug("Generated option string")
return optStr
}
func (e *SAMEmit) Hello() string {
hello := fmt.Sprintf("HELLO VERSION MIN=%s MAX=%s \n", e.I2PConfig.MinSAM(), e.I2PConfig.MaxSAM())
log.WithField("hello", hello).Debug("Generated HELLO command")
return hello
}
func (e *SAMEmit) HelloBytes() []byte {
return []byte(e.Hello())
}
func (e *SAMEmit) GenerateDestination() string {
dest := fmt.Sprintf("DEST GENERATE %s \n", e.I2PConfig.SignatureType())
log.WithField("destination", dest).Debug("Generated DEST GENERATE command")
return dest
}
func (e *SAMEmit) GenerateDestinationBytes() []byte {
return []byte(e.GenerateDestination())
}
func (e *SAMEmit) Lookup(name string) string {
lookup := fmt.Sprintf("NAMING LOOKUP NAME=%s \n", name)
log.WithField("lookup", lookup).Debug("Generated NAMING LOOKUP command")
return lookup
}
func (e *SAMEmit) LookupBytes(name string) []byte {
return []byte(e.Lookup(name))
}
func (e *SAMEmit) Create() string {
create := fmt.Sprintf(
// //1 2 3 4 5 6 7
"SESSION CREATE %s%s%s%s%s%s%s \n",
e.I2PConfig.SessionStyle(), // 1
e.I2PConfig.FromPort(), // 2
e.I2PConfig.ToPort(), // 3
e.I2PConfig.ID(), // 4
e.I2PConfig.DestinationKey(), // 5
e.I2PConfig.SignatureType(), // 6
e.SamOptionsString(), // 7
)
log.WithField("create", create).Debug("Generated SESSION CREATE command")
return create
}
func (e *SAMEmit) CreateBytes() []byte {
fmt.Println("sam command: " + e.Create())
return []byte(e.Create())
}
func (e *SAMEmit) Connect(dest string) string {
connect := fmt.Sprintf(
"STREAM CONNECT ID=%s %s %s DESTINATION=%s \n",
e.I2PConfig.ID(),
e.I2PConfig.FromPort(),
e.I2PConfig.ToPort(),
dest,
)
log.WithField("connect", connect).Debug("Generated STREAM CONNECT command")
return connect
}
func (e *SAMEmit) ConnectBytes(dest string) []byte {
return []byte(e.Connect(dest))
}
func (e *SAMEmit) Accept() string {
accept := fmt.Sprintf(
"STREAM ACCEPT ID=%s %s %s",
e.I2PConfig.ID(),
e.I2PConfig.FromPort(),
e.I2PConfig.ToPort(),
)
log.WithField("accept", accept).Debug("Generated STREAM ACCEPT command")
return accept
}
func (e *SAMEmit) AcceptBytes() []byte {
return []byte(e.Accept())
}
func NewEmit(opts ...func(*SAMEmit) error) (*SAMEmit, error) {
var emit SAMEmit
for _, o := range opts {
if err := o(&emit); err != nil {
log.WithError(err).Error("Failed to apply option")
return nil, err
}
}
log.Debug("New SAMEmit instance created")
return &emit, nil
}
func IgnorePortError(err error) error {
if err == nil {
return nil
}
if strings.Contains(err.Error(), "missing port in address") {
log.Debug("Ignoring 'missing port in address' error")
err = nil
}
return err
}
func SplitHostPort(hostport string) (string, string, error) {
host, port, err := net.SplitHostPort(hostport)
if err != nil {
if IgnorePortError(err) == nil {
log.WithField("host", hostport).Debug("Using full string as host, port set to 0")
host = hostport
port = "0"
}
}
log.WithFields(logrus.Fields{
"host": host,
"port": port,
}).Debug("Split host and port")
return host, port, nil
}

11
go.mod
View File

@@ -4,13 +4,16 @@ go 1.23.5
require (
github.com/go-i2p/i2pkeys v0.33.92
github.com/go-i2p/logger v0.0.0-20241123010126-3050657e5d0c
github.com/samber/oops v1.18.0
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.7.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/oklog/ulid/v2 v2.1.0 // indirect
github.com/samber/lo v1.50.0 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
golang.org/x/sys v0.30.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
golang.org/x/text v0.22.0 // indirect
)

25
go.sum
View File

@@ -3,18 +3,35 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-i2p/i2pkeys v0.33.92 h1:e2vx3vf7tNesaJ8HmAlGPOcfiGM86jzeIGxh27I9J2Y=
github.com/go-i2p/i2pkeys v0.33.92/go.mod h1:BRURQ/twxV0WKjZlFSKki93ivBi+MirZPWudfwTzMpE=
github.com/go-i2p/logger v0.0.0-20241123010126-3050657e5d0c h1:VTiECn3dFEmUlZjto+wOwJ7SSJTHPLyNprQMR5HzIMI=
github.com/go-i2p/logger v0.0.0-20241123010126-3050657e5d0c/go.mod h1:te7Zj3g3oMeIl8uBXAgO62UKmZ6m6kHRNg1Mm+X8Hzk=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
github.com/samber/oops v1.18.0 h1:NnoCdxlOg/ajFos8HIC0+dV8S6cZRcrjW1WrfZe+GOc=
github.com/samber/oops v1.18.0/go.mod h1:DcZbba2s+PzSx14vY6HjvhV1FDsGOZ1TJg7T/ZZARBQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

52
log.go
View File

@@ -1,52 +0,0 @@
// package sam3 wraps the original sam3 API from github.com/go-i2p/sam3
package sam3
import (
"io"
"os"
"strings"
"sync"
"github.com/sirupsen/logrus"
)
var (
log *logrus.Logger
once sync.Once
)
func InitializeSAM3Logger() {
once.Do(func() {
log = logrus.New()
// We do not want to log by default
log.SetOutput(io.Discard)
log.SetLevel(logrus.PanicLevel)
// Check if DEBUG_I2P is set
if logLevel := os.Getenv("DEBUG_I2P"); logLevel != "" {
log.SetOutput(os.Stdout)
switch strings.ToLower(logLevel) {
case "debug":
log.SetLevel(logrus.DebugLevel)
case "warn":
log.SetLevel(logrus.WarnLevel)
case "error":
log.SetLevel(logrus.ErrorLevel)
default:
log.SetLevel(logrus.DebugLevel)
}
log.WithField("level", log.GetLevel()).Debug("Logging enabled.")
}
})
}
// GetSAM3Logger returns the initialized logger
func GetSAM3Logger() *logrus.Logger {
if log == nil {
InitializeSAM3Logger()
}
return log
}
func init() {
InitializeSAM3Logger()
}

View File

@@ -1,51 +0,0 @@
package logger
import (
"io"
"os"
"strings"
"sync"
"github.com/sirupsen/logrus"
)
var (
log *logrus.Logger
once sync.Once
)
func InitializeSAM3Logger() {
once.Do(func() {
log = logrus.New()
// We do not want to log by default
log.SetOutput(io.Discard)
log.SetLevel(logrus.PanicLevel)
// Check if DEBUG_I2P is set
if logLevel := os.Getenv("DEBUG_I2P"); logLevel != "" {
log.SetOutput(os.Stdout)
switch strings.ToLower(logLevel) {
case "debug":
log.SetLevel(logrus.DebugLevel)
case "warn":
log.SetLevel(logrus.WarnLevel)
case "error":
log.SetLevel(logrus.ErrorLevel)
default:
log.SetLevel(logrus.DebugLevel)
}
log.WithField("level", log.GetLevel()).Debug("Logging enabled.")
}
})
}
// GetSAM3Logger returns the initialized logger
func GetSAM3Logger() *logrus.Logger {
if log == nil {
InitializeSAM3Logger()
}
return log
}
func init() {
InitializeSAM3Logger()
}

View File

@@ -1,28 +0,0 @@
// package sam3 wraps the original sam3 API from github.com/go-i2p/sam3
package sam3
import (
"github.com/go-i2p/go-sam-go/primary"
)
const (
session_ADDOK = "SESSION STATUS RESULT=OK"
)
// Represents a primary session.
type PrimarySession struct {
*primary.PrimarySession
}
var PrimarySessionSwitch = "MASTER"
func (p *PrimarySession) NewStreamSubSession(id string) (*StreamSession, error) {
log.WithField("id", id).Debug("NewStreamSubSession called")
session, err := p.PrimarySession.NewStreamSubSession(id)
if err != nil {
return nil, err
}
return &StreamSession{
StreamSession: session,
}, nil
}

6
primary/DOC.md Normal file
View File

@@ -0,0 +1,6 @@
# primary
--
import "github.com/go-i2p/go-sam-go/primary"
## Usage

View File

@@ -1,3 +0,0 @@
package primary
const SESSION_ADDOK = "SESSION STATUS RESULT=OK"

View File

@@ -1,73 +0,0 @@
package primary
import (
"errors"
"net"
"strconv"
"github.com/go-i2p/go-sam-go/common"
"github.com/go-i2p/go-sam-go/datagram"
"github.com/sirupsen/logrus"
)
// Creates a new datagram session. udpPort is the UDP port SAM is listening on,
// and if you set it to zero, it will use SAMs standard UDP port.
func (s *PrimarySession) NewDatagramSubSession(id string, udpPort int) (*datagram.DatagramSession, error) {
log.WithFields(logrus.Fields{"id": id, "udpPort": udpPort}).Debug("NewDatagramSubSession called")
if udpPort > 65335 || udpPort < 0 {
log.WithField("udpPort", udpPort).Error("Invalid UDP port")
return nil, errors.New("udpPort needs to be in the intervall 0-65335")
}
if udpPort == 0 {
udpPort = 7655
log.Debug("Using default UDP port 7655")
}
lhost, _, err := common.SplitHostPort(s.conn.LocalAddr().String())
if err != nil {
log.WithError(err).Error("Failed to split local host port")
s.Close()
return nil, err
}
lUDPAddr, err := net.ResolveUDPAddr("udp4", lhost+":0")
if err != nil {
log.WithError(err).Error("Failed to resolve local UDP address")
return nil, err
}
udpconn, err := net.ListenUDP("udp4", lUDPAddr)
if err != nil {
log.WithError(err).Error("Failed to listen on UDP")
return nil, err
}
rhost, _, err := common.SplitHostPort(s.conn.RemoteAddr().String())
if err != nil {
log.WithError(err).Error("Failed to split remote host port")
s.Close()
return nil, err
}
rUDPAddr, err := net.ResolveUDPAddr("udp4", rhost+":"+strconv.Itoa(udpPort))
if err != nil {
log.WithError(err).Error("Failed to resolve remote UDP address")
return nil, err
}
_, lport, err := net.SplitHostPort(udpconn.LocalAddr().String())
if err != nil {
log.WithError(err).Error("Failed to get local port")
s.Close()
return nil, err
}
conn, err := s.NewGenericSubSession("DATAGRAM", id, []string{"PORT=" + lport})
if err != nil {
log.WithError(err).Error("Failed to create new generic sub-session")
return nil, err
}
log.WithFields(logrus.Fields{"id": id, "localPort": lport}).Debug("Created new datagram sub-session")
datagramSession := &datagram.DatagramSession{
SAM: (*datagram.SAM)(s.SAM),
SAMUDPAddress: rUDPAddr,
UDPConn: udpconn,
RemoteI2PAddr: nil,
}
datagramSession.Conn = conn
return datagramSession, nil
}

View File

@@ -1,105 +0,0 @@
package primary
import (
"fmt"
"net"
"strings"
"github.com/go-i2p/go-sam-go/common"
"github.com/go-i2p/go-sam-go/datagram"
"github.com/sirupsen/logrus"
)
func (sam *PrimarySession) Dial(network, addr string) (net.Conn, error) {
log.WithFields(logrus.Fields{"network": network, "addr": addr}).Debug("Dial() called")
if network == "udp" || network == "udp4" || network == "udp6" {
// return sam.DialUDPI2P(network, network+addr[0:4], addr)
return sam.DialUDPI2P(network, network+addr[0:4], addr)
}
if network == "tcp" || network == "tcp4" || network == "tcp6" {
// return sam.DialTCPI2P(network, network+addr[0:4], addr)
return sam.DialTCPI2P(network, network+addr[0:4], addr)
}
log.WithField("network", network).Error("Invalid network type")
return nil, fmt.Errorf("Error: Must specify a valid network type")
}
// DialTCP implements x/dialer
func (sam *PrimarySession) DialTCP(network string, laddr, raddr net.Addr) (net.Conn, error) {
log.WithFields(logrus.Fields{"network": network, "laddr": laddr, "raddr": raddr}).Debug("DialTCP() called")
ts, ok := sam.stsess[network+raddr.String()[0:4]]
var err error
if !ok {
ts, err = sam.NewUniqueStreamSubSession(network + raddr.String()[0:4])
if err != nil {
log.WithError(err).Error("Failed to create new unique stream sub-session")
return nil, err
}
sam.stsess[network+raddr.String()[0:4]] = ts
ts, _ = sam.stsess[network+raddr.String()[0:4]]
}
return ts.Dial(network, raddr.String())
}
func (sam *PrimarySession) DialTCPI2P(network, laddr, raddr string) (net.Conn, error) {
log.WithFields(logrus.Fields{"network": network, "laddr": laddr, "raddr": raddr}).Debug("DialTCPI2P() called")
ts, ok := sam.stsess[network+raddr[0:4]]
var err error
if !ok {
ts, err = sam.NewUniqueStreamSubSession(network + laddr)
if err != nil {
log.WithError(err).Error("Failed to create new unique stream sub-session")
return nil, err
}
sam.stsess[network+raddr[0:4]] = ts
ts, _ = sam.stsess[network+raddr[0:4]]
}
return ts.Dial(network, raddr)
}
// DialUDP implements x/dialer
func (sam *PrimarySession) DialUDP(network string, laddr, raddr net.Addr) (net.PacketConn, error) {
log.WithFields(logrus.Fields{"network": network, "laddr": laddr, "raddr": raddr}).Debug("DialUDP() called")
ds, ok := sam.dgsess[network+raddr.String()[0:4]]
var err error
if !ok {
ds, err = sam.NewDatagramSubSession(network+raddr.String()[0:4], 0)
if err != nil {
log.WithError(err).Error("Failed to create new datagram sub-session")
return nil, err
}
sam.dgsess[network+raddr.String()[0:4]] = ds
ds, _ = sam.dgsess[network+raddr.String()[0:4]]
}
return ds.Dial(network, raddr.String())
}
func (sam *PrimarySession) DialUDPI2P(network, laddr, raddr string) (*datagram.DatagramSession, error) {
log.WithFields(logrus.Fields{"network": network, "laddr": laddr, "raddr": raddr}).Debug("DialUDPI2P() called")
ds, ok := sam.dgsess[network+raddr[0:4]]
var err error
if !ok {
ds, err = sam.NewDatagramSubSession(network+laddr, 0)
if err != nil {
log.WithError(err).Error("Failed to create new datagram sub-session")
return nil, err
}
sam.dgsess[network+raddr[0:4]] = ds
ds, _ = sam.dgsess[network+raddr[0:4]]
}
return ds.Dial(network, raddr)
}
func (s *PrimarySession) Lookup(name string) (a net.Addr, err error) {
log.WithField("name", name).Debug("Lookup() called")
var sam *common.SAM
name = strings.Split(name, ":")[0]
sam, err = common.NewSAM(s.samAddr)
if err == nil {
log.WithField("addr", a).Debug("Lookup successful")
defer sam.Close()
a, err = sam.Lookup(name)
}
log.WithError(err).Error("Lookup failed")
return
}

View File

@@ -1,101 +0,0 @@
package primary
import (
"errors"
"net"
"strings"
"github.com/sirupsen/logrus"
"github.com/go-i2p/go-sam-go/common"
)
// Creates a new session with the style of either "STREAM", "DATAGRAM" or "RAW",
// for a new I2P tunnel with name id, using the cypher keys specified, with the
// I2CP/streaminglib-options as specified. Extra arguments can be specified by
// setting extra to something else than []string{}.
// This sam3 instance is now a session
func (sam *PrimarySession) NewGenericSubSession(style, id string, extras []string) (net.Conn, error) {
log.WithFields(logrus.Fields{"style": style, "id": id, "extras": extras}).Debug("newGenericSubSession called")
return sam.NewGenericSubSessionWithSignature(style, id, extras)
}
func (sam *PrimarySession) NewGenericSubSessionWithSignature(style, id string, extras []string) (net.Conn, error) {
log.WithFields(logrus.Fields{"style": style, "id": id, "extras": extras}).Debug("newGenericSubSessionWithSignature called")
return sam.NewGenericSubSessionWithSignatureAndPorts(style, id, "0", "0", extras)
}
// Creates a new session with the style of either "STREAM", "DATAGRAM" or "RAW",
// for a new I2P tunnel with name id, using the cypher keys specified, with the
// I2CP/streaminglib-options as specified. Extra arguments can be specified by
// setting extra to something else than []string{}.
// This sam3 instance is now a session
func (sam *PrimarySession) NewGenericSubSessionWithSignatureAndPorts(style, id, from, to string, extras []string) (net.Conn, error) {
log.WithFields(logrus.Fields{"style": style, "id": id, "from": from, "to": to, "extras": extras}).Debug("newGenericSubSessionWithSignatureAndPorts called")
conn := sam.conn
fp := ""
tp := ""
if from != "0" && from != "" {
fp = " FROM_PORT=" + from
}
if to != "0" && to != "" {
tp = " TO_PORT=" + to
}
scmsg := []byte("SESSION ADD STYLE=" + style + " ID=" + id + fp + tp + " " + strings.Join(extras, " ") + "\n")
log.WithField("message", string(scmsg)).Debug("Sending SESSION ADD message")
for m, i := 0, 0; m != len(scmsg); i++ {
if i == 15 {
conn.Close()
log.Error("Writing to SAM failed after 15 attempts")
return nil, errors.New("writing to SAM failed")
}
n, err := conn.Write(scmsg[m:])
if err != nil {
log.WithError(err).Error("Failed to write to SAM connection")
conn.Close()
return nil, err
}
m += n
}
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
log.WithError(err).Error("Failed to read from SAM connection")
conn.Close()
return nil, err
}
text := string(buf[:n])
log.WithField("response", text).Debug("Received response from SAM")
// log.Println("SAM:", text)
if strings.HasPrefix(text, SESSION_ADDOK) {
//if sam.keys.String() != text[len(common.SESSION_ADDOK):len(text)-1] {
//conn.Close()
//return nil, errors.New("SAMv3 created a tunnel with keys other than the ones we asked it for")
//}
log.Debug("Session added successfully")
return conn, nil //&StreamSession{id, conn, keys, nil, sync.RWMutex{}, nil}, nil
} else if text == common.SESSION_DUPLICATE_ID {
log.Error("Duplicate tunnel name")
conn.Close()
return nil, errors.New("Duplicate tunnel name")
} else if text == common.SESSION_DUPLICATE_DEST {
log.Error("Duplicate destination")
conn.Close()
return nil, errors.New("Duplicate destination")
} else if text == common.SESSION_INVALID_KEY {
log.Error("Invalid key - Primary Session")
conn.Close()
return nil, errors.New("Invalid key - Primary Session")
} else if strings.HasPrefix(text, common.SESSION_I2P_ERROR) {
log.WithField("error", text[len(common.SESSION_I2P_ERROR):]).Error("I2P error")
conn.Close()
return nil, errors.New("I2P error " + text[len(common.SESSION_I2P_ERROR):])
} else {
log.WithField("reply", text).Error("Unable to parse SAMv3 reply")
conn.Close()
return nil, errors.New("Unable to parse SAMv3 reply: " + text)
}
}

View File

@@ -1,10 +0,0 @@
package primary
import logger "github.com/go-i2p/go-sam-go/logger"
var log = logger.GetSAM3Logger()
func init() {
logger.InitializeSAM3Logger()
log = logger.GetSAM3Logger()
}

View File

@@ -1,76 +0,0 @@
package primary
import (
"time"
"github.com/go-i2p/go-sam-go/common"
"github.com/go-i2p/go-sam-go/datagram"
"github.com/go-i2p/go-sam-go/stream"
"github.com/go-i2p/i2pkeys"
"github.com/sirupsen/logrus"
)
var PrimarySessionSwitch string = "MASTER"
// Creates a new PrimarySession with the I2CP- and streaminglib options as
// specified. See the I2P documentation for a full list of options.
func (sam *SAM) NewPrimarySession(id string, keys i2pkeys.I2PKeys, options []string) (*PrimarySession, error) {
log.WithFields(logrus.Fields{"id": id, "options": options}).Debug("NewPrimarySession() called")
return sam.newPrimarySession(PrimarySessionSwitch, id, keys, options)
}
func (sam *SAM) newPrimarySession(primarySessionSwitch, id string, keys i2pkeys.I2PKeys, options []string) (*PrimarySession, error) {
log.WithFields(logrus.Fields{
"primarySessionSwitch": primarySessionSwitch,
"id": id,
"options": options,
}).Debug("newPrimarySession() called")
conn, err := sam.NewGenericSession(primarySessionSwitch, id, keys, options)
if err != nil {
log.WithError(err).Error("Failed to create new generic session")
return nil, err
}
return &PrimarySession{
SAM: sam,
samAddr: "",
id: id,
conn: conn,
keys: keys,
Timeout: 0,
Deadline: time.Time{},
sigType: "",
Config: common.SAMEmit{},
stsess: map[string]*stream.StreamSession{},
dgsess: map[string]*datagram.DatagramSession{},
}, nil
}
// Creates a new PrimarySession with the I2CP- and PRIMARYinglib options as
// specified. See the I2P documentation for a full list of options.
func (sam *SAM) NewPrimarySessionWithSignature(id string, keys i2pkeys.I2PKeys, options []string, sigType string) (*PrimarySession, error) {
log.WithFields(logrus.Fields{
"id": id,
"options": options,
"sigType": sigType,
}).Debug("NewPrimarySessionWithSignature() called")
conn, err := sam.NewGenericSessionWithSignature(PrimarySessionSwitch, id, keys, sigType, options)
if err != nil {
log.WithError(err).Error("Failed to create new generic session with signature")
return nil, err
}
return &PrimarySession{
SAM: sam,
samAddr: "",
id: id,
conn: conn,
keys: keys,
Timeout: 0,
Deadline: time.Time{},
sigType: sigType,
Config: common.SAMEmit{},
stsess: map[string]*stream.StreamSession{},
dgsess: map[string]*datagram.DatagramSession{},
}, nil
}

View File

@@ -1,74 +0,0 @@
package primary
import (
"errors"
"net"
"strconv"
"github.com/go-i2p/go-sam-go/common"
"github.com/go-i2p/go-sam-go/raw"
"github.com/sirupsen/logrus"
)
// Creates a new raw session. udpPort is the UDP port SAM is listening on,
// and if you set it to zero, it will use SAMs standard UDP port.
func (s *PrimarySession) NewRawSubSession(id string, udpPort int) (*raw.RawSession, error) {
log.WithFields(logrus.Fields{"id": id, "udpPort": udpPort}).Debug("NewRawSubSession called")
if udpPort > 65335 || udpPort < 0 {
log.WithField("udpPort", udpPort).Error("Invalid UDP port")
return nil, errors.New("udpPort needs to be in the intervall 0-65335")
}
if udpPort == 0 {
udpPort = 7655
log.Debug("Using default UDP port 7655")
}
lhost, _, err := common.SplitHostPort(s.conn.LocalAddr().String())
if err != nil {
log.WithError(err).Error("Failed to split local host port")
s.Close()
return nil, err
}
lUDPAddr, err := net.ResolveUDPAddr("udp4", lhost+":0")
if err != nil {
log.WithError(err).Error("Failed to resolve local UDP address")
return nil, err
}
udpconn, err := net.ListenUDP("udp4", lUDPAddr)
if err != nil {
log.WithError(err).Error("Failed to listen on UDP")
return nil, err
}
rhost, _, err := common.SplitHostPort(s.conn.RemoteAddr().String())
if err != nil {
log.WithError(err).Error("Failed to split remote host port")
s.Close()
return nil, err
}
rUDPAddr, err := net.ResolveUDPAddr("udp4", rhost+":"+strconv.Itoa(udpPort))
if err != nil {
log.WithError(err).Error("Failed to resolve remote UDP address")
return nil, err
}
_, lport, err := net.SplitHostPort(udpconn.LocalAddr().String())
if err != nil {
log.WithError(err).Error("Failed to get local port")
s.Close()
return nil, err
}
// conn, err := s.newGenericSubSession("RAW", id, s.keys, options, []string{"PORT=" + lport})
conn, err := s.NewGenericSubSession("RAW", id, []string{"PORT=" + lport})
if err != nil {
log.WithError(err).Error("Failed to create new generic sub-session")
return nil, err
}
log.WithFields(logrus.Fields{"id": id, "localPort": lport}).Debug("Created new raw sub-session")
rawSession := &raw.RawSession{
SAM: (*raw.SAM)(s.SAM),
SAMUDPConn: udpconn,
SAMUDPAddr: rUDPAddr,
}
rawSession.Conn = conn
return rawSession, nil
}

View File

@@ -1,59 +0,0 @@
package primary
import (
"net"
"github.com/go-i2p/go-sam-go/common"
"github.com/go-i2p/go-sam-go/stream"
"github.com/sirupsen/logrus"
)
// Creates a new stream.StreamSession with the I2CP- and streaminglib options as
// specified. See the I2P documentation for a full list of options.
func (sam *PrimarySession) NewStreamSubSession(id string) (*stream.StreamSession, error) {
log.WithField("id", id).Debug("NewStreamSubSession called")
conn, err := sam.NewGenericSubSession("STREAM", id, []string{})
if err != nil {
log.WithError(err).Error("Failed to create new generic sub-session")
return nil, err
}
return newFromPrimary(sam, conn), nil
}
// Creates a new stream.StreamSession with the I2CP- and streaminglib options as
// specified. See the I2P documentation for a full list of options.
func (sam *PrimarySession) NewUniqueStreamSubSession(id string) (*stream.StreamSession, error) {
log.WithField("id", id).Debug("NewUniqueStreamSubSession called")
conn, err := sam.NewGenericSubSession("STREAM", id, []string{})
if err != nil {
log.WithError(err).Error("Failed to create new generic sub-session")
return nil, err
}
fromPort, toPort := common.RandPort(), common.RandPort()
log.WithFields(logrus.Fields{"fromPort": fromPort, "toPort": toPort}).Debug("Generated random ports")
return newFromPrimary(sam, conn), nil
}
// Creates a new stream.StreamSession with the I2CP- and streaminglib options as
// specified. See the I2P documentation for a full list of options.
func (sam *PrimarySession) NewStreamSubSessionWithPorts(id, from, to string) (*stream.StreamSession, error) {
log.WithFields(logrus.Fields{"id": id, "from": from, "to": to}).Debug("NewStreamSubSessionWithPorts called")
conn, err := sam.NewGenericSubSessionWithSignatureAndPorts("STREAM", id, from, to, []string{})
if err != nil {
log.WithError(err).Error("Failed to create new generic sub-session with signature and ports")
return nil, err
}
return newFromPrimary(sam, conn), nil
}
func newFromPrimary(sam *PrimarySession, conn net.Conn) *stream.StreamSession {
streamSession := &stream.StreamSession{
SAM: &stream.SAM{
SAM: (*common.SAM)(sam.SAM),
},
}
streamSession.Conn = conn
return streamSession
}

View File

@@ -1,30 +1 @@
package primary
import (
"net"
"time"
"github.com/go-i2p/go-sam-go/common"
"github.com/go-i2p/go-sam-go/datagram"
"github.com/go-i2p/go-sam-go/stream"
"github.com/go-i2p/i2pkeys"
)
type SAM common.SAM
// Represents a primary session.
type PrimarySession struct {
*SAM
samAddr string // address to the sam bridge (ipv4:port)
id string // tunnel name
conn net.Conn // connection to sam
keys i2pkeys.I2PKeys // i2p destination keys
Timeout time.Duration
Deadline time.Time
sigType string
Config common.SAMEmit
stsess map[string]*stream.StreamSession
dgsess map[string]*datagram.DatagramSession
// from string
// to string
}

View File

@@ -1,149 +0,0 @@
// package sam3 wraps the original sam3 API from github.com/go-i2p/sam3
package sam3
import (
"fmt"
"testing"
"time"
)
func Test_PrimaryDatagramServerClient(t *testing.T) {
if testing.Short() {
return
}
fmt.Println("Test_PrimaryDatagramServerClient")
earlysam, err := NewSAM(yoursam)
if err != nil {
t.Fail()
return
}
defer earlysam.Close()
keys, err := earlysam.NewKeys()
if err != nil {
t.Fail()
return
}
sam, err := earlysam.NewPrimarySession("PrimaryTunnel", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"})
if err != nil {
t.Fail()
return
}
defer sam.Close()
// fmt.Println("\tServer: My address: " + keys.Addr().Base32())
fmt.Println("\tServer: Creating tunnel")
ds, err := sam.NewDatagramSubSession("PrimaryTunnel"+RandString(), 0)
if err != nil {
fmt.Println("Server: Failed to create tunnel: " + err.Error())
t.Fail()
return
}
defer ds.Close()
c, w := make(chan bool), make(chan bool)
go func(c, w chan (bool)) {
sam2, err := NewSAM(yoursam)
if err != nil {
c <- false
return
}
defer sam2.Close()
keys, err := sam2.NewKeys()
if err != nil {
c <- false
return
}
fmt.Println("\tClient: Creating tunnel")
ds2, err := sam2.NewDatagramSession("PRIMARYClientTunnel", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}, 0)
if err != nil {
c <- false
return
}
defer ds2.Close()
// fmt.Println("\tClient: Servers address: " + ds.LocalAddr().Base32())
// fmt.Println("\tClient: Clients address: " + ds2.LocalAddr().Base32())
fmt.Println("\tClient: Tries to send primary to server")
for {
select {
default:
_, err = ds2.WriteTo([]byte("Hello primary-world! <3 <3 <3 <3 <3 <3"), ds.LocalAddr())
if err != nil {
fmt.Println("\tClient: Failed to send primary: " + err.Error())
c <- false
return
}
time.Sleep(5 * time.Second)
case <-w:
fmt.Println("\tClient: Sent primary, quitting.")
return
}
}
c <- true
}(c, w)
buf := make([]byte, 512)
fmt.Println("\tServer: ReadFrom() waiting...")
n, _, err := ds.ReadFrom(buf)
w <- true
if err != nil {
fmt.Println("\tServer: Failed to ReadFrom(): " + err.Error())
t.Fail()
return
}
fmt.Println("\tServer: Received primary: " + string(buf[:n]))
// fmt.Println("\tServer: Senders address was: " + saddr.Base32())
}
func ExamplePrimaryDatagramSession() {
// Creates a new PrimarySession, then creates a Datagram subsession on top of it
const samBridge = "127.0.0.1:7656"
earlysam, err := NewSAM(samBridge)
if err != nil {
fmt.Println(err.Error())
return
}
defer earlysam.Close()
keys, err := earlysam.NewKeys()
if err != nil {
fmt.Println(err.Error())
return
}
myself := keys.Addr()
sam, err := earlysam.NewPrimarySession("PrimaryTunnel", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"})
if err != nil {
fmt.Println(err.Error())
return
}
defer sam.Close()
// See the example Option_* variables.
dg, err := sam.NewDatagramSubSession("DGTUN"+RandString(), 0)
if err != nil {
fmt.Println(err.Error())
return
}
defer dg.Close()
someone, err := earlysam.Lookup("zzz.i2p")
if err != nil {
fmt.Println(err.Error())
return
}
dg.WriteTo([]byte("Hello stranger!"), someone)
dg.WriteTo([]byte("Hello myself!"), myself)
buf := make([]byte, 31*1024)
n, _, err := dg.ReadFrom(buf)
if err != nil {
fmt.Println(err.Error())
return
}
fmt.Println("Got message: '" + string(buf[:n]) + "'")
fmt.Println("Got message: " + string(buf[:n]))
return
// Output:
// Got message: Hello myself!
}

View File

@@ -1,307 +0,0 @@
// package sam3 wraps the original sam3 API from github.com/go-i2p/sam3
package sam3
import (
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"time"
)
func Test_PrimaryStreamingDial(t *testing.T) {
if testing.Short() {
return
}
fmt.Println("Test_PrimaryStreamingDial")
earlysam, err := NewSAM(yoursam)
if err != nil {
t.Fail()
return
}
defer earlysam.Close()
keys, err := earlysam.NewKeys()
if err != nil {
t.Fail()
return
}
sam, err := earlysam.NewPrimarySession("PrimaryTunnel", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"})
if err != nil {
t.Fail()
return
}
defer sam.Close()
fmt.Println("\tBuilding tunnel")
ss, err := sam.NewStreamSubSession("primaryStreamTunnel")
if err != nil {
fmt.Println(err.Error())
t.Fail()
return
}
defer ss.Close()
fmt.Println("\tNotice: This may fail if your I2P node is not well integrated in the I2P network.")
fmt.Println("\tLooking up i2p-projekt.i2p")
forumAddr, err := earlysam.Lookup("i2p-projekt.i2p")
if err != nil {
fmt.Println(err.Error())
t.Fail()
return
}
fmt.Println("\tDialing i2p-projekt.i2p(", forumAddr.Base32(), forumAddr.DestHash().Hash(), ")")
conn, err := ss.DialI2P(forumAddr)
if err != nil {
fmt.Println(err.Error())
t.Fail()
return
}
defer conn.Close()
fmt.Println("\tSending HTTP GET /")
if _, err := conn.Write([]byte("GET /\n")); err != nil {
fmt.Println(err.Error())
t.Fail()
return
}
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if !strings.Contains(strings.ToLower(string(buf[:n])), "http") && !strings.Contains(strings.ToLower(string(buf[:n])), "html") {
fmt.Printf("\tProbably failed to StreamSession.DialI2P(i2p-projekt.i2p)? It replied %d bytes, but nothing that looked like http/html", n)
} else {
fmt.Println("\tRead HTTP/HTML from i2p-projekt.i2p")
}
}
func Test_PrimaryStreamingServerClient(t *testing.T) {
if testing.Short() {
return
}
fmt.Println("Test_StreamingServerClient")
earlysam, err := NewSAM(yoursam)
if err != nil {
t.Fail()
return
}
defer earlysam.Close()
keys, err := earlysam.NewKeys()
if err != nil {
t.Fail()
return
}
sam, err := earlysam.NewPrimarySession("PrimaryServerClientTunnel", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"})
if err != nil {
t.Fail()
return
}
defer sam.Close()
fmt.Println("\tServer: Creating tunnel")
ss, err := sam.NewUniqueStreamSubSession("PrimaryServerClientTunnel")
if err != nil {
return
}
defer ss.Close()
time.Sleep(time.Second * 10)
c, w := make(chan bool), make(chan bool)
go func(c, w chan (bool)) {
if !(<-w) {
return
}
/*
sam2, err := NewSAM(yoursam)
if err != nil {
c <- false
return
}
defer sam2.Close()
keys, err := sam2.NewKeys()
if err != nil {
c <- false
return
}
*/
fmt.Println("\tClient: Creating tunnel")
ss2, err := sam.NewStreamSubSession("primaryExampleClientTun")
if err != nil {
c <- false
return
}
defer ss2.Close()
fmt.Println("\tClient: Connecting to server")
conn, err := ss2.DialI2P(ss.Addr())
if err != nil {
c <- false
return
}
fmt.Println("\tClient: Connected to tunnel")
defer conn.Close()
_, err = conn.Write([]byte("Hello world <3 <3 <3 <3 <3 <3"))
if err != nil {
c <- false
return
}
c <- true
}(c, w)
l, err := ss.Listen()
if err != nil {
fmt.Println("ss.Listen(): " + err.Error())
t.Fail()
w <- false
return
}
defer l.Close()
w <- true
fmt.Println("\tServer: Accept()ing on tunnel")
conn, err := l.Accept()
if err != nil {
t.Fail()
fmt.Println("Failed to Accept(): " + err.Error())
return
}
defer conn.Close()
buf := make([]byte, 512)
n, err := conn.Read(buf)
fmt.Printf("\tClient exited successfully: %t\n", <-c)
fmt.Println("\tServer: received from Client: " + string(buf[:n]))
}
func ExamplePrimaryStreamSession() {
// Creates a new StreamingSession, dials to idk.i2p and gets a SAMConn
// which behaves just like a normal net.Conn.
const samBridge = "127.0.0.1:7656"
earlysam, err := NewSAM(yoursam)
if err != nil {
log.Fatal(err.Error())
return
}
defer earlysam.Close()
keys, err := earlysam.NewKeys()
if err != nil {
log.Fatal(err.Error())
return
}
sam, err := earlysam.NewPrimarySession("PrimaryStreamSessionTunnel", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"})
if err != nil {
log.Fatal(err.Error())
return
}
defer sam.Close()
conn, err := sam.Dial("tcp", "idk.i2p") // someone.Base32())
if err != nil {
fmt.Println(err.Error())
return
}
defer conn.Close()
fmt.Println("Sending HTTP GET /")
if _, err := conn.Write([]byte("GET /\n")); err != nil {
fmt.Println(err.Error())
return
}
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if !strings.Contains(strings.ToLower(string(buf[:n])), "http") && !strings.Contains(strings.ToLower(string(buf[:n])), "html") {
fmt.Printf("Probably failed to StreamSession.DialI2P(idk.i2p)? It replied %d bytes, but nothing that looked like http/html", n)
log.Printf("Probably failed to StreamSession.DialI2P(idk.i2p)? It replied %d bytes, but nothing that looked like http/html", n)
} else {
fmt.Println("Read HTTP/HTML from idk.i2p")
log.Println("Read HTTP/HTML from idk.i2p")
}
// Output:
// Sending HTTP GET /
// Read HTTP/HTML from idk.i2p
}
func ExamplePrimaryStreamListener() {
// One server Accept()ing on a StreamListener, and one client that Dials
// through I2P to the server. Server writes "Hello world!" through a SAMConn
// (which implements net.Conn) and the client prints the message.
const samBridge = "127.0.0.1:7656"
var ss *StreamSession
go func() {
earlysam, err := NewSAM(yoursam)
if err != nil {
log.Fatal(err.Error())
return
}
defer earlysam.Close()
keys, err := earlysam.NewKeys()
if err != nil {
log.Fatal(err.Error())
return
}
sam, err := earlysam.NewPrimarySession("PrimaryListenerTunnel", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"})
if err != nil {
log.Fatal(err.Error())
return
}
defer sam.Close()
ss, err = sam.NewStreamSubSession("PrimaryListenerServerTunnel2")
if err != nil {
fmt.Println(err.Error())
return
}
defer ss.Close()
l, err := ss.Listen()
if err != nil {
fmt.Println(err.Error())
return
}
defer l.Close()
// fmt.Println("Serving on primary listener", l.Addr().String())
if err := http.Serve(l, &exitHandler{}); err != nil {
fmt.Println(err.Error())
}
}()
time.Sleep(time.Second * 10)
latesam, err := NewSAM(yoursam)
if err != nil {
log.Fatal(err.Error())
return
}
defer latesam.Close()
keys2, err := latesam.NewKeys()
if err != nil {
log.Fatal(err.Error())
return
}
sc, err := latesam.NewStreamSession("PrimaryListenerClientTunnel2", keys2, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"})
if err != nil {
fmt.Println(err.Error())
return
}
defer sc.Close()
client := http.Client{
Transport: &http.Transport{
Dial: sc.Dial,
},
}
// resp, err := client.Get("http://" + "idk.i2p") //ss.Addr().Base32())
resp, err := client.Get("http://" + ss.Addr().Base32())
if err != nil {
fmt.Println(err.Error())
return
}
defer resp.Body.Close()
r, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err.Error())
return
}
fmt.Println("Got response: " + string(r))
// Output:
// Got response: Hello world!
}
type exitHandler struct{}
func (e *exitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello world!"))
}

16
raw.go
View File

@@ -1,16 +0,0 @@
// package sam3 wraps the original sam3 API from github.com/go-i2p/sam3
package sam3
import (
"github.com/go-i2p/go-sam-go/raw"
)
// The RawSession provides no authentication of senders, and there is no sender
// address attached to datagrams, so all communication is anonymous. The
// messages send are however still endpoint-to-endpoint encrypted. You
// need to figure out a way to identify and authenticate clients yourself, iff
// that is needed. Raw datagrams may be at most 32 kB in size. There is no
// overhead of authentication, which is the reason to use this..
type RawSession struct {
*raw.RawSession
}

341
raw/DOC.md Normal file
View File

@@ -0,0 +1,341 @@
# raw
--
import "github.com/go-i2p/go-sam-go/raw"
## Usage
#### type RawAddr
```go
type RawAddr struct {
}
```
RawAddr implements net.Addr for I2P raw addresses
#### func (*RawAddr) Network
```go
func (a *RawAddr) Network() string
```
Network returns the network type
#### func (*RawAddr) String
```go
func (a *RawAddr) String() string
```
String returns the string representation of the address
#### type RawConn
```go
type RawConn struct {
}
```
RawConn implements net.PacketConn for I2P raw datagrams
#### func (*RawConn) Close
```go
func (c *RawConn) Close() error
```
Close closes the raw connection
#### func (*RawConn) LocalAddr
```go
func (c *RawConn) LocalAddr() net.Addr
```
LocalAddr returns the local address
#### func (*RawConn) Read
```go
func (c *RawConn) Read(b []byte) (n int, err error)
```
Read implements net.Conn by wrapping ReadFrom
#### func (*RawConn) ReadFrom
```go
func (c *RawConn) ReadFrom(p []byte) (n int, addr net.Addr, err error)
```
ReadFrom reads a raw datagram from the connection
#### func (*RawConn) RemoteAddr
```go
func (c *RawConn) RemoteAddr() net.Addr
```
RemoteAddr returns the remote address of the connection
#### func (*RawConn) SetDeadline
```go
func (c *RawConn) SetDeadline(t time.Time) error
```
SetDeadline sets the read and write deadlines
#### func (*RawConn) SetReadDeadline
```go
func (c *RawConn) SetReadDeadline(t time.Time) error
```
SetReadDeadline sets the deadline for future ReadFrom calls
#### func (*RawConn) SetWriteDeadline
```go
func (c *RawConn) SetWriteDeadline(t time.Time) error
```
SetWriteDeadline sets the deadline for future WriteTo calls
#### func (*RawConn) Write
```go
func (c *RawConn) Write(b []byte) (n int, err error)
```
Write implements net.Conn by wrapping WriteTo
#### func (*RawConn) WriteTo
```go
func (c *RawConn) WriteTo(p []byte, addr net.Addr) (n int, err error)
```
WriteTo writes a raw datagram to the specified address
#### type RawDatagram
```go
type RawDatagram struct {
Data []byte
Source i2pkeys.I2PAddr
Local i2pkeys.I2PAddr
}
```
RawDatagram represents an I2P raw datagram message
#### type RawListener
```go
type RawListener struct {
}
```
RawListener implements net.Listener for I2P raw connections
#### func (*RawListener) Accept
```go
func (l *RawListener) Accept() (net.Conn, error)
```
Accept waits for and returns the next raw connection to the listener
#### func (*RawListener) Addr
```go
func (l *RawListener) Addr() net.Addr
```
Addr returns the listener's network address
#### func (*RawListener) Close
```go
func (l *RawListener) Close() error
```
Close closes the raw listener
#### type RawReader
```go
type RawReader struct {
}
```
RawReader handles incoming raw datagram reception
#### func (*RawReader) Close
```go
func (r *RawReader) Close() error
```
#### func (*RawReader) ReceiveDatagram
```go
func (r *RawReader) ReceiveDatagram() (*RawDatagram, error)
```
ReceiveDatagram receives a raw datagram from any source
#### type RawSession
```go
type RawSession struct {
*common.BaseSession
}
```
RawSession represents a raw session that can send and receive raw datagrams
#### func NewRawSession
```go
func NewRawSession(sam *common.SAM, id string, keys i2pkeys.I2PKeys, options []string) (*RawSession, error)
```
NewRawSession creates a new raw session
#### func (*RawSession) Addr
```go
func (s *RawSession) Addr() i2pkeys.I2PAddr
```
Addr returns the I2P address of this session
#### func (*RawSession) Close
```go
func (s *RawSession) Close() error
```
Close closes the raw session and all associated resources
#### func (*RawSession) Dial
```go
func (rs *RawSession) Dial(destination string) (net.PacketConn, error)
```
Dial establishes a raw connection to the specified destination
#### func (*RawSession) DialContext
```go
func (rs *RawSession) DialContext(ctx context.Context, destination string) (net.PacketConn, error)
```
DialContext establishes a raw connection with context support
#### func (*RawSession) DialI2P
```go
func (rs *RawSession) DialI2P(addr i2pkeys.I2PAddr) (net.PacketConn, error)
```
DialI2P establishes a raw connection to an I2P address
#### func (*RawSession) DialI2PContext
```go
func (rs *RawSession) DialI2PContext(ctx context.Context, addr i2pkeys.I2PAddr) (net.PacketConn, error)
```
DialI2PContext establishes a raw connection to an I2P address with context
support
#### func (*RawSession) DialI2PTimeout
```go
func (rs *RawSession) DialI2PTimeout(addr i2pkeys.I2PAddr, timeout time.Duration) (net.PacketConn, error)
```
DialI2PTimeout establishes a raw connection to an I2P address with timeout
#### func (*RawSession) DialTimeout
```go
func (rs *RawSession) DialTimeout(destination string, timeout time.Duration) (net.PacketConn, error)
```
DialTimeout establishes a raw connection with a timeout
#### func (*RawSession) Listen
```go
func (s *RawSession) Listen() (*RawListener, error)
```
#### func (*RawSession) NewReader
```go
func (s *RawSession) NewReader() *RawReader
```
NewReader creates a RawReader for receiving raw datagrams
#### func (*RawSession) NewWriter
```go
func (s *RawSession) NewWriter() *RawWriter
```
NewWriter creates a RawWriter for sending raw datagrams
#### func (*RawSession) PacketConn
```go
func (s *RawSession) PacketConn() net.PacketConn
```
PacketConn returns a net.PacketConn interface for this session
#### func (*RawSession) ReceiveDatagram
```go
func (s *RawSession) ReceiveDatagram() (*RawDatagram, error)
```
ReceiveDatagram receives a raw datagram from any source
#### func (*RawSession) SendDatagram
```go
func (s *RawSession) SendDatagram(data []byte, dest i2pkeys.I2PAddr) error
```
SendDatagram sends a raw datagram to the specified destination
#### type RawWriter
```go
type RawWriter struct {
}
```
RawWriter handles outgoing raw datagram transmission
#### func (*RawWriter) SendDatagram
```go
func (w *RawWriter) SendDatagram(data []byte, dest i2pkeys.I2PAddr) error
```
SendDatagram sends a raw datagram to the specified destination
#### func (*RawWriter) SetTimeout
```go
func (w *RawWriter) SetTimeout(timeout time.Duration) *RawWriter
```
SetTimeout sets the timeout for raw datagram operations
#### type SAM
```go
type SAM struct {
*common.SAM
}
```
SAM wraps common.SAM to provide raw-specific functionality
#### func (*SAM) NewRawSession
```go
func (s *SAM) NewRawSession(id string, keys i2pkeys.I2PKeys, options []string) (*RawSession, error)
```
NewRawSession creates a new raw session with the SAM bridge
#### func (*SAM) NewRawSessionWithPorts
```go
func (s *SAM) NewRawSessionWithPorts(id, fromPort, toPort string, keys i2pkeys.I2PKeys, options []string) (*RawSession, error)
```
NewRawSessionWithPorts creates a new raw session with port specifications
#### func (*SAM) NewRawSessionWithSignature
```go
func (s *SAM) NewRawSessionWithSignature(id string, keys i2pkeys.I2PKeys, options []string, sigType string) (*RawSession, error)
```
NewRawSessionWithSignature creates a new raw session with custom signature type

23
raw/README.md Normal file
View File

@@ -0,0 +1,23 @@
# go-sam-go/raw
High-level raw datagram library for unencrypted message delivery over I2P using the SAMv3 protocol.
## Installation
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.
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.
## 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/sirupsen/logrus - Structured logging
- github.com/samber/oops - Enhanced error handling

97
raw/SAM.go Normal file
View File

@@ -0,0 +1,97 @@
package raw
import (
"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 raw-specific functionality for creating and managing
// raw datagram sessions. This type extends the base SAM functionality with methods
// specifically designed for raw I2P datagram communication.
// SAM wraps common.SAM to provide raw-specific functionality
type SAM struct {
*common.SAM
}
// NewRawSession creates a new raw session with the SAM bridge using default settings.
// This method establishes a new raw datagram session with the specified ID, keys, and options.
// Raw sessions enable unencrypted datagram transmission over the I2P network.
// NewRawSession creates a new raw session with the SAM bridge
func (s *SAM) NewRawSession(id string, keys i2pkeys.I2PKeys, options []string) (*RawSession, error) {
return NewRawSession(s.SAM, id, keys, options)
}
// NewRawSessionWithSignature creates a new raw session with custom signature type.
// This method allows specifying a custom cryptographic signature type for the session,
// enabling advanced security configurations beyond the default signature algorithm.
// NewRawSessionWithSignature creates a new raw session with custom signature type
func (s *SAM) NewRawSessionWithSignature(id string, keys i2pkeys.I2PKeys, options []string, sigType string) (*RawSession, error) {
logger := log.WithFields(logrus.Fields{
"id": id,
"options": options,
"sigType": sigType,
})
logger.Debug("Creating new RawSession with signature")
// Create the base session using the common package with signature
session, err := s.SAM.NewGenericSessionWithSignature("RAW", 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 raw session: %w", err)
}
baseSession, ok := session.(*common.BaseSession)
if !ok {
logger.Error("Session is not a BaseSession")
session.Close()
return nil, oops.Errorf("invalid session type")
}
rs := &RawSession{
BaseSession: baseSession,
sam: s.SAM,
options: options,
}
logger.Debug("Successfully created RawSession with signature")
return rs, nil
}
// NewRawSessionWithPorts creates a new raw 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.
// NewRawSessionWithPorts creates a new raw session with port specifications
func (s *SAM) NewRawSessionWithPorts(id, fromPort, toPort string, keys i2pkeys.I2PKeys, options []string) (*RawSession, error) {
logger := log.WithFields(logrus.Fields{
"id": id,
"fromPort": fromPort,
"toPort": toPort,
"options": options,
})
logger.Debug("Creating new RawSession with ports")
// Create the base session using the common package with ports
session, err := s.SAM.NewGenericSessionWithSignatureAndPorts("RAW", id, fromPort, toPort, keys, common.SIG_EdDSA_SHA512_Ed25519, options)
if err != nil {
logger.WithError(err).Error("Failed to create generic session with ports")
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()
return nil, oops.Errorf("invalid session type")
}
rs := &RawSession{
BaseSession: baseSession,
sam: s.SAM,
options: options,
}
logger.Debug("Successfully created RawSession with ports")
return rs, nil
}

144
raw/dial.go Normal file
View File

@@ -0,0 +1,144 @@
package raw
import (
"context"
"net"
"time"
"github.com/go-i2p/i2pkeys"
"github.com/samber/oops"
"github.com/sirupsen/logrus"
)
// Dial establishes a raw connection to the specified I2P destination address.
// This method creates a net.PacketConn interface for sending and receiving raw datagrams
// with the specified destination. It uses a default timeout of 30 seconds.
// Dial establishes a raw connection to the specified destination
func (rs *RawSession) Dial(destination string) (net.PacketConn, error) {
return rs.DialTimeout(destination, 30*time.Second)
}
// DialTimeout establishes a raw connection with a specified timeout duration.
// This method creates a net.PacketConn interface with timeout support, allowing
// for time-bounded connection establishment. Zero or negative timeout values disable the timeout.
// DialTimeout establishes a raw connection with a timeout
func (rs *RawSession) DialTimeout(destination string, timeout time.Duration) (net.PacketConn, error) {
// Handle zero or negative timeout - no timeout should be applied
if timeout <= 0 {
return rs.DialContext(context.Background(), destination)
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return rs.DialContext(ctx, destination)
}
// DialContext establishes a raw connection with context support for cancellation.
// This method provides the core dialing functionality with context-based cancellation support,
// allowing for proper resource cleanup and operation cancellation through the provided context.
// DialContext establishes a raw connection with context support
func (rs *RawSession) DialContext(ctx context.Context, destination string) (net.PacketConn, error) {
// Check if context is cancelled before starting
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
// Validate session state first
rs.mu.RLock()
if rs.closed {
rs.mu.RUnlock()
return nil, oops.Errorf("session is closed")
}
rs.mu.RUnlock()
// Validate destination
if destination == "" {
return nil, oops.Errorf("destination cannot be empty")
}
logger := log.WithFields(logrus.Fields{
"destination": destination,
})
logger.Debug("Dialing raw destination")
// Create a raw connection
conn := &RawConn{
session: rs,
reader: rs.NewReader(),
writer: rs.NewWriter(),
}
// Start the reader loop once for this connection
if conn.reader != nil {
go conn.reader.receiveLoop()
}
logger.WithField("session_id", rs.ID()).Debug("Successfully created raw connection")
return conn, nil
}
// DialI2P establishes a raw connection to an I2P address using native I2P addressing.
// This method creates a net.PacketConn interface for communicating with the specified I2P address
// using the native i2pkeys.I2PAddr type. It uses a default timeout of 30 seconds.
// DialI2P establishes a raw connection to an I2P address
func (rs *RawSession) DialI2P(addr i2pkeys.I2PAddr) (net.PacketConn, error) {
return rs.DialI2PTimeout(addr, 30*time.Second)
}
// DialI2PTimeout establishes a raw connection to an I2P address with timeout support.
// This method provides time-bounded connection establishment using native I2P addressing.
// Zero or negative timeout values disable the timeout mechanism.
// DialI2PTimeout establishes a raw connection to an I2P address with timeout
func (rs *RawSession) DialI2PTimeout(addr i2pkeys.I2PAddr, timeout time.Duration) (net.PacketConn, error) {
// Handle zero or negative timeout - no timeout should be applied
if timeout <= 0 {
return rs.DialI2PContext(context.Background(), addr)
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return rs.DialI2PContext(ctx, addr)
}
// DialI2PContext establishes a raw connection to an I2P address with context support.
// This method provides the core I2P dialing functionality with context-based cancellation,
// allowing for proper resource cleanup and operation cancellation through the provided context.
// DialI2PContext establishes a raw connection to an I2P address with context support
func (rs *RawSession) DialI2PContext(ctx context.Context, addr i2pkeys.I2PAddr) (net.PacketConn, error) {
// Check if context is cancelled before starting
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
// Validate session state first
rs.mu.RLock()
if rs.closed {
rs.mu.RUnlock()
return nil, oops.Errorf("session is closed")
}
rs.mu.RUnlock()
logger := log.WithFields(logrus.Fields{
"destination": addr.Base32(),
})
logger.Debug("Dialing I2P raw destination")
// Create a raw connection
conn := &RawConn{
session: rs,
reader: rs.NewReader(),
writer: rs.NewWriter(),
}
// Start the reader loop once for this connection
if conn.reader != nil {
go conn.reader.receiveLoop()
}
logger.WithField("session_id", rs.ID()).Debug("Successfully created I2P raw connection")
return conn, nil
}

578
raw/dial_test.go Normal file
View File

@@ -0,0 +1,578 @@
package raw
import (
"context"
"net"
"testing"
"time"
"github.com/go-i2p/go-sam-go/common"
"github.com/go-i2p/i2pkeys"
)
func setupTestSession(t *testing.T) *RawSession {
t.Helper()
// Skip actual I2P connection for unit tests
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
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)
}
session, err := NewRawSession(sam, "test_dial_session", keys, nil)
if err != nil {
sam.Close()
t.Fatalf("Failed to create session: %v", err)
}
return session
}
// Update the test to use proper session setup
func TestRawSession_Dial(t *testing.T) {
tests := []struct {
name string
destination string
wantErr bool
errContains string
}{
{
name: "valid_b32_destination",
destination: "example.b32.i2p",
wantErr: false,
},
{
name: "empty_destination",
destination: "",
wantErr: true,
errContains: "destination",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
session := setupTestSession(t)
defer session.Close()
conn, err := session.Dial(tt.destination)
if tt.wantErr {
if err == nil {
t.Errorf("Dial() expected error but got none")
return
}
if tt.errContains != "" && !containsString(err.Error(), tt.errContains) {
t.Errorf("Dial() error = %v, want error containing %q", err, tt.errContains)
}
return
}
if err != nil {
t.Errorf("Dial() unexpected error = %v", err)
return
}
if conn == nil {
t.Error("Dial() returned nil connection")
return
}
// Clean up
if conn != nil {
_ = conn.Close()
}
})
}
}
func TestRawSession_DialTimeout(t *testing.T) {
tests := []struct {
name string
destination string
timeout time.Duration
setupSession func() *RawSession
wantErr bool
errContains string
}{
{
name: "valid_dial_with_timeout",
destination: "example.b32.i2p",
timeout: 5 * time.Second,
setupSession: func() *RawSession {
sam := &common.SAM{}
baseSession := &common.BaseSession{}
return &RawSession{
BaseSession: baseSession,
sam: sam,
options: []string{},
closed: false,
}
},
wantErr: false,
},
{
name: "zero_timeout",
destination: "example.b32.i2p",
timeout: 0,
setupSession: func() *RawSession {
sam := &common.SAM{}
baseSession := &common.BaseSession{}
return &RawSession{
BaseSession: baseSession,
sam: sam,
options: []string{},
closed: false,
}
},
wantErr: false, // Zero timeout should still work, just no timeout
},
{
name: "negative_timeout",
destination: "example.b32.i2p",
timeout: -1 * time.Second,
setupSession: func() *RawSession {
sam := &common.SAM{}
baseSession := &common.BaseSession{}
return &RawSession{
BaseSession: baseSession,
sam: sam,
options: []string{},
closed: false,
}
},
wantErr: false, // Implementation should handle negative timeout gracefully
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
session := tt.setupSession()
conn, err := session.DialTimeout(tt.destination, tt.timeout)
if tt.wantErr {
if err == nil {
t.Errorf("DialTimeout() expected error but got none")
return
}
if tt.errContains != "" && !containsString(err.Error(), tt.errContains) {
t.Errorf("DialTimeout() error = %v, want error containing %q", err, tt.errContains)
}
return
}
if err != nil {
t.Errorf("DialTimeout() unexpected error = %v", err)
return
}
if conn == nil {
t.Error("DialTimeout() returned nil connection")
return
}
// Verify conn implements net.PacketConn
if _, ok := conn.(net.PacketConn); !ok {
t.Error("DialTimeout() returned connection that doesn't implement net.PacketConn")
}
// Clean up
if conn != nil {
_ = conn.Close()
}
})
}
}
func TestRawSession_DialContext(t *testing.T) {
tests := []struct {
name string
destination string
setupContext func() context.Context
setupSession func() *RawSession
wantErr bool
errContains string
}{
{
name: "valid_dial_with_context",
destination: "example.b32.i2p",
setupContext: func() context.Context {
return context.Background()
},
setupSession: func() *RawSession {
sam := &common.SAM{}
baseSession := &common.BaseSession{}
return &RawSession{
BaseSession: baseSession,
sam: sam,
options: []string{},
closed: false,
}
},
wantErr: false,
},
{
name: "cancelled_context",
destination: "example.b32.i2p",
setupContext: func() context.Context {
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
return ctx
},
setupSession: func() *RawSession {
sam := &common.SAM{}
baseSession := &common.BaseSession{}
return &RawSession{
BaseSession: baseSession,
sam: sam,
options: []string{},
closed: false,
}
},
wantErr: true,
errContains: "context",
},
{
name: "context_with_timeout",
destination: "example.b32.i2p",
setupContext: func() context.Context {
ctx, _ := context.WithTimeout(context.Background(), 100*time.Millisecond)
return ctx
},
setupSession: func() *RawSession {
sam := &common.SAM{}
baseSession := &common.BaseSession{}
return &RawSession{
BaseSession: baseSession,
sam: sam,
options: []string{},
closed: false,
}
},
wantErr: false, // Should succeed if dial completes quickly
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
session := tt.setupSession()
ctx := tt.setupContext()
conn, err := session.DialContext(ctx, tt.destination)
if tt.wantErr {
if err == nil {
t.Errorf("DialContext() expected error but got none")
return
}
if tt.errContains != "" && !containsString(err.Error(), tt.errContains) {
t.Errorf("DialContext() error = %v, want error containing %q", err, tt.errContains)
}
return
}
if err != nil {
t.Errorf("DialContext() unexpected error = %v", err)
return
}
if conn == nil {
t.Error("DialContext() returned nil connection")
return
}
// Verify conn implements net.PacketConn
if _, ok := conn.(net.PacketConn); !ok {
t.Error("DialContext() returned connection that doesn't implement net.PacketConn")
}
// Clean up
if conn != nil {
_ = conn.Close()
}
})
}
}
func TestRawSession_DialI2P(t *testing.T) {
// Create a test I2P address
testAddr := createTestI2PAddr()
tests := []struct {
name string
addr i2pkeys.I2PAddr
setupSession func() *RawSession
wantErr bool
errContains string
}{
{
name: "valid_i2p_address",
addr: testAddr,
setupSession: func() *RawSession {
sam := &common.SAM{}
baseSession := &common.BaseSession{}
return &RawSession{
BaseSession: baseSession,
sam: sam,
options: []string{},
closed: false,
}
},
wantErr: false,
},
{
name: "dial_i2p_on_closed_session",
addr: testAddr,
setupSession: func() *RawSession {
sam := &common.SAM{}
baseSession := &common.BaseSession{}
return &RawSession{
BaseSession: baseSession,
sam: sam,
options: []string{},
closed: true,
}
},
wantErr: true,
errContains: "closed",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
session := tt.setupSession()
conn, err := session.DialI2P(tt.addr)
if tt.wantErr {
if err == nil {
t.Errorf("DialI2P() expected error but got none")
return
}
if tt.errContains != "" && !containsString(err.Error(), tt.errContains) {
t.Errorf("DialI2P() error = %v, want error containing %q", err, tt.errContains)
}
return
}
if err != nil {
t.Errorf("DialI2P() unexpected error = %v", err)
return
}
if conn == nil {
t.Error("DialI2P() returned nil connection")
return
}
// Verify conn implements net.PacketConn
if _, ok := conn.(net.PacketConn); !ok {
t.Error("DialI2P() returned connection that doesn't implement net.PacketConn")
}
// Clean up
if conn != nil {
_ = conn.Close()
}
})
}
}
func TestRawSession_DialI2PTimeout(t *testing.T) {
testAddr := createTestI2PAddr()
tests := []struct {
name string
addr i2pkeys.I2PAddr
timeout time.Duration
setupSession func() *RawSession
wantErr bool
errContains string
}{
{
name: "valid_i2p_dial_with_timeout",
addr: testAddr,
timeout: 5 * time.Second,
setupSession: func() *RawSession {
sam := &common.SAM{}
baseSession := &common.BaseSession{}
return &RawSession{
BaseSession: baseSession,
sam: sam,
options: []string{},
closed: false,
}
},
wantErr: false,
},
{
name: "i2p_dial_zero_timeout",
addr: testAddr,
timeout: 0,
setupSession: func() *RawSession {
sam := &common.SAM{}
baseSession := &common.BaseSession{}
return &RawSession{
BaseSession: baseSession,
sam: sam,
options: []string{},
closed: false,
}
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
session := tt.setupSession()
conn, err := session.DialI2PTimeout(tt.addr, tt.timeout)
if tt.wantErr {
if err == nil {
t.Errorf("DialI2PTimeout() expected error but got none")
return
}
if tt.errContains != "" && !containsString(err.Error(), tt.errContains) {
t.Errorf("DialI2PTimeout() error = %v, want error containing %q", err, tt.errContains)
}
return
}
if err != nil {
t.Errorf("DialI2PTimeout() unexpected error = %v", err)
return
}
if conn == nil {
t.Error("DialI2PTimeout() returned nil connection")
return
}
// Verify conn implements net.PacketConn
if _, ok := conn.(net.PacketConn); !ok {
t.Error("DialI2PTimeout() returned connection that doesn't implement net.PacketConn")
}
// Clean up
if conn != nil {
_ = conn.Close()
}
})
}
}
func TestRawSession_DialI2PContext(t *testing.T) {
testAddr := createTestI2PAddr()
tests := []struct {
name string
addr i2pkeys.I2PAddr
setupContext func() context.Context
setupSession func() *RawSession
wantErr bool
errContains string
}{
{
name: "valid_i2p_dial_with_context",
addr: testAddr,
setupContext: func() context.Context {
return context.Background()
},
setupSession: func() *RawSession {
sam := &common.SAM{}
baseSession := &common.BaseSession{}
return &RawSession{
BaseSession: baseSession,
sam: sam,
options: []string{},
closed: false,
}
},
wantErr: false,
},
{
name: "i2p_dial_cancelled_context",
addr: testAddr,
setupContext: func() context.Context {
ctx, cancel := context.WithCancel(context.Background())
cancel()
return ctx
},
setupSession: func() *RawSession {
sam := &common.SAM{}
baseSession := &common.BaseSession{}
return &RawSession{
BaseSession: baseSession,
sam: sam,
options: []string{},
closed: false,
}
},
wantErr: true,
errContains: "context",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
session := tt.setupSession()
ctx := tt.setupContext()
conn, err := session.DialI2PContext(ctx, tt.addr)
if tt.wantErr {
if err == nil {
t.Errorf("DialI2PContext() expected error but got none")
return
}
if tt.errContains != "" && !containsString(err.Error(), tt.errContains) {
t.Errorf("DialI2PContext() error = %v, want error containing %q", err, tt.errContains)
}
return
}
if err != nil {
t.Errorf("DialI2PContext() unexpected error = %v", err)
return
}
if conn == nil {
t.Error("DialI2PContext() returned nil connection")
return
}
// Verify conn implements net.PacketConn
if _, ok := conn.(net.PacketConn); !ok {
t.Error("DialI2PContext() returned connection that doesn't implement net.PacketConn")
}
// Clean up
if conn != nil {
_ = conn.Close()
}
})
}
}
// Helper function to create a test I2P address
func createTestI2PAddr() i2pkeys.I2PAddr {
// Create a minimal test address - in real implementation this would be a proper I2P destination
dest, _ := i2pkeys.NewDestination()
return dest.Addr()
}

39
raw/listen.go Normal file
View File

@@ -0,0 +1,39 @@
package raw
import (
"github.com/samber/oops"
)
// Listen creates a RawListener for accepting incoming raw connections.
// This method initializes the listener with buffered channels for incoming connections
// and starts the accept loop in a background goroutine to handle incoming datagrams.
// Example usage: listener, err := session.Listen()
func (s *RawSession) Listen() (*RawListener, error) {
s.mu.RLock()
defer s.mu.RUnlock()
// Check if the session is already closed before creating listener
if s.closed {
return nil, oops.Errorf("session is closed")
}
logger := log.WithField("id", s.ID())
logger.Debug("Creating RawListener")
// Initialize listener with buffered channels for connection management
// The acceptChan buffers incoming connections to prevent blocking
listener := &RawListener{
session: s,
reader: s.NewReader(),
acceptChan: make(chan *RawConn, 10), // Buffer for incoming connections
errorChan: make(chan error, 1),
closeChan: make(chan struct{}),
}
// Start accepting raw connections in a goroutine
// This allows the listener to handle multiple concurrent connections
go listener.acceptLoop()
logger.Debug("Successfully created RawListener")
return listener, nil
}

238
raw/listen_test.go Normal file
View File

@@ -0,0 +1,238 @@
package raw
import (
"testing"
"time"
"github.com/go-i2p/go-sam-go/common"
)
func TestRawSession_Listen(t *testing.T) {
tests := []struct {
name string
setupSession func() *RawSession
wantErr bool
errContains string
}{
{
name: "successful_listen",
setupSession: func() *RawSession {
// Create a mock SAM connection
sam := &common.SAM{}
baseSession := &common.BaseSession{}
return &RawSession{
BaseSession: baseSession,
sam: sam,
options: []string{},
closed: false,
}
},
wantErr: false,
},
{
name: "listen_on_closed_session",
setupSession: func() *RawSession {
sam := &common.SAM{}
baseSession := &common.BaseSession{}
return &RawSession{
BaseSession: baseSession,
sam: sam,
options: []string{},
closed: true,
}
},
wantErr: true,
errContains: "session is closed",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
session := tt.setupSession()
listener, err := session.Listen()
if tt.wantErr {
if err == nil {
t.Errorf("Listen() expected error but got none")
return
}
if tt.errContains != "" && !containsString(err.Error(), tt.errContains) {
t.Errorf("Listen() error = %v, want error containing %q", err, tt.errContains)
}
return
}
if err != nil {
t.Errorf("Listen() unexpected error = %v", err)
return
}
if listener == nil {
t.Error("Listen() returned nil listener")
return
}
// Verify listener properties
if listener.session != session {
t.Error("Listener session reference incorrect")
}
if listener.reader == nil {
t.Error("Listener reader not initialized")
}
if listener.acceptChan == nil {
t.Error("Listener acceptChan not initialized")
}
if listener.errorChan == nil {
t.Error("Listener errorChan not initialized")
}
if listener.closeChan == nil {
t.Error("Listener closeChan not initialized")
}
// Clean up
if listener != nil {
_ = listener.Close()
}
})
}
}
func TestRawListener_Properties(t *testing.T) {
// Setup a basic session for testing
sam := &common.SAM{}
baseSession := &common.BaseSession{}
session := &RawSession{
BaseSession: baseSession,
sam: sam,
options: []string{},
closed: false,
}
listener, err := session.Listen()
if err != nil {
t.Fatalf("Failed to create listener: %v", err)
}
defer listener.Close()
t.Run("channels_buffered_correctly", func(t *testing.T) {
// Check that acceptChan has proper buffer size
select {
case listener.acceptChan <- &RawConn{}:
// Should not block for first 10 items
case <-time.After(100 * time.Millisecond):
t.Error("acceptChan appears to be unbuffered or too small")
}
// Drain the channel
select {
case <-listener.acceptChan:
default:
t.Error("Failed to read from acceptChan")
}
})
t.Run("initial_state", func(t *testing.T) {
if listener.closed {
t.Error("New listener should not be closed initially")
}
})
}
func TestRawListener_Close(t *testing.T) {
sam := &common.SAM{}
baseSession := &common.BaseSession{}
session := &RawSession{
BaseSession: baseSession,
sam: sam,
options: []string{},
closed: false,
}
listener, err := session.Listen()
if err != nil {
t.Fatalf("Failed to create listener: %v", err)
}
// Test closing
err = listener.Close()
if err != nil {
t.Errorf("Close() returned error: %v", err)
}
// Verify closed state
listener.mu.RLock()
closed := listener.closed
listener.mu.RUnlock()
if !closed {
t.Error("Listener should be marked as closed after Close()")
}
// Test double close
err = listener.Close()
if err == nil {
t.Error("Second Close() should return error")
}
}
func TestRawListener_Concurrent_Access(t *testing.T) {
sam := &common.SAM{}
baseSession := &common.BaseSession{}
session := &RawSession{
BaseSession: baseSession,
sam: sam,
options: []string{},
closed: false,
}
listener, err := session.Listen()
if err != nil {
t.Fatalf("Failed to create listener: %v", err)
}
defer listener.Close()
// Test concurrent access to listener state
done := make(chan bool, 2)
go func() {
defer func() { done <- true }()
for i := 0; i < 100; i++ {
listener.mu.RLock()
_ = listener.closed
listener.mu.RUnlock()
}
}()
go func() {
defer func() { done <- true }()
for i := 0; i < 100; i++ {
listener.mu.RLock()
_ = listener.session
listener.mu.RUnlock()
}
}()
// Wait for both goroutines
<-done
<-done
}
// Helper function to check if a string contains a substring
func containsString(s, substr string) bool {
return len(s) >= len(substr) &&
(substr == "" || findString(s, substr))
}
func findString(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}

View File

@@ -1,10 +1,10 @@
package raw
import logger "github.com/go-i2p/go-sam-go/logger"
import (
"github.com/go-i2p/logger"
)
var log = logger.GetSAM3Logger()
func init() {
logger.InitializeSAM3Logger()
log = logger.GetSAM3Logger()
}
// log provides the default logger instance for the raw package.
// This logger is configured to use the standard go-i2p logging system
// and provides structured logging capabilities for raw session operations.
var log = logger.GetGoI2PLogger()

173
raw/packetconn.go Normal file
View File

@@ -0,0 +1,173 @@
package raw
import (
"net"
"time"
"github.com/samber/oops"
)
// ReadFrom reads a raw datagram from the connection.
// This method implements the net.PacketConn interface and blocks until a datagram
// is received or an error occurs, returning the data, source address, and any error.
// Example usage: n, addr, err := conn.ReadFrom(buffer)
func (c *RawConn) 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()
// Receive a datagram from the reader
datagram, err := c.reader.ReceiveDatagram()
if err != nil {
return 0, nil, err
}
// Copy data to the provided buffer
n = copy(p, datagram.Data)
addr = &RawAddr{addr: datagram.Source}
return n, addr, nil
}
// WriteTo writes a raw datagram to the specified address.
// This method implements the net.PacketConn interface and sends the data
// to the destination address, returning the number of bytes written and any error.
// Example usage: n, err := conn.WriteTo(data, destAddr)
func (c *RawConn) 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.(*RawAddr)
if !ok {
return 0, oops.Errorf("address must be a RawAddr")
}
// Send the datagram using the writer
err = c.writer.SendDatagram(p, i2pAddr.addr)
if err != nil {
return 0, err
}
return len(p), nil
}
// Close closes the raw connection and cleans up associated resources.
// This method is safe to call multiple times and will only perform cleanup once.
// The underlying session remains open and can be used by other connections.
// Example usage: defer conn.Close()
func (c *RawConn) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.closed {
return nil
}
logger := log.WithField("session_id", c.session.ID())
logger.Debug("Closing RawConn")
c.closed = true
// 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
logger.Debug("Successfully closed RawConn")
return nil
}
// LocalAddr returns the local address of the connection.
// This method implements the net.PacketConn interface and returns the I2P address
// of the session wrapped in a RawAddr for compatibility with net.Addr.
// Example usage: addr := conn.LocalAddr()
func (c *RawConn) LocalAddr() net.Addr {
return &RawAddr{addr: c.session.Addr()}
}
// SetDeadline sets the read and write deadlines for the connection.
// This method implements the net.PacketConn interface and applies the deadline
// to both read and write operations through separate deadline methods.
// Example usage: conn.SetDeadline(time.Now().Add(30*time.Second))
func (c *RawConn) SetDeadline(t time.Time) error {
// Apply the deadline to both read and write operations
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.PacketConn interface for timeout support.
// Currently this is a placeholder implementation for I2P raw datagrams.
// Example usage: conn.SetReadDeadline(time.Now().Add(10*time.Second))
func (c *RawConn) SetReadDeadline(t time.Time) error {
// For raw 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.PacketConn interface by configuring the writer timeout
// based on the deadline duration, providing timeout support for send operations.
// Example usage: conn.SetWriteDeadline(time.Now().Add(5*time.Second))
func (c *RawConn) SetWriteDeadline(t time.Time) error {
// Calculate timeout duration from deadline and apply to writer
// Zero deadline means no timeout should be applied
if !t.IsZero() {
timeout := time.Until(t)
c.writer.SetTimeout(timeout)
}
return nil
}
// Read implements net.Conn by wrapping ReadFrom for stream-like operations.
// This method reads data and updates the remote address from the sender,
// providing compatibility with net.Conn interface expectations.
// Example usage: n, err := conn.Read(buffer)
func (c *RawConn) Read(b []byte) (n int, err error) {
// Perform the ReadFrom operation
n, addr, err := c.ReadFrom(b)
// Update the remote address if one was received
if addr != nil {
c.remoteAddr = &addr.(*RawAddr).addr
}
return n, err
}
// RemoteAddr returns the remote address of the connection.
// This method implements the net.Conn interface and returns the address of the last
// sender if available, or nil if no remote address has been established.
// Example usage: addr := conn.RemoteAddr()
func (c *RawConn) RemoteAddr() net.Addr {
// Return the remote address if one has been set
if c.remoteAddr != nil {
return &RawAddr{addr: *c.remoteAddr}
}
return nil
}
// Write implements net.Conn by wrapping WriteTo for stream-like operations.
// This method requires a remote address to be set through prior Read operations
// and provides compatibility with net.Conn interface expectations.
// Example usage: n, err := conn.Write(data)
func (c *RawConn) Write(b []byte) (n int, err error) {
// Check if a remote address has been set
if c.remoteAddr == nil {
return 0, oops.Errorf("no remote address set, use WriteTo or Read first")
}
// Use the stored remote address for writing
addr := &RawAddr{addr: *c.remoteAddr}
return c.WriteTo(b, addr)
}

138
raw/packetlistener.go Normal file
View File

@@ -0,0 +1,138 @@
package raw
import (
"net"
"github.com/samber/oops"
)
// Accept waits for and returns the next raw connection to the listener.
// This method implements the net.Listener interface and blocks until a connection
// is available or an error occurs, returning the connection or error.
// Example usage: conn, err := listener.Accept()
func (l *RawListener) Accept() (net.Conn, error) {
l.mu.RLock()
if l.closed {
l.mu.RUnlock()
return nil, oops.Errorf("listener is closed")
}
l.mu.RUnlock()
// Use select to handle multiple channels atomically
// This ensures proper handling of connections, errors, and close signals
select {
case conn := <-l.acceptChan:
return conn, nil
case err := <-l.errorChan:
return nil, err
case <-l.closeChan:
return nil, oops.Errorf("listener is closed")
}
}
// Close closes the raw listener and stops accepting new connections.
// This method is safe to call multiple times and will clean up all resources
// including the reader and associated channels.
// Example usage: defer listener.Close()
func (l *RawListener) Close() error {
l.mu.Lock()
defer l.mu.Unlock()
if l.closed {
return oops.Errorf("listener is already closed")
}
logger := log.WithField("session_id", l.session.ID())
logger.Debug("Closing RawListener")
l.closed = true
// Signal the accept loop to terminate
close(l.closeChan)
// Close the reader to stop receiving datagrams
if l.reader != nil {
l.reader.Close()
}
logger.Debug("Successfully closed RawListener")
return nil
}
// Addr returns the listener's network address.
// This method implements the net.Listener interface and returns the I2P address
// of the session wrapped in a RawAddr for compatibility with net.Addr.
// Example usage: addr := listener.Addr()
func (l *RawListener) Addr() net.Addr {
return &RawAddr{addr: l.session.Addr()}
}
// acceptLoop continuously accepts incoming raw connections in a separate goroutine.
// This method manages the connection acceptance lifecycle, handles error conditions,
// and maintains the acceptChan buffer for incoming connections until the listener is closed.
// acceptLoop continuously accepts incoming raw connections
func (l *RawListener) acceptLoop() {
logger := log.WithField("session_id", l.session.ID())
logger.Debug("Starting raw accept loop")
// Continuously accept connections until the listener is closed
for {
select {
case <-l.closeChan:
logger.Debug("Raw accept loop terminated - listener closed")
return
default:
// Try to accept a new raw connection
conn, err := l.acceptRawConnection()
if err != nil {
// Check if the listener is still open before sending error
l.mu.RLock()
closed := l.closed
l.mu.RUnlock()
if !closed {
logger.WithError(err).Error("Failed to accept raw connection")
select {
case l.errorChan <- err:
case <-l.closeChan:
return
}
}
continue
}
// Send the new connection to the accept channel
select {
case l.acceptChan <- conn:
logger.Debug("Successfully accepted new raw connection")
case <-l.closeChan:
// Clean up the connection if listener was closed
conn.Close()
return
}
}
}
}
// acceptRawConnection creates a new raw connection for handling incoming datagrams.
// For raw sessions, this method creates a RawConn that shares the session resources
// but has its own dedicated reader and writer components for handling the specific connection.
// acceptRawConnection creates a new raw connection for incoming datagrams
func (l *RawListener) acceptRawConnection() (*RawConn, error) {
logger := log.WithField("session_id", l.session.ID())
logger.Debug("Creating new raw connection")
// For raw sessions, we create a new RawConn that shares the session
// but has its own reader/writer for handling the specific connection
conn := &RawConn{
session: l.session,
reader: l.session.NewReader(),
writer: l.session.NewWriter(),
}
// Start the reader loop once for this connection
if conn.reader != nil {
go conn.reader.receiveLoop()
}
return conn, nil
}

View File

@@ -1,73 +0,0 @@
package raw
import (
"errors"
"net"
"strconv"
"github.com/go-i2p/go-sam-go/common"
"github.com/go-i2p/i2pkeys"
"github.com/sirupsen/logrus"
)
// Creates a new raw session. udpPort is the UDP port SAM is listening on,
// and if you set it to zero, it will use SAMs standard UDP port.
func (s *SAM) NewRawSession(id string, keys i2pkeys.I2PKeys, options []string, udpPort int) (*RawSession, error) {
log.WithFields(logrus.Fields{"id": id, "udpPort": udpPort}).Debug("Creating new RawSession")
if udpPort > 65335 || udpPort < 0 {
log.WithField("udpPort", udpPort).Error("Invalid UDP port")
return nil, errors.New("udpPort needs to be in the interval 0-65335")
}
if udpPort == 0 {
udpPort = 7655
log.Debug("Using default UDP port 7655")
}
lhost, _, err := common.SplitHostPort(s.LocalAddr().String())
if err != nil {
log.Debug("Using default UDP port 7655")
s.Close()
return nil, err
}
lUDPAddr, err := net.ResolveUDPAddr("udp4", lhost+":0")
if err != nil {
log.WithError(err).Error("Failed to resolve local UDP address")
return nil, err
}
udpconn, err := net.ListenUDP("udp4", lUDPAddr)
if err != nil {
log.WithError(err).Error("Failed to listen on UDP")
return nil, err
}
rhost, _, err := common.SplitHostPort(s.RemoteAddr().String())
if err != nil {
log.WithError(err).Error("Failed to split remote host port")
s.Close()
return nil, err
}
rUDPAddr, err := net.ResolveUDPAddr("udp4", rhost+":"+strconv.Itoa(udpPort))
if err != nil {
log.WithError(err).Error("Failed to resolve remote UDP address")
return nil, err
}
_, lport, err := net.SplitHostPort(udpconn.LocalAddr().String())
if err != nil {
log.WithError(err).Error("Failed to get local port")
return nil, err
}
conn, err := s.NewGenericSession("RAW", id, keys, []string{"PORT=" + lport})
if err != nil {
log.WithError(err).Error("Failed to create new generic session")
return nil, err
}
log.WithFields(logrus.Fields{
"id": id,
"localPort": lport,
"remoteUDPAddr": rUDPAddr,
}).Debug("Created new RawSession")
rawSession := &RawSession{
SAM: s,
}
rawSession.Conn = conn
return rawSession, nil
}

312
raw/read.go Normal file
View File

@@ -0,0 +1,312 @@
package raw
import (
"bufio"
"encoding/base64"
"strings"
"sync/atomic"
"time"
"github.com/go-i2p/i2pkeys"
"github.com/go-i2p/logger"
"github.com/samber/oops"
)
// ReceiveDatagram receives a raw datagram from any source
func (r *RawReader) ReceiveDatagram() (*RawDatagram, error) {
// Check if closed first, but don't rely on this check for safety
r.mu.RLock()
if r.closed {
r.mu.RUnlock()
return nil, oops.Errorf("reader is closed")
}
r.mu.RUnlock()
// Use select with closeChan to handle concurrent close operations safely
select {
case datagram := <-r.recvChan:
return datagram, nil
case err := <-r.errorChan:
return nil, err
case <-r.closeChan:
return nil, oops.Errorf("reader is closed")
}
}
// Close closes the RawReader and stops its receive loop, cleaning up all associated resources.
// This method is safe to call multiple times and will not block if the reader is already closed.
func (r *RawReader) Close() error {
r.mu.Lock()
defer r.mu.Unlock()
if r.closed {
return nil
}
// Safe session ID retrieval with nil checks for logging
sessionID := "unknown"
if r.session != nil && r.session.BaseSession != nil {
sessionID = r.session.ID()
}
logger := log.WithField("session_id", sessionID)
logger.Debug("Closing RawReader")
r.closed = true
// Set atomic flag to indicate we're closing
atomic.StoreInt32(&r.closing, 1)
// Signal termination to receiveLoop
close(r.closeChan)
// Wait for receiveLoop to signal it has exited
select {
case <-r.doneChan:
// receiveLoop has confirmed it stopped
case <-time.After(5 * time.Second):
// Timeout protection - log warning but continue cleanup
logger.Warn("Timeout waiting for receive loop to stop")
}
// Don't close doneChan - let the sender close it
// Close receiver channels here under mutex protection
close(r.recvChan)
close(r.errorChan)
logger.Debug("Successfully closed RawReader")
return nil
}
// receiveLoop continuously receives incoming raw datagrams in a separate goroutine.
// This method handles the SAM protocol communication, parses RAW RECEIVED responses,
// and forwards datagrams to the appropriate channels until the reader is closed.
func (r *RawReader) receiveLoop() {
logger := r.initializeReceiveLoop()
defer r.signalReceiveLoopCompletion()
if !r.validateSessionForReceive(logger) {
return
}
r.runMainReceiveLoop(logger)
}
// initializeReceiveLoop sets up logging and returns a configured logger for the receive loop.
func (r *RawReader) 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 raw receive loop")
return logger
}
// signalReceiveLoopCompletion signals that the receive loop has completed execution.
func (r *RawReader) signalReceiveLoopCompletion() {
// Close doneChan to signal completion - channels should be closed by sender
close(r.doneChan)
}
// validateSessionForReceive checks if the session is valid for receiving operations.
func (r *RawReader) validateSessionForReceive(logger *logger.Entry) bool {
if r.session == nil {
logger.Debug("Raw receive loop terminated - session is nil")
return false
}
r.session.mu.RLock()
defer r.session.mu.RUnlock()
if r.session.closed || r.session.BaseSession == nil {
logger.Debug("Raw receive loop terminated - session invalid")
return false
}
return true
}
// runMainReceiveLoop executes the main receive loop that processes datagrams until closed.
func (r *RawReader) runMainReceiveLoop(logger *logger.Entry) {
for {
if r.checkForClosure(logger) {
return
}
datagram, err := r.receiveDatagram()
if err != nil {
if r.handleReceiveError(err, logger) {
return
}
continue
}
if r.forwardDatagram(datagram, logger) {
return
}
}
}
// checkForClosure checks if the reader has been closed in a non-blocking way.
func (r *RawReader) checkForClosure(logger *logger.Entry) bool {
select {
case <-r.closeChan:
logger.Debug("Raw receive loop terminated - reader closed")
return true
default:
return false
}
}
// handleReceiveError handles errors that occur during datagram reception.
func (r *RawReader) handleReceiveError(err error, logger *logger.Entry) bool {
select {
case r.errorChan <- err:
logger.WithError(err).Error("Failed to receive raw datagram")
case <-r.closeChan:
// Reader was closed during error handling
return true
}
return false
}
// forwardDatagram forwards a received datagram to the receive channel.
func (r *RawReader) forwardDatagram(datagram *RawDatagram, logger *logger.Entry) bool {
// Check atomic flag first to avoid race condition
if atomic.LoadInt32(&r.closing) == 1 {
return true
}
select {
case r.recvChan <- datagram:
logger.Debug("Successfully received raw datagram")
case <-r.closeChan:
// Reader was closed during datagram send
return true
}
return false
}
// receiveDatagram handles the low-level protocol parsing for incoming raw datagrams.
// It reads from the SAM connection, parses the RAW RECEIVED response format,
// and constructs RawDatagram objects with decoded data and address information.
func (r *RawReader) receiveDatagram() (*RawDatagram, error) {
logger := log.WithField("session_id", r.session.ID())
// Validate session state before processing
if err := r.validateSessionState(); err != nil {
return nil, err
}
// Read raw response from SAM connection
response, err := r.readRawResponse()
if err != nil {
return nil, err
}
logger.WithField("response", response).Debug("Received raw datagram data")
// Parse the RAW RECEIVED response to extract source and data
source, data, err := r.parseRawResponse(response)
if err != nil {
return nil, err
}
// Create and return the raw datagram
return r.createRawDatagram(source, data)
}
// validateSessionState checks if the session is valid and ready for use.
func (r *RawReader) validateSessionState() error {
r.session.mu.RLock()
defer r.session.mu.RUnlock()
if r.session.closed {
return oops.Errorf("session is closed")
}
if r.session.BaseSession == nil {
return oops.Errorf("session is not properly initialized")
}
if r.session.BaseSession.Conn() == nil {
return oops.Errorf("session connection is not available")
}
return nil
}
// readRawResponse reads the raw response from the SAM connection.
func (r *RawReader) readRawResponse() (string, error) {
buf := make([]byte, 4096)
n, err := r.session.Read(buf)
if err != nil {
return "", oops.Errorf("failed to read from session: %w", err)
}
return string(buf[:n]), nil
}
// parseRawResponse parses the RAW RECEIVED response to extract source and data.
func (r *RawReader) parseRawResponse(response string) (source, data string, err error) {
scanner := bufio.NewScanner(strings.NewReader(response))
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
word := scanner.Text()
switch {
case word == "RAW":
continue
case word == "RECEIVED":
continue
case strings.HasPrefix(word, "DESTINATION="):
// Extract source destination from the response
source = word[12:]
case strings.HasPrefix(word, "SIZE="):
continue // We'll get the actual data size from the payload
default:
// Remaining data is the base64-encoded payload
if data == "" {
data = word
} else {
data += " " + word
}
}
}
// Validate that we have both source and data
if source == "" {
return "", "", oops.Errorf("no source in raw datagram")
}
if data == "" {
return "", "", oops.Errorf("no data in raw datagram")
}
return source, data, nil
}
// createRawDatagram creates a RawDatagram from source and data strings.
func (r *RawReader) createRawDatagram(source, data string) (*RawDatagram, error) {
// Parse the source destination into an I2P address
sourceAddr, err := i2pkeys.NewI2PAddrFromString(source)
if err != nil {
return nil, oops.Errorf("failed to parse source address: %w", err)
}
// Decode the base64 data into bytes
decodedData, err := base64.StdEncoding.DecodeString(data)
if err != nil {
return nil, oops.Errorf("failed to decode raw datagram data: %w", err)
}
// Create the raw datagram with decoded data and address information
datagram := &RawDatagram{
Data: decodedData,
Source: sourceAddr,
Local: r.session.Addr(),
}
return datagram, nil
}

61
raw/read_test.go Normal file
View File

@@ -0,0 +1,61 @@
package raw
import (
"sync"
"testing"
"github.com/go-i2p/go-sam-go/common"
)
func TestRawReader_ConcurrentClose(t *testing.T) {
// Test concurrent Close() calls don't panic
session := &RawSession{
BaseSession: &common.BaseSession{},
closed: false,
}
reader := &RawReader{
session: session,
recvChan: make(chan *RawDatagram, 10),
errorChan: make(chan error, 1),
closeChan: make(chan struct{}),
doneChan: make(chan struct{}),
closed: false,
mu: sync.RWMutex{},
}
// Start receive loop
go reader.receiveLoop()
// Simulate concurrent close attempts
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = reader.Close() // Should not panic
}()
}
wg.Wait()
// Verify reader is properly closed
if !reader.closed {
t.Error("Reader should be marked as closed")
}
}
func TestRawReader_CloseRaceCondition(t *testing.T) {
// Test that rapid close after start doesn't cause channel panic
for i := 0; i < 100; i++ {
session := &RawSession{closed: false}
reader := session.NewReader()
go reader.receiveLoop()
// Close immediately to trigger race condition
if err := reader.Close(); err != nil {
t.Errorf("Close() failed: %v", err)
}
}
}

173
raw/session.go Normal file
View File

@@ -0,0 +1,173 @@
package raw
import (
"net"
"sync"
"github.com/go-i2p/go-sam-go/common"
"github.com/go-i2p/i2pkeys"
"github.com/samber/oops"
"github.com/sirupsen/logrus"
)
// 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"})
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")
// 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")
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()
return nil, oops.Errorf("invalid session type")
}
rs := &RawSession{
BaseSession: baseSession,
sam: sam,
options: options,
}
logger.Debug("Successfully created RawSession")
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 {
return &RawConn{
session: s,
reader: s.NewReader(),
writer: s.NewWriter(),
}
}
// 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.
// 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()
func (s *RawSession) ReceiveDatagram() (*RawDatagram, error) {
reader := s.NewReader()
if reader == nil {
return nil, oops.Errorf("session is closed")
}
// Start the receive loop for this reader
go reader.receiveLoop()
// Get one datagram and then close the reader to clean up the goroutine
datagram, err := reader.ReceiveDatagram()
reader.Close()
return datagram, err
}
// 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()
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 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()
}

274
raw/session_test.go Normal file
View File

@@ -0,0 +1,274 @@
package raw
import (
"testing"
"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
}
func TestNewRawSession(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
tests := []struct {
name string
id string
options []string
wantErr bool
}{
{
name: "basic session creation",
id: "test_raw_session",
options: nil,
wantErr: false,
},
{
name: "session with options",
id: "test_raw_with_opts",
options: []string{"inbound.length=1", "outbound.length=1"},
wantErr: false,
},
{
name: "session with small tunnel config",
id: "test_raw_small",
options: []string{
"inbound.length=0",
"outbound.length=0",
"inbound.lengthVariance=0",
"outbound.lengthVariance=0",
"inbound.quantity=1",
"outbound.quantity=1",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewRawSession(sam, tt.id, keys, tt.options)
if (err != nil) != tt.wantErr {
t.Errorf("NewRawSession() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil {
// Verify session properties
if session.ID() != tt.id {
t.Errorf("Session ID = %v, want %v", session.ID(), tt.id)
}
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")
}
// Clean up
if err := session.Close(); err != nil {
t.Errorf("Failed to close session: %v", err)
}
}
})
}
}
func TestRawSession_Close(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewRawSession(sam, "test_close", 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
err = session.Close()
if err != nil {
t.Errorf("Second Close() error = %v", err)
}
}
func TestRawSession_Addr(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewRawSession(sam, "test_addr", 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())
}
}
func TestRawSession_NewReader(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewRawSession(sam, "test_reader", 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")
}
}
func TestRawSession_NewWriter(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewRawSession(sam, "test_writer", 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)
}
}
func TestRawSession_PacketConn(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewRawSession(sam, "test_packetconn", 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")
}
rawConn, ok := conn.(*RawConn)
if !ok {
t.Error("PacketConn() did not return a RawConn")
}
if rawConn.session != session {
t.Error("RawConn session reference is incorrect")
}
if rawConn.reader == nil {
t.Error("RawConn reader is nil")
}
if rawConn.writer == nil {
t.Error("RawConn writer is nil")
}
}
func TestRawAddr_Network(t *testing.T) {
addr := &RawAddr{}
if addr.Network() != "i2p-raw" {
t.Errorf("Network() = %v, want i2p-raw", addr.Network())
}
}
func TestRawAddr_String(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
sam, keys := setupTestSAM(t)
defer sam.Close()
addr := &RawAddr{addr: keys.Addr()}
expected := keys.Addr().Base32()
if addr.String() != expected {
t.Errorf("String() = %v, want %v", addr.String(), expected)
}
}

View File

@@ -1,21 +1,69 @@
package raw
import (
"net"
"sync"
"time"
"github.com/go-i2p/go-sam-go/common"
"github.com/go-i2p/i2pkeys"
)
type SAM common.SAM
// The RawSession provides no authentication of senders, and there is no sender
// address attached to datagrams, so all communication is anonymous. The
// messages send are however still endpoint-to-endpoint encrypted. You
// need to figure out a way to identify and authenticate clients yourself, iff
// that is needed. Raw datagrams may be at most 32 kB in size. There is no
// overhead of authentication, which is the reason to use this..
// RawSession represents a raw session that can send and receive raw datagrams
type RawSession struct {
*SAM
SAMUDPConn *net.UDPConn // used to deliver datagrams
SAMUDPAddr *net.UDPAddr // the SAM bridge UDP-port
*common.BaseSession
sam *common.SAM
options []string
mu sync.RWMutex
closed bool
}
// RawReader handles incoming raw datagram reception
type RawReader struct {
session *RawSession
recvChan chan *RawDatagram
errorChan chan error
closeChan chan struct{}
doneChan chan struct{}
closed bool
closing int32 // atomic flag to coordinate channel closure
mu sync.RWMutex
}
// RawWriter handles outgoing raw datagram transmission
type RawWriter struct {
session *RawSession
timeout time.Duration
}
// RawDatagram represents an I2P raw datagram message
type RawDatagram struct {
Data []byte
Source i2pkeys.I2PAddr
Local i2pkeys.I2PAddr
}
// RawAddr implements net.Addr for I2P raw addresses
type RawAddr struct {
addr i2pkeys.I2PAddr
}
// RawConn implements net.PacketConn for I2P raw datagrams
type RawConn struct {
session *RawSession
reader *RawReader
writer *RawWriter
remoteAddr *i2pkeys.I2PAddr
mu sync.RWMutex
closed bool
}
// RawListener implements net.Listener for I2P raw connections
type RawListener struct {
session *RawSession
reader *RawReader
acceptChan chan *RawConn
errorChan chan error
closeChan chan struct{}
closed bool
mu sync.RWMutex
}

13
raw/types_test.go Normal file
View File

@@ -0,0 +1,13 @@
package raw
import (
"net"
"github.com/go-i2p/go-sam-go/common"
)
var (
ds common.Session = &RawSession{}
dl net.Listener = &RawListener{}
dc net.PacketConn = &RawConn{}
)

111
raw/write.go Normal file
View File

@@ -0,0 +1,111 @@
package raw
import (
"encoding/base64"
"fmt"
"strings"
"time"
"github.com/go-i2p/i2pkeys"
"github.com/samber/oops"
"github.com/sirupsen/logrus"
)
// SetTimeout sets the timeout for raw datagram operations.
// This method configures the maximum time to wait for send operations to complete.
// It returns the writer instance for method chaining.
// Example usage: writer.SetTimeout(30*time.Second).SendDatagram(data, dest)
func (w *RawWriter) SetTimeout(timeout time.Duration) *RawWriter {
w.timeout = timeout
return w
}
// SendDatagram sends a raw datagram to the specified destination.
// This method handles the complete send operation including data encoding,
// SAM protocol communication, and response parsing for error handling.
// Example usage: err := writer.SendDatagram([]byte("hello"), destAddr)
func (w *RawWriter) SendDatagram(data []byte, dest i2pkeys.I2PAddr) error {
w.session.mu.RLock()
if w.session.closed {
w.session.mu.RUnlock()
return oops.Errorf("session is closed")
}
w.session.mu.RUnlock()
logger := log.WithFields(logrus.Fields{
"session_id": w.session.ID(),
"destination": dest.Base32(),
"size": len(data),
})
logger.Debug("Sending raw datagram")
// Encode the data as base64 for SAM protocol transmission
encodedData := base64.StdEncoding.EncodeToString(data)
// Create the RAW SEND command following SAMv3 protocol format
// The command includes session ID, destination, size, and base64-encoded data
sendCmd := fmt.Sprintf("RAW SEND ID=%s DESTINATION=%s SIZE=%d\n%s\n",
w.session.ID(), dest.Base64(), len(data), encodedData)
logger.WithField("command", strings.Split(sendCmd, "\n")[0]).Debug("Sending RAW SEND")
// Send the command to the SAM bridge over the session connection
_, err := w.session.Write([]byte(sendCmd))
if err != nil {
logger.WithError(err).Error("Failed to send raw datagram")
return oops.Errorf("failed to send raw datagram: %w", err)
}
// Read the response from the SAM bridge to determine send status
buf := make([]byte, 1024)
n, err := w.session.Read(buf)
if err != nil {
logger.WithError(err).Error("Failed to read send response")
return oops.Errorf("failed to read send response: %w", err)
}
response := string(buf[:n])
logger.WithField("response", response).Debug("Received send response")
// Parse the response to check for errors and handle failure conditions
if err := w.parseSendResponse(response); err != nil {
return err
}
logger.Debug("Successfully sent raw datagram")
return nil
}
// parseSendResponse parses the RAW STATUS response from the SAM bridge after sending a datagram.
// It examines the response string to determine if the send operation was successful or failed,
// and returns appropriate error messages for different failure conditions like unreachable peers,
// invalid keys, timeouts, and other I2P network errors.
// Example response: "RAW STATUS RESULT=OK" or "RAW STATUS RESULT=CANT_REACH_PEER"
func (w *RawWriter) parseSendResponse(response string) error {
// Check for successful send operation first
if strings.Contains(response, "RESULT=OK") {
return nil
}
// Handle specific error conditions returned by the SAM bridge
// These errors provide meaningful feedback about I2P network failures
switch {
case strings.Contains(response, "RESULT=CANT_REACH_PEER"):
return oops.Errorf("cannot reach peer")
case strings.Contains(response, "RESULT=I2P_ERROR"):
return oops.Errorf("I2P internal error")
case strings.Contains(response, "RESULT=INVALID_KEY"):
return oops.Errorf("invalid destination key")
case strings.Contains(response, "RESULT=INVALID_ID"):
return oops.Errorf("invalid session ID")
case strings.Contains(response, "RESULT=TIMEOUT"):
return oops.Errorf("send timeout")
default:
// Handle unknown error responses by extracting the result portion
if strings.HasPrefix(response, "RAW STATUS RESULT=") {
result := strings.TrimSpace(response[18:])
return oops.Errorf("send failed: %s", result)
}
return oops.Errorf("unexpected response format: %s", response)
}
}

View File

@@ -1,25 +0,0 @@
// package sam3 wraps the original sam3 API from github.com/go-i2p/sam3
package sam3
type SAMResolver struct {
*SAM
}
func NewSAMResolver(parent *SAM) (*SAMResolver, error) {
log.Debug("Creating new SAMResolver from existing SAM instance")
var s SAMResolver
s.SAM = parent
return &s, nil
}
func NewFullSAMResolver(address string) (*SAMResolver, error) {
log.WithField("address", address).Debug("Creating new full SAMResolver")
var s SAMResolver
var err error
s.SAM, err = NewSAM(address)
if err != nil {
log.WithError(err).Error("Failed to create new SAM instance")
return nil, err
}
return &s, nil
}

91
sam3.go
View File

@@ -1,91 +0,0 @@
// package sam3 wraps the original sam3 API from github.com/go-i2p/sam3
package sam3
import (
"math/rand"
"github.com/go-i2p/go-sam-go/common"
"github.com/go-i2p/go-sam-go/datagram"
"github.com/go-i2p/go-sam-go/primary"
"github.com/go-i2p/go-sam-go/stream"
"github.com/go-i2p/i2pkeys"
)
// Used for controlling I2Ps SAMv3.
type SAM struct {
*common.SAM
}
// Creates a new stream session by wrapping stream.NewStreamSession
func (s *SAM) NewStreamSession(param1 string, keys i2pkeys.I2PKeys, param3 []string) (*StreamSession, error) {
sam := &stream.SAM{
SAM: s.SAM,
}
ss, err := sam.NewStreamSession(param1, keys, param3)
if err != nil {
return nil, err
}
streamSession := &StreamSession{
StreamSession: ss,
}
return streamSession, nil
}
// Creates a new Datagram session by wrapping datagram.NewDatagramSession
func (s *SAM) NewDatagramSession(id string, keys i2pkeys.I2PKeys, options []string, port int) (*DatagramSession, error) {
sam := datagram.SAM(*s.SAM)
dgs, err := sam.NewDatagramSession(id, keys, options, port)
if err != nil {
return nil, err
}
datagramSession := DatagramSession{
DatagramSession: *dgs,
}
return &datagramSession, nil
}
func (s *SAM) NewPrimarySession(id string, keys i2pkeys.I2PKeys, options []string) (*PrimarySession, error) {
sam := primary.SAM(*s.SAM)
ps, err := sam.NewPrimarySession(id, keys, options)
if err != nil {
return nil, err
}
primarySession := PrimarySession{
PrimarySession: ps,
}
return &primarySession, nil
}
const (
Sig_NONE = "SIGNATURE_TYPE=EdDSA_SHA512_Ed25519"
Sig_DSA_SHA1 = "SIGNATURE_TYPE=DSA_SHA1"
Sig_ECDSA_SHA256_P256 = "SIGNATURE_TYPE=ECDSA_SHA256_P256"
Sig_ECDSA_SHA384_P384 = "SIGNATURE_TYPE=ECDSA_SHA384_P384"
Sig_ECDSA_SHA512_P521 = "SIGNATURE_TYPE=ECDSA_SHA512_P521"
Sig_EdDSA_SHA512_Ed25519 = "SIGNATURE_TYPE=EdDSA_SHA512_Ed25519"
)
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func RandString() string {
n := 4
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
log.WithField("randomString", string(b)).Debug("Generated random string")
return string(b)
}
// Creates a new controller for the I2P routers SAM bridge.
func NewSAM(address string) (*SAM, error) {
is, err := common.NewSAM(address)
if err != nil {
log.WithError(err).Error("Failed to create new SAM instance")
return nil, err
}
s := &SAM{
SAM: is,
}
return s, nil
}

View File

@@ -1,165 +0,0 @@
// package sam3 wraps the original sam3 API from github.com/go-i2p/sam3
package sam3
import (
"fmt"
"testing"
"time"
)
const yoursam = "127.0.0.1:7656"
func Test_Basic(t *testing.T) {
fmt.Println("Test_Basic")
fmt.Println("\tAttaching to SAM at " + yoursam)
sam, err := NewSAM(yoursam)
if err != nil {
fmt.Println(err.Error())
t.Fail()
return
}
fmt.Println("\tCreating new keys...")
keys, err := sam.NewKeys()
if err != nil {
fmt.Println(err.Error())
t.Fail()
} else {
fmt.Println("\tAddress created: " + keys.Addr().Base32())
fmt.Println("\tI2PKeys: " + string(keys.String())[:50] + "(...etc)")
}
addr2, err := sam.Lookup("zzz.i2p")
if err != nil {
fmt.Println(err.Error())
t.Fail()
} else {
fmt.Println("\tzzz.i2p = " + addr2.Base32())
}
if err := sam.Close(); err != nil {
fmt.Println(err.Error())
t.Fail()
}
}
/*
func Test_GenericSession(t *testing.T) {
if testing.Short() {
return
}
fmt.Println("Test_GenericSession")
sam, err := NewSAM(yoursam)
if err != nil {
fmt.Println(err.Error)
t.Fail()
return
}
keys, err := sam.NewKeys()
if err != nil {
fmt.Println(err.Error())
t.Fail()
} else {
conn1, err := sam.newGenericSession("STREAM", "testTun", keys, []string{})
if err != nil {
fmt.Println(err.Error())
t.Fail()
} else {
conn1.Close()
}
conn2, err := sam.newGenericSession("STREAM", "testTun", keys, []string{"inbound.length=1", "outbound.length=1", "inbound.lengthVariance=1", "outbound.lengthVariance=1", "inbound.quantity=1", "outbound.quantity=1"})
if err != nil {
fmt.Println(err.Error())
t.Fail()
} else {
conn2.Close()
}
conn3, err := sam.newGenericSession("DATAGRAM", "testTun", keys, []string{"inbound.length=1", "outbound.length=1", "inbound.lengthVariance=1", "outbound.lengthVariance=1", "inbound.quantity=1", "outbound.quantity=1"})
if err != nil {
fmt.Println(err.Error())
t.Fail()
} else {
conn3.Close()
}
}
if err := sam.Close(); err != nil {
fmt.Println(err.Error())
t.Fail()
}
}
*/
func Test_RawServerClient(t *testing.T) {
if testing.Short() {
return
}
fmt.Println("Test_RawServerClient")
sam, err := NewSAM(yoursam)
if err != nil {
t.Fail()
return
}
defer sam.Close()
keys, err := sam.NewKeys()
if err != nil {
t.Fail()
return
}
fmt.Println("\tServer: Creating tunnel")
rs, err := sam.NewDatagramSession("RAWserverTun", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}, 0)
if err != nil {
fmt.Println("Server: Failed to create tunnel: " + err.Error())
t.Fail()
return
}
c, w := make(chan bool), make(chan bool)
go func(c, w chan (bool)) {
sam2, err := NewSAM(yoursam)
if err != nil {
c <- false
return
}
defer sam2.Close()
keys, err := sam2.NewKeys()
if err != nil {
c <- false
return
}
fmt.Println("\tClient: Creating tunnel")
rs2, err := sam2.NewDatagramSession("RAWclientTun", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}, 0)
if err != nil {
c <- false
return
}
defer rs2.Close()
fmt.Println("\tClient: Tries to send raw datagram to server")
for {
select {
default:
_, err = rs2.WriteTo([]byte("Hello raw-world! <3 <3 <3 <3 <3 <3"), rs.LocalAddr())
if err != nil {
fmt.Println("\tClient: Failed to send raw datagram: " + err.Error())
c <- false
return
}
time.Sleep(5 * time.Second)
case <-w:
fmt.Println("\tClient: Sent raw datagram, quitting.")
return
}
}
c <- true
}(c, w)
buf := make([]byte, 512)
fmt.Println("\tServer: Read() waiting...")
n, _, err := rs.ReadFrom(buf)
w <- true
if err != nil {
fmt.Println("\tServer: Failed to Read(): " + err.Error())
t.Fail()
return
}
fmt.Println("\tServer: Received datagram: " + string(buf[:n]))
// fmt.Println("\tServer: Senders address was: " + saddr.Base32())
}

1461
sigs.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +0,0 @@
// package sam3 wraps the original sam3 API from github.com/go-i2p/sam3
package sam3
import (
"github.com/go-i2p/go-sam-go/stream"
)
// Represents a streaming session.
type StreamSession struct {
*stream.StreamSession
}
/*
func (s *StreamSession) Cancel() chan *StreamSession {
ch := make(chan *StreamSession)
ch <- s
return ch
}*/

252
stream/DOC.md Normal file
View File

@@ -0,0 +1,252 @@
# stream
--
import "github.com/go-i2p/go-sam-go/stream"
## Usage
#### type SAM
```go
type SAM struct {
*common.SAM
}
```
SAM wraps common.SAM to provide stream-specific functionality
#### func (*SAM) NewStreamSession
```go
func (s *SAM) NewStreamSession(id string, keys i2pkeys.I2PKeys, options []string) (*StreamSession, error)
```
NewStreamSession creates a new streaming session with the SAM bridge
#### func (*SAM) NewStreamSessionWithPorts
```go
func (s *SAM) NewStreamSessionWithPorts(id, fromPort, toPort string, keys i2pkeys.I2PKeys, options []string) (*StreamSession, error)
```
NewStreamSessionWithPorts creates a new streaming session with port
specifications
#### func (*SAM) NewStreamSessionWithSignature
```go
func (s *SAM) NewStreamSessionWithSignature(id string, keys i2pkeys.I2PKeys, options []string, sigType string) (*StreamSession, error)
```
NewStreamSessionWithSignature creates a new streaming session with custom
signature type
#### type StreamConn
```go
type StreamConn struct {
}
```
StreamConn implements net.Conn for I2P streaming connections
#### func (*StreamConn) Close
```go
func (c *StreamConn) Close() error
```
Close closes the connection
#### func (*StreamConn) LocalAddr
```go
func (c *StreamConn) LocalAddr() net.Addr
```
LocalAddr returns the local network address
#### func (*StreamConn) Read
```go
func (c *StreamConn) Read(b []byte) (int, error)
```
Read reads data from the connection
#### func (*StreamConn) RemoteAddr
```go
func (c *StreamConn) RemoteAddr() net.Addr
```
RemoteAddr returns the remote network address
#### func (*StreamConn) SetDeadline
```go
func (c *StreamConn) SetDeadline(t time.Time) error
```
SetDeadline sets the read and write deadlines
#### func (*StreamConn) SetReadDeadline
```go
func (c *StreamConn) SetReadDeadline(t time.Time) error
```
SetReadDeadline sets the deadline for future Read calls
#### func (*StreamConn) SetWriteDeadline
```go
func (c *StreamConn) SetWriteDeadline(t time.Time) error
```
SetWriteDeadline sets the deadline for future Write calls
#### func (*StreamConn) Write
```go
func (c *StreamConn) Write(b []byte) (int, error)
```
Write writes data to the connection
#### type StreamDialer
```go
type StreamDialer struct {
}
```
StreamDialer handles client-side connection establishment
#### func (*StreamDialer) Dial
```go
func (d *StreamDialer) Dial(destination string) (*StreamConn, error)
```
Dial establishes a connection to the specified destination
#### func (*StreamDialer) DialContext
```go
func (d *StreamDialer) DialContext(ctx context.Context, destination string) (*StreamConn, error)
```
DialContext establishes a connection with context support
#### func (*StreamDialer) DialI2P
```go
func (d *StreamDialer) DialI2P(addr i2pkeys.I2PAddr) (*StreamConn, error)
```
DialI2P establishes a connection to the specified I2P address
#### func (*StreamDialer) DialI2PContext
```go
func (d *StreamDialer) DialI2PContext(ctx context.Context, addr i2pkeys.I2PAddr) (*StreamConn, error)
```
DialI2PContext establishes a connection to an I2P address with context support
#### func (*StreamDialer) SetTimeout
```go
func (d *StreamDialer) SetTimeout(timeout time.Duration) *StreamDialer
```
SetTimeout sets the default timeout for new dialers
#### type StreamListener
```go
type StreamListener struct {
}
```
StreamListener implements net.Listener for I2P streaming connections
#### func (*StreamListener) Accept
```go
func (l *StreamListener) Accept() (net.Conn, error)
```
Accept waits for and returns the next connection to the listener
#### func (*StreamListener) AcceptStream
```go
func (l *StreamListener) AcceptStream() (*StreamConn, error)
```
AcceptStream waits for and returns the next I2P streaming connection
#### func (*StreamListener) Addr
```go
func (l *StreamListener) Addr() net.Addr
```
Addr returns the listener's network address
#### func (*StreamListener) Close
```go
func (l *StreamListener) Close() error
```
Close closes the listener
#### type StreamSession
```go
type StreamSession struct {
*common.BaseSession
}
```
StreamSession represents a streaming session that can create listeners and
dialers
#### func NewStreamSession
```go
func NewStreamSession(sam *common.SAM, id string, keys i2pkeys.I2PKeys, options []string) (*StreamSession, error)
```
NewStreamSession creates a new streaming session
#### func (*StreamSession) Addr
```go
func (s *StreamSession) Addr() i2pkeys.I2PAddr
```
Addr returns the I2P address of this session
#### func (*StreamSession) Close
```go
func (s *StreamSession) Close() error
```
Close closes the streaming session and all associated resources
#### func (*StreamSession) Dial
```go
func (s *StreamSession) Dial(destination string) (*StreamConn, error)
```
Dial establishes a connection to the specified I2P destination
#### func (*StreamSession) DialContext
```go
func (s *StreamSession) DialContext(ctx context.Context, destination string) (*StreamConn, error)
```
DialContext establishes a connection with context support
#### func (*StreamSession) DialI2P
```go
func (s *StreamSession) DialI2P(addr i2pkeys.I2PAddr) (*StreamConn, error)
```
DialI2P establishes a connection to the specified I2P address
#### func (*StreamSession) Listen
```go
func (s *StreamSession) Listen() (*StreamListener, error)
```
Listen creates a StreamListener that accepts incoming connections
#### func (*StreamSession) NewDialer
```go
func (s *StreamSession) NewDialer() *StreamDialer
```
NewDialer creates a StreamDialer for establishing outbound connections

23
stream/README.md Normal file
View File

@@ -0,0 +1,23 @@
# go-sam-go/stream
High-level streaming library for reliable TCP-like connections over I2P using the SAMv3 protocol.
## Installation
Install using Go modules with the package path `github.com/go-i2p/go-sam-go/stream`.
## 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.
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.
## 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/sirupsen/logrus - Structured logging
- github.com/samber/oops - Enhanced error handling

97
stream/SAM.go Normal file
View File

@@ -0,0 +1,97 @@
package stream
import (
"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 stream-specific functionality and convenience methods.
// It extends the base SAM connection with streaming-specific session creation methods,
// providing a more convenient API for creating streaming sessions without requiring
// direct interaction with the generic session creation methods.
// Example usage: sam := &SAM{SAM: commonSAM}; session, err := sam.NewStreamSession("id", keys, options)
type SAM struct {
*common.SAM
}
// NewStreamSession creates a new streaming session with the SAM bridge using default signature.
// This is a convenience method that wraps the generic session creation with streaming-specific
// parameters. It uses the default Ed25519 signature type and provides a simpler API for
// creating streaming sessions without requiring explicit signature type specification.
// Example usage: session, err := sam.NewStreamSession("my-session", keys, options)
func (s *SAM) NewStreamSession(id string, keys i2pkeys.I2PKeys, options []string) (*StreamSession, error) {
return NewStreamSession(s.SAM, id, keys, options)
}
// NewStreamSessionWithSignature creates a new streaming session with custom signature type.
// This method provides advanced control over the cryptographic signature type used for
// the I2P destination. It supports various signature algorithms like Ed25519, ECDSA,
// and DSA, allowing applications to choose the most appropriate signature type for their needs.
// Example usage: session, err := sam.NewStreamSessionWithSignature("my-session", keys, options, "EdDSA_SHA512_Ed25519")
func (s *SAM) NewStreamSessionWithSignature(id string, keys i2pkeys.I2PKeys, options []string, sigType string) (*StreamSession, error) {
logger := log.WithFields(logrus.Fields{
"id": id,
"options": options,
"sigType": sigType,
})
logger.Debug("Creating new StreamSession with signature")
// Create the base session using the common package with signature
session, err := s.SAM.NewGenericSessionWithSignature("STREAM", 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 stream session: %w", err)
}
baseSession, ok := session.(*common.BaseSession)
if !ok {
logger.Error("Session is not a BaseSession")
session.Close()
return nil, oops.Errorf("invalid session type")
}
ss := &StreamSession{
BaseSession: baseSession,
sam: s.SAM,
options: options,
}
logger.Debug("Successfully created StreamSession with signature")
return ss, nil
}
// NewStreamSessionWithPorts creates a new streaming session with port specifications
func (s *SAM) NewStreamSessionWithPorts(id, fromPort, toPort string, keys i2pkeys.I2PKeys, options []string) (*StreamSession, error) {
logger := log.WithFields(logrus.Fields{
"id": id,
"fromPort": fromPort,
"toPort": toPort,
"options": options,
})
logger.Debug("Creating new StreamSession with ports")
// Create the base session using the common package with ports
session, err := s.SAM.NewGenericSessionWithSignatureAndPorts("STREAM", id, fromPort, toPort, keys, common.SIG_EdDSA_SHA512_Ed25519, options)
if err != nil {
logger.WithError(err).Error("Failed to create generic session with ports")
return nil, oops.Errorf("failed to create stream session: %w", err)
}
baseSession, ok := session.(*common.BaseSession)
if !ok {
logger.Error("Session is not a BaseSession")
session.Close()
return nil, oops.Errorf("invalid session type")
}
ss := &StreamSession{
BaseSession: baseSession,
sam: s.SAM,
options: options,
}
logger.Debug("Successfully created StreamSession with ports")
return ss, nil
}

View File

@@ -4,55 +4,151 @@ import (
"net"
"time"
"github.com/go-i2p/i2pkeys"
"github.com/samber/oops"
"github.com/sirupsen/logrus"
)
// Implements net.Conn
func (sc *StreamConn) Read(buf []byte) (int, error) {
n, err := sc.conn.Read(buf)
// Read reads data from the connection into the provided buffer.
// This method implements the net.Conn interface and provides thread-safe reading
// from the underlying I2P streaming connection. It handles connection state checking
// and proper error reporting for closed connections.
// Example usage: n, err := conn.Read(buffer)
func (c *StreamConn) Read(b []byte) (int, error) {
c.mu.RLock()
if c.closed {
c.mu.RUnlock()
return 0, oops.Errorf("connection is closed")
}
conn := c.conn
c.mu.RUnlock()
n, err := conn.Read(b)
if err != nil {
log.WithFields(logrus.Fields{
"local": c.laddr.Base32(),
"remote": c.raddr.Base32(),
}).WithError(err).Debug("Read error")
}
return n, err
}
// Implements net.Conn
func (sc *StreamConn) Write(buf []byte) (int, error) {
n, err := sc.conn.Write(buf)
// Write writes data to the connection from the provided buffer.
// This method implements the net.Conn interface and provides thread-safe writing
// to the underlying I2P streaming connection. It handles connection state checking
// and proper error reporting for closed connections.
// Example usage: n, err := conn.Write(data)
func (c *StreamConn) Write(b []byte) (int, error) {
c.mu.RLock()
if c.closed {
c.mu.RUnlock()
return 0, oops.Errorf("connection is closed")
}
conn := c.conn
c.mu.RUnlock()
n, err := conn.Write(b)
if err != nil {
log.WithFields(logrus.Fields{
"local": c.laddr.Base32(),
"remote": c.raddr.Base32(),
}).WithError(err).Debug("Write error")
}
return n, err
}
// Implements net.Conn
func (sc *StreamConn) Close() error {
return sc.conn.Close()
// Close closes the connection and releases all associated resources.
// This method implements the net.Conn interface and is safe to call multiple times.
// It properly handles concurrent access and ensures clean shutdown of the underlying
// I2P streaming connection with appropriate error handling.
// Example usage: defer conn.Close()
func (c *StreamConn) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.closed {
return nil
}
logger := log.WithFields(logrus.Fields{
"local": c.laddr.Base32(),
"remote": c.raddr.Base32(),
})
logger.Debug("Closing StreamConn")
c.closed = true
if c.conn != nil {
err := c.conn.Close()
if err != nil {
logger.WithError(err).Error("Failed to close underlying connection")
return oops.Errorf("failed to close connection: %w", err)
}
}
logger.Debug("Successfully closed StreamConn")
return nil
}
func (sc *StreamConn) LocalAddr() net.Addr {
return sc.localAddr()
// LocalAddr returns the local network address of the connection.
// This method implements the net.Conn interface and provides the I2P address
// of the local endpoint. The returned address implements the net.Addr interface
// and can be used for logging or connection management.
// Example usage: localAddr := conn.LocalAddr()
func (c *StreamConn) LocalAddr() net.Addr {
return &i2pAddr{addr: c.laddr}
}
// Implements net.Conn
func (sc *StreamConn) localAddr() i2pkeys.I2PAddr {
return sc.laddr
// RemoteAddr returns the remote network address of the connection.
// This method implements the net.Conn interface and provides the I2P address
// of the remote endpoint. The returned address implements the net.Addr interface
// and can be used for logging, authentication, or connection management.
// Example usage: remoteAddr := conn.RemoteAddr()
func (c *StreamConn) RemoteAddr() net.Addr {
return &i2pAddr{addr: c.raddr}
}
func (sc *StreamConn) RemoteAddr() net.Addr {
return sc.remoteAddr()
// SetDeadline sets the read and write deadlines for the connection.
// This method implements the net.Conn interface and sets both read and write
// deadlines to the same time. It provides a convenient way to set overall
// connection timeouts for both read and write operations.
// Example usage: conn.SetDeadline(time.Now().Add(30*time.Second))
func (c *StreamConn) SetDeadline(t time.Time) error {
if err := c.SetReadDeadline(t); err != nil {
return err
}
return c.SetWriteDeadline(t)
}
// Implements net.Conn
func (sc *StreamConn) remoteAddr() i2pkeys.I2PAddr {
return sc.raddr
// SetReadDeadline sets the deadline for future Read calls on the connection.
// This method implements the net.Conn interface and allows setting read-specific
// timeouts. A zero time value disables the deadline, and the deadline applies
// to all future and pending Read calls.
// Example usage: conn.SetReadDeadline(time.Now().Add(30*time.Second))
func (c *StreamConn) SetReadDeadline(t time.Time) error {
c.mu.RLock()
conn := c.conn
c.mu.RUnlock()
if conn == nil {
return oops.Errorf("connection is nil")
}
return conn.SetReadDeadline(t)
}
// Implements net.Conn
func (sc *StreamConn) SetDeadline(t time.Time) error {
return sc.conn.SetDeadline(t)
}
// SetWriteDeadline sets the deadline for future Write calls on the connection.
// This method implements the net.Conn interface and allows setting write-specific
// timeouts. A zero time value disables the deadline, and the deadline applies
// to all future and pending Write calls.
// Example usage: conn.SetWriteDeadline(time.Now().Add(30*time.Second))
func (c *StreamConn) SetWriteDeadline(t time.Time) error {
c.mu.RLock()
conn := c.conn
c.mu.RUnlock()
// Implements net.Conn
func (sc *StreamConn) SetReadDeadline(t time.Time) error {
return sc.conn.SetReadDeadline(t)
}
if conn == nil {
return oops.Errorf("connection is nil")
}
// Implements net.Conn
func (sc *StreamConn) SetWriteDeadline(t time.Time) error {
return sc.conn.SetWriteDeadline(t)
return conn.SetWriteDeadline(t)
}

View File

@@ -1,11 +0,0 @@
package stream
const (
ResultOK = "RESULT=OK"
ResultCantReachPeer = "RESULT=CANT_REACH_PEER"
ResultI2PError = "RESULT=I2P_ERROR"
ResultInvalidKey = "RESULT=INVALID_KEY"
ResultInvalidID = "RESULT=INVALID_ID"
ResultTimeout = "RESULT=TIMEOUT"
StreamConnectCommand = "STREAM CONNECT ID="
)

236
stream/dialer.go Normal file
View File

@@ -0,0 +1,236 @@
package stream
import (
"bufio"
"context"
"fmt"
"strings"
"github.com/go-i2p/go-sam-go/common"
"github.com/go-i2p/i2pkeys"
"github.com/samber/oops"
"github.com/sirupsen/logrus"
)
// Dial establishes a connection to the specified destination using the default context.
// This method resolves the destination string and establishes a streaming connection
// using the dialer's configured timeout. It provides a simple interface for connection
// establishment without requiring explicit context management.
// Example usage: conn, err := dialer.Dial("destination.b32.i2p")
func (d *StreamDialer) Dial(destination string) (*StreamConn, error) {
return d.DialContext(context.Background(), destination)
}
// DialI2P establishes a connection to the specified I2P address using native addressing.
// This method accepts an i2pkeys.I2PAddr directly, bypassing the need for destination
// resolution. It uses the dialer's configured timeout and provides efficient connection
// establishment for known I2P addresses.
// Example usage: conn, err := dialer.DialI2P(addr)
func (d *StreamDialer) DialI2P(addr i2pkeys.I2PAddr) (*StreamConn, error) {
return d.DialI2PContext(context.Background(), addr)
}
// DialContext establishes a connection with context support for cancellation and timeout.
// This method resolves the destination string and establishes a streaming connection
// with context-based cancellation support. The context can override the dialer's
// default timeout and provides fine-grained control over connection establishment.
// Example usage: conn, err := dialer.DialContext(ctx, "destination.b32.i2p")
func (d *StreamDialer) DialContext(ctx context.Context, destination string) (*StreamConn, error) {
// First resolve the destination
addr, err := d.session.sam.Lookup(destination)
if err != nil {
return nil, oops.Errorf("failed to resolve destination %s: %w", destination, err)
}
return d.DialI2PContext(ctx, addr)
}
// DialI2PContext establishes a connection to an I2P address with context support.
// This method provides the core dialing functionality with context-based cancellation
// and timeout support. It handles SAM protocol communication, connection establishment,
// and proper resource management for streaming connections over I2P.
// Example usage: conn, err := dialer.DialI2PContext(ctx, addr)
func (d *StreamDialer) DialI2PContext(ctx context.Context, addr i2pkeys.I2PAddr) (*StreamConn, error) {
if err := d.validateSessionState(); err != nil {
return nil, err
}
d.logDialAttempt(addr)
sam, err := d.createSAMConnection()
if err != nil {
return nil, err
}
ctx, cancel := d.setupTimeout(ctx)
if cancel != nil {
defer cancel()
}
return d.performAsyncDial(ctx, sam, addr)
}
// validateSessionState checks if the session is valid and ready for dialing.
func (d *StreamDialer) validateSessionState() error {
d.session.mu.RLock()
defer d.session.mu.RUnlock()
if d.session.closed {
return oops.Errorf("session is closed")
}
return nil
}
// logDialAttempt logs the dial attempt with appropriate context fields.
func (d *StreamDialer) logDialAttempt(addr i2pkeys.I2PAddr) {
log.WithFields(logrus.Fields{
"session_id": d.session.ID(),
"destination": addr.Base32(),
}).Debug("Dialing I2P destination")
}
// createSAMConnection creates a new SAM connection for the dial operation.
func (d *StreamDialer) createSAMConnection() (*common.SAM, error) {
sam, err := common.NewSAM(d.session.sam.Sam())
if err != nil {
log.WithError(err).Error("Failed to create SAM connection")
return nil, oops.Errorf("failed to create SAM connection: %w", err)
}
return sam, nil
}
// setupTimeout configures context timeout if specified in the dialer.
func (d *StreamDialer) setupTimeout(ctx context.Context) (context.Context, context.CancelFunc) {
if d.timeout > 0 {
return context.WithTimeout(ctx, d.timeout)
}
return ctx, nil
}
// performAsyncDial executes the dial operation asynchronously with proper cancellation support.
func (d *StreamDialer) performAsyncDial(ctx context.Context, sam *common.SAM, addr i2pkeys.I2PAddr) (*StreamConn, error) {
connChan := make(chan *StreamConn, 1)
errChan := make(chan error, 1)
go func() {
conn, err := d.performDial(sam, addr)
if err != nil {
errChan <- err
return
}
connChan <- conn
}()
return d.handleDialResult(ctx, sam, connChan, errChan)
}
// handleDialResult manages the result of the dial operation with timeout and cancellation support.
func (d *StreamDialer) handleDialResult(ctx context.Context, sam *common.SAM, connChan chan *StreamConn, errChan chan error) (*StreamConn, error) {
select {
case conn := <-connChan:
log.Debug("Successfully established connection")
return conn, nil
case err := <-errChan:
sam.Close()
log.WithError(err).Error("Failed to establish connection")
return nil, err
case <-ctx.Done():
sam.Close()
log.Error("Connection attempt timed out")
return nil, oops.Errorf("connection attempt timed out: %w", ctx.Err())
}
}
// performDial handles the actual SAM protocol for establishing connections
func (d *StreamDialer) performDial(sam *common.SAM, addr i2pkeys.I2PAddr) (*StreamConn, error) {
if err := d.sendStreamConnectCommand(sam, addr); err != nil {
return nil, err
}
response, err := d.readStreamConnectResponse(sam)
if err != nil {
return nil, err
}
if err := d.parseConnectResponse(response); err != nil {
return nil, err
}
return d.createStreamConnection(sam, addr), nil
}
// sendStreamConnectCommand sends the STREAM CONNECT command to the SAM bridge.
func (d *StreamDialer) sendStreamConnectCommand(sam *common.SAM, addr i2pkeys.I2PAddr) error {
connectCmd := fmt.Sprintf("STREAM CONNECT ID=%s DESTINATION=%s SILENT=false\n",
d.session.ID(), addr.Base64())
log.WithFields(logrus.Fields{
"session_id": d.session.ID(),
"destination": addr.Base32(),
"command": strings.TrimSpace(connectCmd),
}).Debug("Sending STREAM CONNECT")
_, err := sam.Write([]byte(connectCmd))
if err != nil {
return oops.Errorf("failed to send STREAM CONNECT: %w", err)
}
return nil
}
// readStreamConnectResponse reads and logs the response from the SAM bridge.
func (d *StreamDialer) readStreamConnectResponse(sam *common.SAM) (string, error) {
buf := make([]byte, 4096)
n, err := sam.Read(buf)
if err != nil {
return "", oops.Errorf("failed to read STREAM CONNECT response: %w", err)
}
response := string(buf[:n])
log.WithFields(logrus.Fields{
"session_id": d.session.ID(),
"response": response,
}).Debug("Received STREAM CONNECT response")
return response, nil
}
// createStreamConnection creates a new StreamConn instance with the established connection.
func (d *StreamDialer) createStreamConnection(sam *common.SAM, addr i2pkeys.I2PAddr) *StreamConn {
return &StreamConn{
session: d.session,
conn: sam,
laddr: d.session.Addr(),
raddr: addr,
}
}
// parseConnectResponse parses the STREAM STATUS response
func (d *StreamDialer) parseConnectResponse(response string) error {
scanner := bufio.NewScanner(strings.NewReader(response))
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
word := scanner.Text()
switch word {
case "STREAM", "STATUS":
continue
case "RESULT=OK":
return nil
case "RESULT=CANT_REACH_PEER":
return oops.Errorf("cannot reach peer")
case "RESULT=I2P_ERROR":
return oops.Errorf("I2P internal error")
case "RESULT=INVALID_KEY":
return oops.Errorf("invalid destination key")
case "RESULT=INVALID_ID":
return oops.Errorf("invalid session ID")
case "RESULT=TIMEOUT":
return oops.Errorf("connection timeout")
default:
if strings.HasPrefix(word, "RESULT=") {
return oops.Errorf("connection failed: %s", word[7:])
}
}
}
return oops.Errorf("unexpected response format: %s", response)
}

100
stream/dialer_test.go Normal file
View File

@@ -0,0 +1,100 @@
package stream
import (
"context"
"testing"
"time"
)
func TestStreamSession_Dial(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewStreamSession(sam, "test_dial", keys, []string{
"inbound.length=1", "outbound.length=1",
})
if err != nil {
t.Fatalf("Failed to create session: %v", err)
}
defer session.Close()
// Test dialing to a known I2P destination
// This test might fail if the destination is not reachable
// but it tests the basic dial functionality
_, err = session.Dial("idk.i2p")
// We don't fail the test if dial fails since it depends on network conditions
// but we log it for debugging
if err != nil {
t.Logf("Dial to idk.i2p failed (expected in some network conditions): %v", err)
}
}
func TestStreamSession_DialI2P(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewStreamSession(sam, "test_dial_i2p", keys, []string{
"inbound.length=1", "outbound.length=1",
})
if err != nil {
t.Fatalf("Failed to create session: %v", err)
}
defer session.Close()
// Try to lookup a destination first
addr, err := sam.Lookup("zzz.i2p")
if err != nil {
t.Skipf("Failed to lookup destination: %v", err)
}
// Test dialing to the looked up address
_, err = session.DialI2P(addr)
if err != nil {
t.Logf("DialI2P failed (expected in some network conditions): %v", err)
}
}
func TestStreamSession_DialContext(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewStreamSession(sam, "test_dial_context", keys, nil)
if err != nil {
t.Fatalf("Failed to create session: %v", err)
}
defer session.Close()
t.Run("dial with context timeout", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := session.DialContext(ctx, "nonexistent.i2p")
if err == nil {
t.Log("Dial succeeded unexpectedly")
} else {
t.Logf("Dial failed as expected: %v", err)
}
})
t.Run("dial with cancelled context", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
_, err := session.DialContext(ctx, "test.i2p")
if err == nil {
t.Error("Expected dial to fail with cancelled context")
}
})
}

View File

@@ -1,138 +0,0 @@
package stream
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"net"
"strings"
"time"
"github.com/go-i2p/go-sam-go/common"
"github.com/go-i2p/i2pkeys"
"github.com/sirupsen/logrus"
)
// context-aware dialer, eventually...
func (s *StreamSession) DialContext(ctx context.Context, n, addr string) (net.Conn, error) {
log.WithFields(logrus.Fields{"network": n, "addr": addr}).Debug("DialContext called")
return s.DialContextI2P(ctx, n, addr)
}
// context-aware dialer, eventually...
func (s *StreamSession) DialContextI2P(ctx context.Context, n, addr string) (*StreamConn, error) {
log.WithFields(logrus.Fields{"network": n, "addr": addr}).Debug("DialContextI2P called")
if ctx == nil {
log.Panic("nil context")
panic("nil context")
}
deadline := s.deadline(ctx, time.Now())
if !deadline.IsZero() {
if d, ok := ctx.Deadline(); !ok || deadline.Before(d) {
subCtx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()
ctx = subCtx
}
}
i2paddr, err := i2pkeys.NewI2PAddrFromString(addr)
if err != nil {
log.WithError(err).Error("Failed to create I2P address from string")
return nil, err
}
return s.DialI2P(i2paddr)
}
// implement net.Dialer
func (s *StreamSession) Dial(n, addr string) (c net.Conn, err error) {
log.WithFields(logrus.Fields{"network": n, "addr": addr}).Debug("Dial called")
var i2paddr i2pkeys.I2PAddr
var host string
host, _, err = net.SplitHostPort(addr)
// log.Println("Dialing:", host)
if err = common.IgnorePortError(err); err == nil {
// check for name
if strings.HasSuffix(host, ".b32.i2p") || strings.HasSuffix(host, ".i2p") {
// name lookup
i2paddr, err = s.Lookup(host)
log.WithFields(logrus.Fields{"host": host, "i2paddr": i2paddr}).Debug("Looked up I2P address")
} else {
// probably a destination
i2paddr, err = i2pkeys.NewI2PAddrFromBytes([]byte(host))
// i2paddr = i2pkeys.I2PAddr(host)
// log.Println("Destination:", i2paddr, err)
log.WithFields(logrus.Fields{"host": host, "i2paddr": i2paddr}).Debug("Created I2P address from bytes")
}
if err == nil {
return s.DialI2P(i2paddr)
}
}
log.WithError(err).Error("Dial failed")
return
}
// Dials to an I2P destination and returns a SAMConn, which implements a net.Conn.
func (s *StreamSession) DialI2P(addr i2pkeys.I2PAddr) (*StreamConn, error) {
log.WithField("addr", addr).Debug("DialI2P called")
sam, err := common.NewSAM(s.Sam())
if err != nil {
log.WithError(err).Error("Failed to create new SAM instance")
return nil, err
}
conn := sam.Conn
_, err = conn.Write([]byte("STREAM CONNECT ID=" + s.ID() + s.FromPort() + s.ToPort() + " DESTINATION=" + addr.Base64() + " SILENT=false\n"))
if err != nil {
log.WithError(err).Error("Failed to write STREAM CONNECT command")
conn.Close()
return nil, err
}
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil && err != io.EOF {
log.WithError(err).Error("Failed to write STREAM CONNECT command")
conn.Close()
return nil, err
}
scanner := bufio.NewScanner(bytes.NewReader(buf[:n]))
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
switch scanner.Text() {
case "STREAM":
continue
case "STATUS":
continue
case ResultOK:
log.Debug("Successfully connected to I2P destination")
return &StreamConn{s.Addr(), addr, conn}, nil
case ResultCantReachPeer:
log.Error("Can't reach peer")
conn.Close()
return nil, fmt.Errorf("Can not reach peer")
case ResultI2PError:
log.Error("I2P internal error")
conn.Close()
return nil, fmt.Errorf("I2P internal error")
case ResultInvalidKey:
log.Error("Invalid key - Stream Session")
conn.Close()
return nil, fmt.Errorf("Invalid key - Stream Session")
case ResultInvalidID:
log.Error("Invalid tunnel ID")
conn.Close()
return nil, fmt.Errorf("Invalid tunnel ID")
case ResultTimeout:
log.Error("Connection timeout")
conn.Close()
return nil, fmt.Errorf("Timeout")
default:
log.WithField("error", scanner.Text()).Error("Unknown error")
conn.Close()
return nil, fmt.Errorf("Unknown error: %s : %s", scanner.Text(), string(buf[:n]))
}
}
log.Panic("Unexpected end of StreamSession.DialI2P()")
panic("sam3 go library error in StreamSession.DialI2P()")
}

104
stream/lifecycle_test.go Normal file
View File

@@ -0,0 +1,104 @@
package stream
import (
"runtime"
"testing"
"time"
)
// TestSessionListenerLifecycle tests that listeners are properly cleaned up when session closes
func TestSessionListenerLifecycle(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
// Create a session
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewStreamSession(sam, "test_lifecycle", keys, nil)
if err != nil {
t.Fatalf("Failed to create session: %v", err)
}
// Get initial goroutine count
initialGoroutines := runtime.NumGoroutine()
// Create multiple listeners
var listeners []*StreamListener
for i := 0; i < 3; i++ {
listener, err := session.Listen()
if err != nil {
t.Fatalf("Failed to create listener %d: %v", i, err)
}
listeners = append(listeners, listener)
}
// Wait for goroutines to start
time.Sleep(100 * time.Millisecond)
// Verify goroutines increased
afterCreateGoroutines := runtime.NumGoroutine()
if afterCreateGoroutines <= initialGoroutines {
t.Errorf("Expected more goroutines after creating listeners, got %d, was %d", afterCreateGoroutines, initialGoroutines)
}
// Close the session - this should clean up all listeners
err = session.Close()
if err != nil {
t.Errorf("Failed to close session: %v", err)
}
// Wait for cleanup
time.Sleep(200 * time.Millisecond)
// Verify goroutines were cleaned up
finalGoroutines := runtime.NumGoroutine()
if finalGoroutines > afterCreateGoroutines {
t.Errorf("Expected fewer or same goroutines after closing session, got %d, was %d", finalGoroutines, afterCreateGoroutines)
}
// Verify listeners are actually closed
for i, listener := range listeners {
// Try to use the listener - should fail
_, err := listener.Accept()
if err == nil {
t.Errorf("Listener %d should be closed but Accept() succeeded", i)
}
}
}
// TestExplicitListenerClose tests that explicitly closing listeners works correctly
func TestExplicitListenerClose(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
// Create a session
sam, keys := setupTestSAM(t)
defer sam.Close()
session, err := NewStreamSession(sam, "test_explicit_close", keys, nil)
if err != nil {
t.Fatalf("Failed to create session: %v", err)
}
defer session.Close()
// Create a listener
listener, err := session.Listen()
if err != nil {
t.Fatalf("Failed to create listener: %v", err)
}
// Close the listener explicitly
err = listener.Close()
if err != nil {
t.Errorf("Failed to close listener: %v", err)
}
// Verify listener is closed
_, err = listener.Accept()
if err == nil {
t.Error("Listener should be closed but Accept() succeeded")
}
}

Some files were not shown because too many files have changed in this diff Show More