bundle sam3
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*~
|
||||
*.swp
|
119
I2PAddr.go
Normal file
119
I2PAddr.go
Normal file
@ -0,0 +1,119 @@
|
||||
package sam3
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base32"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
)
|
||||
|
||||
|
||||
|
||||
var (
|
||||
i2pB64enc *base64.Encoding = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~")
|
||||
i2pB32enc *base32.Encoding = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567")
|
||||
)
|
||||
|
||||
|
||||
// The public and private keys associated with an I2P destination. I2P hides the
|
||||
// details of exactly what this is, so treat them as blobs, but generally: One
|
||||
// pair of DSA keys, one pair of ElGamal keys, and sometimes (almost never) also
|
||||
// a certificate. String() returns you the full content of I2PKeys and Addr()
|
||||
// returns the public keys.
|
||||
type I2PKeys struct {
|
||||
addr I2PAddr // only the public key
|
||||
both string // both public and private keys
|
||||
}
|
||||
|
||||
// Creates I2PKeys from an I2PAddr and a public/private keypair string (as
|
||||
// generated by String().)
|
||||
func NewKeys(addr I2PAddr, both string) I2PKeys {
|
||||
return I2PKeys{addr, both}
|
||||
}
|
||||
|
||||
// Returns the public keys of the I2PKeys.
|
||||
func (k I2PKeys) Addr() I2PAddr {
|
||||
return k.addr
|
||||
}
|
||||
|
||||
// Returns the keys (both public and private), in I2Ps base64 format. Use this
|
||||
// when you create sessions.
|
||||
func (k I2PKeys) String() string {
|
||||
return k.both
|
||||
}
|
||||
|
||||
// I2PAddr represents an I2P destination, almost equivalent to an IP address.
|
||||
// This is the humongously huge base64 representation of such an address, which
|
||||
// really is just a pair of public keys and also maybe a certificate. (I2P hides
|
||||
// the details of exactly what it is. Read the I2P specifications for more info.)
|
||||
type I2PAddr string
|
||||
|
||||
// Returns the base64 representation of the I2PAddr
|
||||
func (a I2PAddr) Base64() string {
|
||||
return string(a)
|
||||
}
|
||||
|
||||
// Returns the I2P destination (base64-encoded)
|
||||
func (a I2PAddr) String() string {
|
||||
return string(a)
|
||||
}
|
||||
|
||||
// Returns "I2P"
|
||||
func (a I2PAddr) Network() string {
|
||||
return "I2P"
|
||||
}
|
||||
|
||||
// Creates a new I2P address from a base64-encoded string. Checks if the address
|
||||
// addr is in correct format. (If you know for sure it is, use I2PAddr(addr).)
|
||||
func NewI2PAddrFromString(addr string) (I2PAddr, error) {
|
||||
// very basic check
|
||||
if len(addr) > 4096 || len(addr) < 516 {
|
||||
return I2PAddr(""), errors.New("Not an I2P address")
|
||||
}
|
||||
buf := make([]byte, i2pB64enc.DecodedLen(len(addr)))
|
||||
if _, err := i2pB64enc.Decode(buf, []byte(addr)); err != nil {
|
||||
return I2PAddr(""), errors.New("Address is not base64-encoded")
|
||||
}
|
||||
return I2PAddr(addr), nil
|
||||
}
|
||||
|
||||
// Creates a new I2P address from a byte array. The inverse of ToBytes().
|
||||
func NewI2PAddrFromBytes(addr []byte) (I2PAddr, error) {
|
||||
if len(addr) > 4096 || len(addr) < 384 {
|
||||
return I2PAddr(""), errors.New("Not an I2P address")
|
||||
}
|
||||
buf := make([]byte, i2pB64enc.EncodedLen(len(addr)))
|
||||
i2pB64enc.Encode(buf, addr)
|
||||
return I2PAddr(string(buf)), nil
|
||||
}
|
||||
|
||||
// Turns an I2P address to a byte array. The inverse of NewI2PAddrFromBytes().
|
||||
func (addr I2PAddr) ToBytes() ([]byte, error) {
|
||||
buf := make([]byte, i2pB64enc.DecodedLen(len(addr)))
|
||||
if _, err := i2pB64enc.Decode(buf, []byte(addr)); err != nil {
|
||||
return buf, errors.New("Address is not base64-encoded")
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// Returns the *.b32.i2p address of the I2P address. It is supposed to be a
|
||||
// somewhat human-manageable 64 character long pseudo-domain name equivalent of
|
||||
// the 516+ characters long default base64-address (the I2PAddr format). It is
|
||||
// not possible to turn the base32-address back into a usable I2PAddr without
|
||||
// performing a Lookup(). Lookup only works if you are using the I2PAddr from
|
||||
// which the b32 address was generated.
|
||||
func (addr I2PAddr) Base32() string {
|
||||
hash := sha256.New()
|
||||
b, _ := addr.ToBytes()
|
||||
hash.Write(b)
|
||||
digest := hash.Sum(nil)
|
||||
b32addr := make([]byte, 56)
|
||||
i2pB32enc.Encode(b32addr, digest)
|
||||
return string(b32addr[:52]) + ".b32.i2p"
|
||||
}
|
||||
|
||||
// Makes any string into a *.b32.i2p human-readable I2P address. This makes no
|
||||
// sense, unless "anything" is an I2P destination of some sort.
|
||||
func Base32(anything string) string {
|
||||
return I2PAddr(anything).Base32()
|
||||
}
|
90
README.md
Normal file
90
README.md
Normal file
@ -0,0 +1,90 @@
|
||||
# README #
|
||||
|
||||
go library for the I2P [SAMv3.0](https://geti2p.net/en/docs/api/samv3) bridge, used to build anonymous/pseudonymous end-to-end encrypted sockets.
|
||||
|
||||
This library is much better than ccondom (that use BOB), much more stable and much easier to maintain.
|
||||
|
||||
## Support/TODO ##
|
||||
|
||||
**What works:**
|
||||
|
||||
* Utils
|
||||
* Resolving domain names to I2P destinations
|
||||
* .b32.i2p hashes
|
||||
* Generating keys/i2p destinations
|
||||
* Streaming
|
||||
* DialI2P() - Connecting to stuff in I2P
|
||||
* Listen()/Accept() - Handling incomming connections
|
||||
* Implements net.Conn and net.Listener
|
||||
* Datagrams
|
||||
* Implements net.PacketConn
|
||||
* Raw datagrams
|
||||
* Like datagrams, but without addresses
|
||||
|
||||
**Does not work:**
|
||||
|
||||
* Everything works! :D
|
||||
* Probably needs some real-world testing
|
||||
|
||||
## Documentation ##
|
||||
|
||||
* Latest version-documentation:
|
||||
* set your GOPATH
|
||||
* Enter `godoc -http=:8081` into your terminal and hit enter.
|
||||
* Goto http://localhost:8081, click packages, and navigate to sam3
|
||||
|
||||
## Examples ##
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/majestrate/i2p-tools/sam3"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const yoursam = "127.0.0.1:7656" // sam bridge
|
||||
|
||||
func client(server I2PAddr) {
|
||||
sam, _ := NewSAM(yoursam)
|
||||
keys, _ := sam.NewKeys()
|
||||
stream, _ := sam.NewStreamSession("clientTun", keys, Options_Small)
|
||||
fmt.Println("Client: Connecting to " + server.Base32())
|
||||
conn, _ := stream.DialI2P(server)
|
||||
conn.Write([]byte("Hello world!"))
|
||||
return
|
||||
}
|
||||
|
||||
func main() {
|
||||
sam, _ := NewSAM(yoursam)
|
||||
keys, _ := sam.NewKeys()
|
||||
go client(keys.Addr())
|
||||
stream, _ := sam.NewStreamSession("serverTun", keys, Options_Medium)
|
||||
listener, _ := stream.Listen()
|
||||
conn, _ := listener.Accept()
|
||||
buf := make([]byte, 4096)
|
||||
n, _ := conn.Read(buf)
|
||||
fmt.Println("Server received: " + string(buf[:n]))
|
||||
}
|
||||
```
|
||||
|
||||
The above will write to the terminal:
|
||||
|
||||
```text
|
||||
Client: Connecting to zjnvfh4hs3et5vtz35ogwzrws26zvwkcad5uo5esecvg4qpk5b4a.b32.i2p
|
||||
Server received: Hello world!
|
||||
```
|
||||
|
||||
Error handling was omitted in the above code for readability.
|
||||
|
||||
## Testing ##
|
||||
|
||||
* `go test` runs the whole suite (takes 90+ sec to perform!)
|
||||
* `go test -short` runs the shorter variant, does not connect to anything
|
||||
|
||||
## License ##
|
||||
|
||||
Public domain.
|
||||
|
||||
## Author ##
|
||||
|
||||
Kalle Vedin `kalle.vedin@fripost.org`
|
57
SAMConn.go
Normal file
57
SAMConn.go
Normal file
@ -0,0 +1,57 @@
|
||||
package sam3
|
||||
|
||||
import (
|
||||
"time"
|
||||
"net"
|
||||
)
|
||||
|
||||
// Implements net.Conn
|
||||
type SAMConn struct {
|
||||
laddr I2PAddr
|
||||
raddr I2PAddr
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
// Implements net.Conn
|
||||
func (sc SAMConn) Read(buf []byte) (int, error) {
|
||||
n, err := sc.conn.Read(buf)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Implements net.Conn
|
||||
func (sc SAMConn) Write(buf []byte) (int, error) {
|
||||
n, err := sc.conn.Write(buf)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Implements net.Conn
|
||||
func (sc SAMConn) Close() error {
|
||||
return sc.conn.Close()
|
||||
}
|
||||
|
||||
// Implements net.Conn
|
||||
func (sc SAMConn) LocalAddr() I2PAddr {
|
||||
return sc.laddr
|
||||
}
|
||||
|
||||
// Implements net.Conn
|
||||
func (sc SAMConn) RemoteAddr() I2PAddr {
|
||||
return sc.raddr
|
||||
}
|
||||
|
||||
// Implements net.Conn
|
||||
func (sc SAMConn) SetDeadline(t time.Time) error {
|
||||
return sc.conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
// Implements net.Conn
|
||||
func (sc SAMConn) SetReadDeadline(t time.Time) error {
|
||||
return sc.conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
// Implements net.Conn
|
||||
func (sc SAMConn) SetWriteDeadline(t time.Time) error {
|
||||
return sc.conn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
|
141
datagram.go
Normal file
141
datagram.go
Normal file
@ -0,0 +1,141 @@
|
||||
package sam3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
samAddr string // address to the sam bridge (ipv4:port)
|
||||
id string // tunnel name
|
||||
conn net.Conn // connection to sam bridge
|
||||
udpconn *net.UDPConn // used to deliver datagrams
|
||||
keys I2PKeys // i2p destination keys
|
||||
rUDPAddr *net.UDPAddr // the SAM bridge UDP-port
|
||||
}
|
||||
|
||||
// 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, options []string, udpPort int) (*DatagramSession, error) {
|
||||
if udpPort > 65335 || udpPort < 0 {
|
||||
return nil, errors.New("udpPort needs to be in the intervall 0-65335")
|
||||
}
|
||||
if udpPort == 0 {
|
||||
udpPort = 7655
|
||||
}
|
||||
lhost, _, err := net.SplitHostPort(s.conn.LocalAddr().String())
|
||||
if err != nil {
|
||||
s.Close()
|
||||
return nil, err
|
||||
}
|
||||
lUDPAddr, err := net.ResolveUDPAddr("udp4", lhost + ":0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
udpconn, err := net.ListenUDP("udp4", lUDPAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rhost, _, err := net.SplitHostPort(s.conn.RemoteAddr().String())
|
||||
if err != nil {
|
||||
s.Close()
|
||||
return nil, err
|
||||
}
|
||||
rUDPAddr, err := net.ResolveUDPAddr("udp4", rhost + ":" + strconv.Itoa(udpPort))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, lport, err := net.SplitHostPort(udpconn.LocalAddr().String())
|
||||
conn, err := s.newGenericSession("DATAGRAM", id, keys, options, []string{"PORT=" + lport})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &DatagramSession{s.address, id, conn, udpconn, keys, rUDPAddr}, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (s *DatagramSession) ReadFrom(b []byte) (n int, addr I2PAddr, err error) {
|
||||
// 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 {
|
||||
return 0, I2PAddr(""), err
|
||||
}
|
||||
if bytes.Equal(saddr.IP, s.rUDPAddr.IP) {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
i := bytes.IndexByte(buf, byte('\n'))
|
||||
if i > 4096 || i > n {
|
||||
return 0, I2PAddr(""), errors.New("Could not parse incomming message remote address.")
|
||||
}
|
||||
raddr, err := NewI2PAddrFromString(string(buf[:i]))
|
||||
if err != nil {
|
||||
return 0, 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])
|
||||
return n-(i+1), raddr, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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 I2PAddr) (n int, err error) {
|
||||
header := []byte("3.0 " + s.id + " " + addr.String() + "\n")
|
||||
msg := append(header, b...)
|
||||
n, err = s.udpconn.WriteToUDP(msg, s.rUDPAddr)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Closes the DatagramSession. Implements net.PacketConn
|
||||
func (s *DatagramSession) Close() error {
|
||||
err := s.conn.Close()
|
||||
err2 := s.udpconn.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err2
|
||||
}
|
||||
|
||||
// Returns the I2P destination of the DatagramSession. Implements net.PacketConn
|
||||
func (s *DatagramSession) LocalAddr() I2PAddr {
|
||||
return s.keys.Addr()
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return s.udpconn.SetDeadline(t)
|
||||
}
|
||||
|
||||
// Sets read deadline for the DatagramSession. Implements net.PacketConn
|
||||
func (s *DatagramSession) SetReadDeadline(t time.Time) error {
|
||||
return s.udpconn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
// Sets the write deadline for the DatagramSession. Implements net.Packetconn.
|
||||
func (s *DatagramSession) SetWriteDeadline(t time.Time) error {
|
||||
return s.udpconn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
|
137
datagram_test.go
Normal file
137
datagram_test.go
Normal file
@ -0,0 +1,137 @@
|
||||
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]))
|
||||
|
||||
return
|
||||
// Output:
|
||||
//Got message: Hello myself!
|
||||
}
|
||||
|
120
raw.go
Normal file
120
raw.go
Normal file
@ -0,0 +1,120 @@
|
||||
package sam3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
samAddr string // address to the sam bridge (ipv4:port)
|
||||
id string // tunnel name
|
||||
conn net.Conn // connection to sam bridge
|
||||
udpconn *net.UDPConn // used to deliver datagrams
|
||||
keys I2PKeys // i2p destination keys
|
||||
rUDPAddr *net.UDPAddr // the SAM bridge UDP-port
|
||||
}
|
||||
|
||||
// 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, options []string, udpPort int) (*RawSession, error) {
|
||||
if udpPort > 65335 || udpPort < 0 {
|
||||
return nil, errors.New("udpPort needs to be in the intervall 0-65335")
|
||||
}
|
||||
if udpPort == 0 {
|
||||
udpPort = 7655
|
||||
}
|
||||
lhost, _, err := net.SplitHostPort(s.conn.LocalAddr().String())
|
||||
if err != nil {
|
||||
s.Close()
|
||||
return nil, err
|
||||
}
|
||||
lUDPAddr, err := net.ResolveUDPAddr("udp4", lhost + ":0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
udpconn, err := net.ListenUDP("udp4", lUDPAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rhost, _, err := net.SplitHostPort(s.conn.RemoteAddr().String())
|
||||
if err != nil {
|
||||
s.Close()
|
||||
return nil, err
|
||||
}
|
||||
rUDPAddr, err := net.ResolveUDPAddr("udp4", rhost + ":" + strconv.Itoa(udpPort))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, lport, err := net.SplitHostPort(udpconn.LocalAddr().String())
|
||||
conn, err := s.newGenericSession("RAW", id, keys, options, []string{"PORT=" + lport})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &RawSession{s.address, id, conn, udpconn, keys, rUDPAddr}, nil
|
||||
}
|
||||
|
||||
// Reads one raw datagram sent to the destination of the DatagramSession. Returns
|
||||
// the number of bytes read. Who sent the raw message can not be determined at
|
||||
// this layer - you need to do it (in a secure way!).
|
||||
func (s *RawSession) Read(b []byte) (n int, err error) {
|
||||
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(b)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if bytes.Equal(saddr.IP, s.rUDPAddr.IP) {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Sends one raw datagram to the destination specified. At the time of writing,
|
||||
// maximum size is 32 kilobyte, but this may change in the future.
|
||||
func (s *RawSession) WriteTo(b []byte, addr I2PAddr) (n int, err error) {
|
||||
header := []byte("3.0 " + s.id + " " + addr.String() + "\n")
|
||||
msg := append(header, b...)
|
||||
n, err = s.udpconn.WriteToUDP(msg, s.rUDPAddr)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Closes the RawSession.
|
||||
func (s *RawSession) Close() error {
|
||||
err := s.conn.Close()
|
||||
err2 := s.udpconn.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err2
|
||||
}
|
||||
|
||||
// Returns the local I2P destination of the RawSession.
|
||||
func (s *RawSession) LocalAddr() I2PAddr {
|
||||
return s.keys.Addr()
|
||||
}
|
||||
|
||||
func (s *RawSession) SetDeadline(t time.Time) error {
|
||||
return s.udpconn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (s *RawSession) SetReadDeadline(t time.Time) error {
|
||||
return s.udpconn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (s *RawSession) SetWriteDeadline(t time.Time) error {
|
||||
return s.udpconn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
|
189
sam3.go
Normal file
189
sam3.go
Normal file
@ -0,0 +1,189 @@
|
||||
// Library for I2Ps SAMv3 bridge (https://geti2p.com)
|
||||
package sam3
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"net"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
||||
|
||||
// Used for controlling I2Ps SAMv3.
|
||||
type SAM struct {
|
||||
address string // ipv4:port
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
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="
|
||||
)
|
||||
|
||||
// Creates a new controller for the I2P routers SAM bridge.
|
||||
func NewSAM(address string) (*SAM, error) {
|
||||
conn, err := net.Dial("tcp4", address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := conn.Write([]byte("HELLO VERSION MIN=3.0 MAX=3.0\n")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf := make([]byte, 256)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if string(buf[:n]) == "HELLO REPLY RESULT=OK VERSION=3.0\n" {
|
||||
return &SAM{address, conn}, nil
|
||||
} else if string(buf[:n]) == "HELLO REPLY RESULT=NOVERSION\n" {
|
||||
return nil, errors.New("That SAM bridge does not support SAMv3.")
|
||||
} else {
|
||||
return nil, errors.New(string(buf[:n]))
|
||||
}
|
||||
}
|
||||
|
||||
// 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() (I2PKeys, error) {
|
||||
if _, err := sam.conn.Write([]byte("DEST GENERATE\n")); err != nil {
|
||||
return I2PKeys{}, err
|
||||
}
|
||||
buf := make([]byte, 8192)
|
||||
n, err := sam.conn.Read(buf)
|
||||
if err != nil {
|
||||
return I2PKeys{}, err
|
||||
}
|
||||
s := bufio.NewScanner(bytes.NewReader(buf[:n]))
|
||||
s.Split(bufio.ScanWords)
|
||||
|
||||
var pub, priv string
|
||||
for s.Scan() {
|
||||
text := s.Text()
|
||||
if text == "DEST" {
|
||||
continue
|
||||
} else if text == "REPLY" {
|
||||
continue
|
||||
} else if strings.HasPrefix(text, "PUB=") {
|
||||
pub = text[4:]
|
||||
} else if strings.HasPrefix(text, "PRIV=") {
|
||||
priv = text[5:]
|
||||
} else {
|
||||
return I2PKeys{}, errors.New("Failed to parse keys.")
|
||||
}
|
||||
}
|
||||
return I2PKeys{I2PAddr(pub), priv}, nil
|
||||
}
|
||||
|
||||
// Performs a lookup, probably this order: 1) routers known addresses, cached
|
||||
// addresses, 3) by asking peers in the I2P network.
|
||||
func (sam *SAM) Lookup(name string) (I2PAddr, error) {
|
||||
if _, err := sam.conn.Write([]byte("NAMING LOOKUP NAME=" + name + "\n")); err != nil {
|
||||
return I2PAddr(""), err
|
||||
}
|
||||
buf := make([]byte, 4096)
|
||||
n, err := sam.conn.Read(buf)
|
||||
if err != nil {
|
||||
return I2PAddr(""), err
|
||||
}
|
||||
if n <= 13 || !strings.HasPrefix(string(buf[:n]), "NAMING REPLY ") {
|
||||
return I2PAddr(""), errors.New("Failed to parse.")
|
||||
}
|
||||
s := bufio.NewScanner(bytes.NewReader(buf[13:n]))
|
||||
s.Split(bufio.ScanWords)
|
||||
|
||||
errStr := ""
|
||||
for s.Scan() {
|
||||
text := s.Text()
|
||||
if text == "RESULT=OK" {
|
||||
continue
|
||||
} else if text == "RESULT=INVALID_KEY" {
|
||||
errStr += "Invalid key."
|
||||
} else if text == "RESULT=KEY_NOT_FOUND" {
|
||||
errStr += "Unable to resolve " + name
|
||||
} else if text == "NAME=" + name {
|
||||
continue
|
||||
} else if strings.HasPrefix(text, "VALUE=") {
|
||||
return I2PAddr(text[6:]), nil
|
||||
} else if strings.HasPrefix(text, "MESSAGE=") {
|
||||
errStr += " " + text[8:]
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return I2PAddr(""), errors.New(errStr)
|
||||
}
|
||||
|
||||
// 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, options []string, extras []string) (net.Conn, error) {
|
||||
|
||||
optStr := ""
|
||||
for _, opt := range options {
|
||||
optStr += "OPTION=" + opt + " "
|
||||
}
|
||||
|
||||
conn := sam.conn
|
||||
scmsg := []byte("SESSION CREATE STYLE=" + style + " ID=" + id + " DESTINATION=" + keys.String() + " " + optStr + strings.Join(extras, " ") + "\n")
|
||||
for m, i:=0, 0; m!=len(scmsg); i++ {
|
||||
if i == 15 {
|
||||
conn.Close()
|
||||
return nil, errors.New("writing to SAM failed")
|
||||
}
|
||||
n, err := conn.Write(scmsg[m:])
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
m += n
|
||||
}
|
||||
buf := make([]byte, 4096)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
text := string(buf[:n])
|
||||
if strings.HasPrefix(text, session_OK) {
|
||||
if keys.String() != text[len(session_OK):len(text)-1] {
|
||||
return nil, errors.New("SAMv3 created a tunnel with keys other than the ones we asked it for")
|
||||
}
|
||||
return conn, nil //&StreamSession{id, conn, keys, nil, sync.RWMutex{}, nil}, nil
|
||||
} else if text == session_DUPLICATE_ID {
|
||||
conn.Close()
|
||||
return nil, errors.New("Duplicate tunnel name")
|
||||
} else if text == session_DUPLICATE_DEST {
|
||||
conn.Close()
|
||||
return nil, errors.New("Duplicate destination")
|
||||
} else if text == session_INVALID_KEY {
|
||||
conn.Close()
|
||||
return nil, errors.New("Invalid key")
|
||||
} else if strings.HasPrefix(text, session_I2P_ERROR) {
|
||||
conn.Close()
|
||||
return nil, errors.New("I2P error " + text[len(session_I2P_ERROR):])
|
||||
} else {
|
||||
conn.Close()
|
||||
return nil, errors.New("Unable to parse SAMv3 reply: " + text)
|
||||
}
|
||||
}
|
||||
|
||||
// close this sam session
|
||||
func (sam *SAM) Close() error {
|
||||
if err := sam.conn.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
180
sam3_test.go
Normal file
180
sam3_test.go
Normal file
@ -0,0 +1,180 @@
|
||||
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.both)[: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())
|
||||
}
|
||||
|
||||
|
200
stream.go
Normal file
200
stream.go
Normal file
@ -0,0 +1,200 @@
|
||||
package sam3
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Represents a streaming session.
|
||||
type StreamSession struct {
|
||||
samAddr string // address to the sam bridge (ipv4:port)
|
||||
id string // tunnel name
|
||||
conn net.Conn // connection to sam bridge
|
||||
keys I2PKeys // i2p destination keys
|
||||
}
|
||||
|
||||
// Returns the local tunnel name of the I2P tunnel used for the stream session
|
||||
func (ss StreamSession) ID() string {
|
||||
return ss.id
|
||||
}
|
||||
|
||||
// Returns the I2P destination (the address) of the stream session
|
||||
func (ss StreamSession) Addr() I2PAddr {
|
||||
return ss.keys.Addr()
|
||||
}
|
||||
|
||||
// Returns the keys associated with the stream session
|
||||
func (ss StreamSession) Keys() I2PKeys {
|
||||
return ss.keys
|
||||
}
|
||||
|
||||
// Creates a new StreamSession with the I2CP- and streaminglib options as
|
||||
// specified. See the I2P documentation for a full list of options.
|
||||
func (sam *SAM) NewStreamSession(id string, keys I2PKeys, options []string) (*StreamSession, error) {
|
||||
conn, err := sam.newGenericSession("STREAM", id, keys, options, []string{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &StreamSession{sam.address, id, conn, keys}, nil
|
||||
}
|
||||
|
||||
// Dials to an I2P destination and returns a SAMConn, which implements a net.Conn.
|
||||
func (s *StreamSession) DialI2P(addr I2PAddr) (*SAMConn, error) {
|
||||
sam, err := NewSAM(s.samAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn := sam.conn
|
||||
_,err = conn.Write([]byte("STREAM CONNECT ID=" + s.id + " DESTINATION=" + addr.Base64() + " SILENT=false\n"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf := make([]byte, 4096)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
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 "RESULT=OK" :
|
||||
return &SAMConn{s.keys.addr, addr, conn}, nil
|
||||
case "RESULT=CANT_REACH_PEER" :
|
||||
return nil, errors.New("Can not reach peer")
|
||||
case "RESULT=I2P_ERROR" :
|
||||
return nil, errors.New("I2P internal error")
|
||||
case "RESULT=INVALID_KEY" :
|
||||
return nil, errors.New("Invalid key")
|
||||
case "RESULT=INVALID_ID" :
|
||||
return nil, errors.New("Invalid tunnel ID")
|
||||
case "RESULT=TIMEOUT" :
|
||||
return nil, errors.New("Timeout")
|
||||
default :
|
||||
return nil, errors.New("Unknown error: " + scanner.Text() + " : " + string(buf[:n]))
|
||||
}
|
||||
}
|
||||
panic("sam3 go library error in StreamSession.DialI2P()")
|
||||
}
|
||||
|
||||
// Returns a listener for the I2P destination (I2PAddr) associated with the
|
||||
// StreamSession.
|
||||
func (s *StreamSession) Listen() (*StreamListener, error) {
|
||||
sam, err := NewSAM(s.conn.RemoteAddr().String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lhost, _, err := net.SplitHostPort(s.conn.LocalAddr().String())
|
||||
if err != nil {
|
||||
sam.Close()
|
||||
return nil, err
|
||||
}
|
||||
listener, err := net.Listen("tcp4", lhost + ":0")
|
||||
_, lport, err := net.SplitHostPort(listener.Addr().String())
|
||||
if err != nil {
|
||||
sam.Close()
|
||||
return nil, err
|
||||
}
|
||||
conn := sam.conn
|
||||
_, err = conn.Write([]byte("STREAM FORWARD ID=" + s.id + " PORT=" + lport + " SILENT=false\n"))
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
buf := make([]byte, 512)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
scanner := bufio.NewScanner(bytes.NewReader(buf[:n]))
|
||||
scanner.Split(bufio.ScanWords)
|
||||
for scanner.Scan() {
|
||||
switch strings.TrimSpace(scanner.Text()) {
|
||||
case "STREAM" :
|
||||
continue
|
||||
case "STATUS" :
|
||||
continue
|
||||
case "RESULT=OK" :
|
||||
port,_ := strconv.Atoi(lport)
|
||||
return &StreamListener{conn, listener, port, s.keys.Addr()}, nil
|
||||
case "RESULT=I2P_ERROR" :
|
||||
conn.Close()
|
||||
return nil, errors.New("I2P internal error")
|
||||
case "RESULT=INVALID_ID" :
|
||||
conn.Close()
|
||||
return nil, errors.New("Invalid tunnel ID")
|
||||
default :
|
||||
conn.Close()
|
||||
return nil, errors.New("Unknown error: " + scanner.Text() + " : " + string(buf[:n]))
|
||||
}
|
||||
}
|
||||
panic("sam3 go library error in StreamSession.Listener()")
|
||||
}
|
||||
|
||||
// Implements net.Listener for I2P streaming sessions
|
||||
type StreamListener struct {
|
||||
conn net.Conn
|
||||
listener net.Listener
|
||||
lport int
|
||||
laddr I2PAddr
|
||||
}
|
||||
|
||||
const defaultListenReadLen = 516
|
||||
|
||||
// Accepts incomming connections to your StreamSession tunnel. Implements net.Listener
|
||||
func (l *StreamListener) Accept() (*SAMConn, error) {
|
||||
conn, err := l.listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf := make([]byte, defaultListenReadLen)
|
||||
n, err := conn.Read(buf)
|
||||
if n < defaultListenReadLen {
|
||||
return nil, errors.New("Unknown destination type: " + string(buf[:n]))
|
||||
}
|
||||
// I2P inserts the I2P address ("destination") of the connecting peer into the datastream, followed by
|
||||
// a \n. Since the length of a destination may vary, this reads until a newline is found. At the time
|
||||
// of writing, the length is never less then, and almost always equals 516 bytes, which is why
|
||||
// defaultListenReadLen is 516.
|
||||
if rune(buf[defaultListenReadLen - 1]) != '\n' {
|
||||
abuf := make([]byte, 1)
|
||||
for {
|
||||
n, err := conn.Read(abuf)
|
||||
if n != 1 || err != nil {
|
||||
return nil, errors.New("Failed to decode connecting peers I2P destination.")
|
||||
}
|
||||
buf = append(buf, abuf[0])
|
||||
if rune(abuf[0]) == '\n' { break }
|
||||
}
|
||||
}
|
||||
rAddr, err := NewI2PAddrFromString(string(buf[:len(buf)-1])) // the address minus the trailing newline
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, errors.New("Could not determine connecting tunnels address.")
|
||||
}
|
||||
return &SAMConn{l.laddr, rAddr, conn}, nil
|
||||
}
|
||||
|
||||
// Closes the stream session. Implements net.Listener
|
||||
func (l *StreamListener) Close() error {
|
||||
err := l.listener.Close()
|
||||
err2 := l.conn.Close()
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Returns the I2P destination (address) of the stream session. Implements net.Listener
|
||||
func (l *StreamListener) Addr() net.Addr {
|
||||
return l.laddr
|
||||
}
|
281
stream_test.go
Normal file
281
stream_test.go
Normal file
@ -0,0 +1,281 @@
|
||||
package sam3
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
||||
func Test_StreamingDial(t *testing.T) {
|
||||
if testing.Short() {
|
||||
return
|
||||
}
|
||||
fmt.Println("Test_StreamingDial")
|
||||
sam, err := NewSAM(yoursam)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error)
|
||||
t.Fail()
|
||||
return
|
||||
}
|
||||
defer sam.Close()
|
||||
keys, err := sam.NewKeys()
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
t.Fail()
|
||||
return
|
||||
}
|
||||
fmt.Println("\tBuilding tunnel")
|
||||
ss, err := sam.NewStreamSession("streamTun", 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())
|
||||
t.Fail()
|
||||
return
|
||||
}
|
||||
fmt.Println("\tNotice: This may fail if your I2P node is not well integrated in the I2P network.")
|
||||
fmt.Println("\tLooking up forum.i2p")
|
||||
forumAddr, err := sam.Lookup("forum.i2p")
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
t.Fail()
|
||||
return
|
||||
}
|
||||
fmt.Println("\tDialing forum.i2p")
|
||||
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(forum.i2p)? It replied %d bytes, but nothing that looked like http/html", n)
|
||||
} else {
|
||||
fmt.Println("\tRead HTTP/HTML from forum.i2p")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_StreamingServerClient(t *testing.T) {
|
||||
if testing.Short() {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Test_StreamingServerClient")
|
||||
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")
|
||||
ss, err := sam.NewStreamSession("serverTun", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
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 := sam2.NewStreamSession("clientTun", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"})
|
||||
if err != nil {
|
||||
c <- false
|
||||
return
|
||||
}
|
||||
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 ExampleStreamSession() {
|
||||
// Creates a new StreamingSession, dials to zzz.i2p and gets a SAMConn
|
||||
// which behaves just like a normal net.Conn.
|
||||
|
||||
const samBridge = "127.0.0.1:7656"
|
||||
|
||||
sam, err := NewSAM(samBridge)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
defer sam.Close()
|
||||
keys, err := sam.NewKeys()
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
// See the example Option_* variables.
|
||||
ss, err := sam.NewStreamSession("stream_example", keys, Options_Small)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
someone, err := sam.Lookup("zzz.i2p")
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := ss.DialI2P(someone)
|
||||
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(zzz.i2p)? It replied %d bytes, but nothing that looked like http/html", n)
|
||||
} else {
|
||||
fmt.Println("Read HTTP/HTML from zzz.i2p")
|
||||
}
|
||||
return
|
||||
|
||||
// Output:
|
||||
//Sending HTTP GET /
|
||||
//Read HTTP/HTML from zzz.i2p
|
||||
}
|
||||
|
||||
func ExampleStreamListener() {
|
||||
// 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"
|
||||
|
||||
sam, err := NewSAM(samBridge)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
defer sam.Close()
|
||||
keys, err := sam.NewKeys()
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
quit := make(chan bool)
|
||||
|
||||
// Client connecting to the server
|
||||
go func(server I2PAddr) {
|
||||
csam, err := NewSAM(samBridge)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
defer csam.Close()
|
||||
keys, err := csam.NewKeys()
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
cs, err := csam.NewStreamSession("client_example", keys, Options_Small)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
quit <- false
|
||||
return
|
||||
}
|
||||
conn, err := cs.DialI2P(server)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
quit <- false
|
||||
return
|
||||
}
|
||||
buf := make([]byte, 256)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
quit <- false
|
||||
return
|
||||
}
|
||||
fmt.Println(string(buf[:n]))
|
||||
quit <- true
|
||||
}(keys.Addr()) // end of client
|
||||
|
||||
ss, err := sam.NewStreamSession("server_example", keys, Options_Small)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
l, err := ss.Listen()
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
conn.Write([]byte("Hello world!"))
|
||||
|
||||
<-quit // waits for client to die, for example only
|
||||
|
||||
// Output:
|
||||
//Hello world!
|
||||
}
|
40
suggestedOptions.go
Normal file
40
suggestedOptions.go
Normal file
@ -0,0 +1,40 @@
|
||||
package sam3
|
||||
|
||||
// Examples and suggestions for options when creating sessions.
|
||||
var (
|
||||
// Suitable options if you are shuffling A LOT of traffic. If unused, this
|
||||
// will waste your resources.
|
||||
Options_Humongous = []string{"inbound.length=3", "outbound.length=3",
|
||||
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
|
||||
"inbound.backupQuantity=3", "outbound.backupQuantity=3",
|
||||
"inbound.quantity=6", "outbound.quantity=6"}
|
||||
|
||||
// Suitable for shuffling a lot of traffic.
|
||||
Options_Fat = []string{"inbound.length=3", "outbound.length=3",
|
||||
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
|
||||
"inbound.backupQuantity=1", "outbound.backupQuantity=1",
|
||||
"inbound.quantity=4", "outbound.quantity=4"}
|
||||
|
||||
// Suitable for shuffling medium amounts of traffic.
|
||||
Options_Medium = []string{"inbound.length=3", "outbound.length=3",
|
||||
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
|
||||
"inbound.backupQuantity=0", "outbound.backupQuantity=0",
|
||||
"inbound.quantity=2", "outbound.quantity=2"}
|
||||
|
||||
// Suitable only for small dataflows, and very short lasting connections:
|
||||
// You only have one tunnel in each direction, so if any of the nodes
|
||||
// through which any of your two tunnels pass through go offline, there will
|
||||
// be a complete halt in the dataflow, until a new tunnel is built.
|
||||
Options_Small = []string{"inbound.length=3", "outbound.length=3",
|
||||
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
|
||||
"inbound.backupQuantity=0", "outbound.backupQuantity=0",
|
||||
"inbound.quantity=1", "outbound.quantity=1"}
|
||||
|
||||
// Does not use any anonymization, you connect directly to others tunnel
|
||||
// endpoints, thus revealing your identity but not theirs. Use this only
|
||||
// if you don't care.
|
||||
Options_Warning_ZeroHop = []string{"inbound.length=0", "outbound.length=0",
|
||||
"inbound.lengthVariance=0", "outbound.lengthVariance=0",
|
||||
"inbound.backupQuantity=0", "outbound.backupQuantity=0",
|
||||
"inbound.quantity=2", "outbound.quantity=2"}
|
||||
)
|
Reference in New Issue
Block a user