Initial commit

This commit is contained in:
dd dd 2024-07-22 23:00:21 +02:00
commit 081f678a10
38 changed files with 2783 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
localhost.crt
localhost.key
.idea
nostr

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 asmogo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

97
README.md Normal file
View File

@ -0,0 +1,97 @@
# Nostr Web Services (NWS)
NWS replaces the IP layer in TCP transport using Nostr, enabling a secure connection between
clients and backend services.
Exit nodes are reachable through their [nprofiles](https://nostr-nips.com/nip-19), which are combinations of a Nostr public key and multiple relays.
### Prerequisites
- A list of Nostr relays that the exit node is connected to.
- The Nostr private key of the exit node.
The exit node utilizes the private key and relay list to generate an [nprofile](https://nostr-nips.com/nip-19), which is printed in the console on startup.
## Overview
### NWS main components
1. **Entry node**: It forwards tcp packets to the exit node using a SOCKS proxy and creates encrypted events for the public key of the exit node.
2. **Exit node**: It is a TCP reverse proxy that listens for incoming Nostr subscriptions and forwards the payload to the designated backend service.
<img src="nws.png" width="900"/>
## Quickstart
Running NWS using Docker is recommended. For instructions on running NWS on your local machine, refer to the [Build from source](#build-from-source) section.
### Using Docker Compose
To set up using Docker Compose, run the following command:
```
docker compose up -d --build
```
This will start an example setup, including the entry node, exit node, and a backend service.
### Sending Requests to the Entry node
You can use the following command to send a request to the nprofile:
```
curl -v -x socks5h://localhost:8882 http://nprofile1qqsp98rnlp7sn4xuf7meyec48njp2qyfch0jktwvfuqx8vdqgexkg8gpz4mhxw309ahx7um5wgkhyetvv9un5wps8qcqggauk8/v1/info --insecure
```
If the nprofile supports TLS, you can choose to connect using https scheme
```
curl -v -x socks5h://localhost:8882 https://nprofile1qqstw2nc544vkl4760yeq9xt2yd0gthl4trm6ruvpukdthx9fy5xqjcpz4mhxw309ahx7um5wgkhyetvv9un5wps8qcqcelsf6/v1/info --insecure
```
When using https, the entry node can be used as a service, since the operator will not be able to see the request data.
## Build from source
The exit node must be set up to make the services reachable via Nostr.
### Configuration
Configuration can be completed using environment variables.
Alternatively, you can create a `.env` file in the current working directory with the following content:
```
NOSTR_RELAYS = 'ws://localhost:6666;wss://relay.damus.io'
NOSTR_PRIVATE_KEY = "EXITPRIVATEHEX"
BACKEND_HOST = 'localhost:3338'
```
- `NOSTR_RELAYS`: A list of nostr relays to publish events to. Will only be used if there was no nprofile in the
request.
- `NOSTR_PRIVATE_KEY`: The private key to sign the events
- `BACKEND_HOST`: The host of the backend to forward requests to
To start the exit node, use this command:
```
go run cmd/exit/main.go
```
If your backend services support TLS, your service can now start using TLS encryption through a publicly available entry node.
---
To run an entry node for accessing NWS services behind exit nodes, use the following command:
```
go run cmd/proxy/main.go
```
#### Entry node Configuration
If you used environment variables, no further configuration is needed.
For `.env` file configurations, do so in the current working directory with the following content:
```
NOSTR_RELAYS = 'ws://localhost:6666;wss://relay.damus.io'
```
Here, NOSTR_RELAYS is a list of nostr relays to publish events to and will only be used if there was no nprofile in the request.

35
cmd/echo/echo.go Normal file
View File

@ -0,0 +1,35 @@
package main
import (
"fmt"
"io/ioutil"
"log/slog"
"math/rand"
"net/http"
"time"
)
func echoHandler(w http.ResponseWriter, r *http.Request) {
body, _ := ioutil.ReadAll(r.Body)
// Seed the random number generator
rand.Seed(time.Now().UnixNano())
// Generate random number between 1 and 10
randomSleep := rand.Intn(10) + 1
// Sleep for random number of seconds
// time.Sleep(time.Duration(randomSleep) * time.Second)
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "hi there, you were sleeping for %d seconds. I received your request: %s", randomSleep, string(body))
slog.Info("Received request", "wait", randomSleep)
}
func main() {
http.HandleFunc("/", echoHandler)
//err := http.ListenAndServe(":3338", nil)
err := http.ListenAndServeTLS(":3338", "localhost.crt", "localhost.key", nil)
if err != nil {
fmt.Println("Error while starting server:", err)
}
}

4
cmd/exit/.env Normal file
View File

@ -0,0 +1,4 @@
#NOSTR_RELAYS = 'wss://relay.8333.space'
NOSTR_RELAYS = 'ws://localhost:6666'
NOSTR_PRIVATE_KEY = ""
BACKEND_HOST = 'localhost:3338'

17
cmd/exit/Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM golang:1.21-alpine as builder
ADD . /build/
WORKDIR /build
RUN apk add --no-cache git bash openssh-client && \
go build -o exit cmd/exit/*.go
#building finished. Now extracting single bin in second stage.
FROM alpine
COPY --from=builder /build/exit /app/
WORKDIR /app
CMD ["./exit"]

23
cmd/exit/exit.go Normal file
View File

@ -0,0 +1,23 @@
package main
import (
"github.com/asmogo/nws/config"
"github.com/asmogo/nws/exit"
"golang.org/x/net/context"
)
func main() {
// load the configuration
// from the environment
cfg, err := config.LoadConfig[config.ExitConfig]()
if err != nil {
panic(err)
}
// create a new gw server
// and start it
ctx := context.Background()
exitNode := exit.NewExit(ctx, cfg)
exitNode.ListenAndServe(ctx)
}

3
cmd/proxy/.env Normal file
View File

@ -0,0 +1,3 @@
#NOSTR_RELAYS = 'wss://relay.8333.space'
NOSTR_RELAYS = 'ws://localhost:6666'
NOSTR_PRIVATE_KEY = ""

17
cmd/proxy/Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM golang:1.21-alpine as builder
ADD . /build/
WORKDIR /build
RUN apk add --no-cache git bash openssh-client && \
go build -o proxy cmd/proxy/*.go
#building finished. Now extracting single bin in second stage.
FROM alpine
COPY --from=builder /build/proxy /app/
WORKDIR /app
CMD ["./proxy"]

25
cmd/proxy/proxy.go Normal file
View File

@ -0,0 +1,25 @@
package main
import (
"github.com/asmogo/nws/config"
"github.com/asmogo/nws/proxy"
"golang.org/x/net/context"
)
func main() {
// load the configuration
// from the environment
cfg, err := config.LoadConfig[config.ProxyConfig]()
if err != nil {
panic(err)
}
// create a new gw server
// and start it
socksProxy := proxy.New(context.Background(), cfg)
err = socksProxy.Start()
if err != nil {
panic(err)
}
}

68
config/config.go Normal file
View File

@ -0,0 +1,68 @@
package config
import (
"fmt"
"log/slog"
"os"
"github.com/caarlos0/env/v11"
"github.com/joho/godotenv"
)
type ProxyConfig struct {
NostrRelays []string `env:"NOSTR_RELAYS" envSeparator:";"`
}
type ExitConfig struct {
NostrRelays []string `env:"NOSTR_RELAYS" envSeparator:";"`
NostrPrivateKey string `env:"NOSTR_PRIVATE_KEY"`
BackendHost string `env:"BACKEND_HOST"`
BackendScheme string `env:"BACKEND_SCHEME"`
}
// load the and marshal Configuration from .env file from the UserHomeDir
// if this file was not found, fallback to the os environment variables
func LoadConfig[T any]() (*T, error) {
// load current users home directory as a string
homeDir, err := os.UserHomeDir()
if err != nil {
slog.Error("error loading home directory", err)
}
// check if .env file exist in the home directory
// if it does, load the configuration from it
// else fallback to the os environment variables
if _, err := os.Stat(homeDir + "/.env"); err == nil {
// load configuration from .env file
return loadFromEnv[T](homeDir + "/.env")
} else if _, err := os.Stat(".env"); err == nil {
// load configuration from .env file in current directory
return loadFromEnv[T]("")
} else {
// load configuration from os environment variables
return loadFromEnv[T]("")
}
}
// loadFromEnv loads the configuration from the specified .env file path.
// If the path is empty, it does not load any configuration.
// It returns an error if there was a problem loading the configuration.
func loadFromEnv[T any](path string) (*T, error) {
// check path
// load configuration from .env file
err := godotenv.Load()
if err != nil {
cfg, err := env.ParseAs[T]()
if err != nil {
fmt.Printf("%+v\n", err)
}
return &cfg, nil
}
// or you can use generics
cfg, err := env.ParseAs[T]()
if err != nil {
fmt.Printf("%+v\n", err)
}
return &cfg, nil
}

60
docker-compose.yaml Normal file
View File

@ -0,0 +1,60 @@
version: '3'
networks:
nostr:
enable_ipv6: true
ipam:
config:
- subnet: fd00:db8:a::/64
gateway: fd00:db8:a::1
services:
mint:
image: cashubtc/nutshell:0.15.3
container_name: mint
ports:
- "3338"
networks:
nostr:
environment:
- MINT_BACKEND_BOLT11_SAT=FakeWallet
- MINT_LISTEN_HOST=0.0.0.0
- MINT_LISTEN_PORT=3338
- MINT_PRIVATE_KEY=TEST_PRIVATE_KEY
- MINT_INFO_DESCRIPTION=This Cashu test mint has no public IP address and can only be reached via NWS powered by Nostr
- MINT_INFO_NAME=Cashu NWS mint
command: ["poetry", "run", "mint"]
exit:
build:
context: .
dockerfile: cmd/exit/Dockerfile
container_name: exit
networks:
nostr:
environment:
- NOSTR_RELAYS=ws://nostr-relay:8080
- NOSTR_PRIVATE_KEY=003632642b6df1bb7f150c25aae079d590e6cfcceca924304154fbc2a3a938e3
- BACKEND_HOST=mint:3338
proxy:
build:
context: .
dockerfile: cmd/proxy/Dockerfile
container_name: proxy
ports:
- 8882:8882
networks:
nostr:
environment:
- NOSTR_RELAYS=ws://nostr-relay:8080
- NOSTR_PRIVATE_KEY=b0aceff311951aaa014c3296f4346f91f7fd4fc17396e060acbb48d2f42ef1fe
nostr:
image: scsibug/nostr-rs-relay:latest
container_name: nostr-relay
ports:
- 8080:8080
networks:
nostr:
restart: always
volumes:
- ./nostr/data:/usr/src/app/db:Z
- ./nostr/config/config.toml:/usr/src/app/config.toml:ro,Z
user: 100:100

219
exit/exit.go Normal file
View File

@ -0,0 +1,219 @@
package exit
import (
"crypto/tls"
"fmt"
"github.com/asmogo/nws/config"
"github.com/asmogo/nws/netstr"
"github.com/asmogo/nws/protocol"
"github.com/asmogo/nws/socks5"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip04"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/puzpuzpuz/xsync/v3"
"golang.org/x/net/context"
"log"
"log/slog"
"net"
"net/http"
_ "net/http/pprof"
)
// Exit represents a structure that holds information related to an exit node.
type Exit struct {
// pool represents a pool of relays and manages the subscription to incoming events from relays.
pool *nostr.SimplePool
// config is a field in the Exit struct that holds information related to exit node configuration.
config *config.ExitConfig
// relays represents a slice of *nostr.Relay, which contains information about the relay nodes used by the Exit node.
// Todo -- check if this is deprecated
relays []*nostr.Relay
// nostrConnectionMap is a concurrent map used to store connections for the Exit node.
// It is used to establish and maintain connections between the Exit node and the backend host.
nostrConnectionMap *xsync.MapOf[string, *netstr.NostrConnection]
// mutexMap is a field in the Exit struct that represents a map used for synchronizing access to resources based on a string key.
mutexMap *MutexMap
// incomingChannel represents a channel used to receive incoming events from relays.
incomingChannel chan nostr.IncomingEvent
}
// NewExit creates a new Exit node with the provided context and config.
func NewExit(ctx context.Context, config *config.ExitConfig) *Exit {
// todo -- this is for debugging purposes only and should be removed
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
pool := nostr.NewSimplePool(ctx)
exit := &Exit{
nostrConnectionMap: xsync.NewMapOf[string, *netstr.NostrConnection](),
config: config,
pool: pool,
mutexMap: NewMutexMap(),
}
for _, relayUrl := range config.NostrRelays {
relay, err := exit.pool.EnsureRelay(relayUrl)
if err != nil {
fmt.Println(err)
continue
}
exit.relays = append(exit.relays, relay)
fmt.Printf("added relay connection to %s\n", relayUrl)
}
pubKey, err := nostr.GetPublicKey(config.NostrPrivateKey)
if err != nil {
panic(err)
}
profile, err := nip19.EncodeProfile(pubKey,
config.NostrRelays)
if err != nil {
panic(err)
}
slog.Info("created exit node", "profile", profile)
err = exit.setSubscriptions(ctx)
if err != nil {
panic(err)
}
return exit
}
// setSubscriptions sets up subscriptions for the Exit node to receive incoming events from the specified relays.
// It first obtains the public key using the configured Nostr private key.
// Then it calls the `handleSubscription` method to open a subscription to the relays with the specified filters.
// This method runs in a separate goroutine and continuously handles the incoming events by calling the `processMessage` method.
// If the context is canceled before the subscription is established, it returns the context error.
// If any errors occur during the process, they are returned.
// This method should be called once when starting the Exit node.
func (e *Exit) setSubscriptions(ctx context.Context) error {
pubKey, err := nostr.GetPublicKey(e.config.NostrPrivateKey)
if err != nil {
return err
}
now := nostr.Now()
if err = e.handleSubscription(ctx, pubKey, now); err != nil {
return err
}
return nil
}
// handleSubscription handles the subscription to incoming events from relays based on the provided filters.
// It sets up the incoming event channel and starts a goroutine to handle the events.
// It returns an error if there is any issue with the subscription.
func (e *Exit) handleSubscription(ctx context.Context, pubKey string, since nostr.Timestamp) error {
incomingEventChannel := e.pool.SubMany(ctx, e.config.NostrRelays, nostr.Filters{
{Kinds: []int{protocol.KindEphemeralEvent},
Since: &since,
Tags: nostr.TagMap{
"p": []string{pubKey},
}},
})
e.incomingChannel = incomingEventChannel
return nil
}
// ListenAndServe handles incoming events from the subscription channel.
// It processes each event by calling the processMessage method, as long as the event is not nil.
// If the context is canceled (ctx.Done() receives a value), the method returns.
func (e *Exit) ListenAndServe(ctx context.Context) {
for {
select {
case event := <-e.incomingChannel:
slog.Debug("received event", "event", event)
if event.Relay == nil {
continue
}
go e.processMessage(ctx, event)
case <-ctx.Done():
return
}
}
}
// processMessage decrypts and unmarshals the incoming event message, and then
// routes the message to the appropriate handler based on its protocol type.
func (e *Exit) processMessage(ctx context.Context, msg nostr.IncomingEvent) {
sharedKey, err := nip04.ComputeSharedSecret(msg.PubKey, e.config.NostrPrivateKey)
if err != nil {
return
}
decodedMessage, err := nip04.Decrypt(msg.Content, sharedKey)
if err != nil {
return
}
protocolMessage, err := protocol.UnmarshalJSON([]byte(decodedMessage))
if err != nil {
slog.Error("could not unmarshal message")
return
}
switch protocolMessage.Type {
case protocol.MessageConnect:
e.handleConnect(ctx, msg, protocolMessage, false)
case protocol.MessageTypeSocks5:
e.handleSocks5ProxyMessage(msg, protocolMessage)
}
}
// handleConnect handles the connection for the given message and protocol message.
// It locks the mutex for the protocol message key, encodes the receiver's profile,
// creates a new connection with the provided context and options, and establishes
// a connection to the backend host.
// If the connection cannot be established, it logs an error and returns.
// It then stores the connection in the nostrConnectionMap and creates two goroutines
// to proxy the data between the connection and the backend.
func (e *Exit) handleConnect(ctx context.Context, msg nostr.IncomingEvent, protocolMessage *protocol.Message, isTLS bool) {
e.mutexMap.Lock(protocolMessage.Key.String())
defer e.mutexMap.Unlock(protocolMessage.Key.String())
receiver, err := nip19.EncodeProfile(msg.PubKey, []string{msg.Relay.String()})
if err != nil {
return
}
connection := netstr.NewConnection(
ctx,
netstr.WithPrivateKey(e.config.NostrPrivateKey),
netstr.WithDst(receiver),
netstr.WithUUID(protocolMessage.Key),
)
var dst net.Conn
if isTLS {
conf := tls.Config{InsecureSkipVerify: true}
dst, err = tls.Dial("tcp", e.config.BackendHost, &conf)
} else {
dst, err = net.Dial("tcp", e.config.BackendHost)
}
if err != nil {
slog.Error("could not connect to backend", "error", err)
return
}
e.nostrConnectionMap.Store(protocolMessage.Key.String(), connection)
go socks5.Proxy(dst, connection, nil)
go socks5.Proxy(connection, dst, nil)
}
// handleSocks5ProxyMessage handles the SOCKS5 proxy message by writing it to the destination connection.
// If the destination connection does not exist, the function returns without doing anything.
//
// Parameters:
// - msg: The incoming event containing the SOCKS5 proxy message.
// - protocolMessage: The protocol message associated with the incoming event.
func (e *Exit) handleSocks5ProxyMessage(
msg nostr.IncomingEvent,
protocolMessage *protocol.Message,
) {
e.mutexMap.Lock(protocolMessage.Key.String())
defer e.mutexMap.Unlock(protocolMessage.Key.String())
dst, ok := e.nostrConnectionMap.Load(protocolMessage.Key.String())
if !ok {
return
}
dst.WriteNostrEvent(msg)
}

40
exit/mutex.go Normal file
View File

@ -0,0 +1,40 @@
package exit
import (
"fmt"
"sync"
)
type MutexMap struct {
mu sync.Mutex // a separate mutex to protect the map
m map[string]*sync.Mutex // map from IDs to mutexes
}
func NewMutexMap() *MutexMap {
return &MutexMap{
m: make(map[string]*sync.Mutex),
}
}
func (mm *MutexMap) Lock(id string) {
mm.mu.Lock()
mutex, ok := mm.m[id]
if !ok {
mutex = &sync.Mutex{}
mm.m[id] = mutex
}
mm.mu.Unlock()
mutex.Lock()
}
func (mm *MutexMap) Unlock(id string) {
mm.mu.Lock()
mutex, ok := mm.m[id]
mm.mu.Unlock()
if !ok {
panic(fmt.Sprintf("tried to unlock mutex for non-existent id %s", id))
}
mutex.Unlock()
}

36
go.mod Normal file
View File

@ -0,0 +1,36 @@
module github.com/asmogo/nws
go 1.21
require (
github.com/caarlos0/env/v11 v11.0.0
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/nbd-wtf/go-nostr v0.30.2
github.com/puzpuzpuz/xsync/v3 v3.0.2
github.com/samber/lo v1.45.0
github.com/stretchr/testify v1.9.0
golang.org/x/net v0.23.0
)
require (
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.2.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

147
go.sum Normal file
View File

@ -0,0 +1,147 @@
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U=
github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ=
github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9w277ABlgELO7H04IM=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/caarlos0/env/v11 v11.0.0 h1:ZIlkOjuL3xoZS0kmUJlF74j2Qj8GMOq3CDLX/Viak8Q=
github.com/caarlos0/env/v11 v11.0.0/go.mod h1:2RC3HQu8BQqtEK3V4iHPxj0jOdWdbPpWJ6pOueeU1xM=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.2.0 h1:u0p9s3xLYpZCA1z5JgCkMeB34CKCMMQbM+G8Ii7YD0I=
github.com/gobwas/ws v1.2.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/nbd-wtf/go-nostr v0.30.2 h1:dG/2X52/XDg+7phZH+BClcvA5D+S6dXvxJKkBaySEzI=
github.com/nbd-wtf/go-nostr v0.30.2/go.mod h1:tiKJY6fWYSujbTQb201Y+IQ3l4szqYVt+fsTnsm7FCk=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
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/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew=
github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/samber/lo v1.45.0 h1:TPK85Y30Lv9Jh8s3TrJeA94u1hwcbFA9JObx/vT6lYU=
github.com/samber/lo v1.45.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o=
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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=

16
netstr/address.go Normal file
View File

@ -0,0 +1,16 @@
package netstr
// NostrAddress represents a type that holds the profile and public key of a Nostr address.
type NostrAddress struct {
Nprofile string
pubkey string
}
func (n NostrAddress) String() string {
return n.Nprofile
}
// Network returns the network type of the NostrAddress, which is "nostr".
func (n NostrAddress) Network() string {
return "nostr"
}

326
netstr/conn.go Normal file
View File

@ -0,0 +1,326 @@
package netstr
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"github.com/asmogo/nws/protocol"
"github.com/google/uuid"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip04"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/samber/lo"
"log/slog"
"net"
"time"
)
// NostrConnection implements the net.Conn interface.
// It is used to establish a connection to Nostr relays.
// It provides methods for reading and writing data.
type NostrConnection struct {
// uuid is a field of type uuid.UUID in the NostrConnection struct.
uuid uuid.UUID
// ctx is a field of type context.Context in the NostrConnection struct.
ctx context.Context
// cancel is a field of type context.CancelFunc in the NostrConnection struct.
cancel context.CancelFunc
// readBuffer is a field of type `bytes.Buffer` in the `NostrConnection` struct.
// It is used to store the decrypted message from incoming events.
// The `handleSubscription` method continuously listens for events on the subscription channel,
// decrypts the event content, and writes the decrypted message to the `readBuffer`.
readBuffer bytes.Buffer
// private key of the connection
privateKey string
// NostrConnection represents a connection object.
pool *nostr.SimplePool
// dst is a field that represents the destination address for the Nostr connection configuration.
dst string
// subscriptionChan is a channel of type protocol.IncomingEvent.
// It is used to write incoming events which will be read and processed by the Read method.
subscriptionChan chan nostr.IncomingEvent
// readIds represents the list of event IDs that have been read by the NostrConnection object.
readIds []string
// writeIds is a field of type []string in the NostrConnection struct.
// It stores the IDs of the events that have been written to the connection.
// This field is used to check if an event has already been written and avoid duplicate writes.
writeIds []string
// sentBytes is a field that stores the bytes of data that have been sent by the connection.
sentBytes [][]byte
// sub represents a boolean value indicating if a connection should subscribe to a response when writing.
sub bool
}
// WriteNostrEvent writes the incoming event to the subscription channel of the NostrConnection.
// The subscription channel is used by the Read method to read events and handle them.
// Parameters:
// - event: The incoming event to be written to the subscription channel.
func (nc *NostrConnection) WriteNostrEvent(event nostr.IncomingEvent) {
nc.subscriptionChan <- event
}
// NewConnection creates a new NostrConnection object with the provided context and options.
// It initializes the config with default values, processes the options to customize the config,
// and creates a new NostrConnection object using the config.
// The NostrConnection object includes the privateKey, dst, pool, ctx, cancel, sub, subscriptionChan, readIds, and sentBytes fields.
// If an uuid is provided in the options, it is assigned to the NostrConnection object.
// The NostrConnection object is then returned.
func NewConnection(ctx context.Context, opts ...NostrConnOption) *NostrConnection {
ctx, c := context.WithCancel(ctx)
nostrConnection := &NostrConnection{
pool: nostr.NewSimplePool(ctx),
ctx: ctx,
cancel: c,
subscriptionChan: make(chan nostr.IncomingEvent),
readIds: make([]string, 0),
sentBytes: make([][]byte, 0),
}
for _, opt := range opts {
opt(nostrConnection)
}
return nostrConnection
}
// Read reads data from the connection. The data is decrypted and returned in the provided byte slice.
// If there is no data available, Read blocks until data arrives or the context is canceled.
// If the context is canceled before data is received, Read returns an error.
//
// The number of bytes read is returned as n and any error encountered is returned as err.
// The content of the decrypted message is then copied to the provided byte slice b.
func (nc *NostrConnection) Read(b []byte) (n int, err error) {
return nc.handleNostrRead(b, n)
}
// handleNostrRead reads the incoming events from the subscription channel and processes them.
// It checks if the event has already been read, decrypts the content using the shared key,
// unmarshals the decoded message and copies the content into the provided byte slice.
// It returns the number of bytes copied and any error encountered.
// If the context is canceled, it returns an error with "context canceled" message.
func (nc *NostrConnection) handleNostrRead(b []byte, n int) (int, error) {
for {
select {
case event := <-nc.subscriptionChan:
if event.Relay == nil {
return 0, nil
}
// check if we have already read this event
if lo.Contains(nc.readIds, event.ID) {
continue
}
nc.readIds = append(nc.readIds, event.ID)
sharedKey, err := nip04.ComputeSharedSecret(event.PubKey, nc.privateKey)
if err != nil {
return 0, err
}
decodedMessage, err := nip04.Decrypt(event.Content, sharedKey)
if err != nil {
return 0, err
}
message, err := protocol.UnmarshalJSON([]byte(decodedMessage))
if err != nil {
return 0, err
}
slog.Info("reading", slog.String("event", event.ID), slog.String("content", base64.StdEncoding.EncodeToString(message.Data)))
n = copy(b, message.Data)
return n, nil
case <-nc.ctx.Done():
return 0, fmt.Errorf("context canceled")
default:
time.Sleep(time.Millisecond * 100)
}
}
}
// Write writes data to the connection.
// It delegates the writing logic to handleNostrWrite method.
// The number of bytes written and error (if any) are returned.
func (nc *NostrConnection) Write(b []byte) (n int, err error) {
return nc.handleNostrWrite(b, err)
}
// handleNostrWrite handles the writing of a Nostr event.
// It checks if the event has already been sent, parses the destination,
// creates a message signer, creates message options, signs the event,
// publishes the event to relays, and appends the sent bytes to the connection's sentBytes array.
// The method returns the number of bytes written and any error that occurred.
func (nc *NostrConnection) handleNostrWrite(b []byte, err error) (int, error) {
// check if we have already sent this event
publicKey, relays, err := ParseDestination(nc.dst)
if err != nil {
return 0, err
}
signer, err := protocol.NewEventSigner(nc.privateKey)
if err != nil {
return 0, err
}
// create message options
opts := []protocol.MessageOption{
protocol.WithUUID(nc.uuid),
protocol.WithType(protocol.MessageTypeSocks5),
protocol.WithData(b),
}
ev, err := signer.CreateSignedEvent(publicKey, nostr.Tags{nostr.Tag{"p", publicKey}}, opts...)
if err != nil {
return 0, err
}
if lo.Contains(nc.writeIds, ev.ID) {
slog.Info("event already sent", slog.String("event", ev.ID))
return 0, nil
}
nc.writeIds = append(nc.writeIds, ev.ID)
if nc.sub {
nc.sub = false
now := nostr.Now()
incomingEventChannel := nc.pool.SubMany(nc.ctx, relays,
nostr.Filters{
{Kinds: []int{protocol.KindEphemeralEvent},
Authors: []string{publicKey},
Since: &now,
Tags: nostr.TagMap{
"p": []string{ev.PubKey},
}}})
nc.subscriptionChan = incomingEventChannel
}
for _, responseRelay := range relays {
var relay *nostr.Relay
relay, err = nc.pool.EnsureRelay(responseRelay)
if err != nil {
return 0, err
}
err = relay.Publish(nc.ctx, ev)
if err != nil {
return 0, err
}
}
nc.sentBytes = append(nc.sentBytes, b)
slog.Info("writing", slog.String("event", ev.ID), slog.String("content", base64.StdEncoding.EncodeToString(b)))
return len(b), nil
}
// ParseDestination takes a destination string and returns a public key and relays.
// The destination can be "npub" or "nprofile".
// If the prefix is "npub", the public key is extracted.
// If the prefix is "nprofile", the public key and relays are extracted.
// Returns the public key, relays (if any), and any error encountered.
func ParseDestination(destination string) (string, []string, error) {
// destination can be npub or nprofile
prefix, pubKey, err := nip19.Decode(destination)
if err != nil {
return "", nil, err
}
var relays []string
var publicKey string
switch prefix {
case "npub":
publicKey = pubKey.(string)
case "nprofile":
profilePointer := pubKey.(nostr.ProfilePointer)
publicKey = profilePointer.PublicKey
relays = profilePointer.Relays
}
return publicKey, relays, nil
}
func (nc *NostrConnection) Close() error {
nc.cancel()
return nil
}
func (nc *NostrConnection) LocalAddr() net.Addr {
return &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 9333}
}
func (nc *NostrConnection) RemoteAddr() net.Addr {
return &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}
}
func (nc *NostrConnection) SetDeadline(t time.Time) error {
return nil
}
func (nc *NostrConnection) SetReadDeadline(t time.Time) error {
return nil
}
func (nc *NostrConnection) SetWriteDeadline(t time.Time) error {
return nil
}
// NostrConnOption is a functional option type for configuring NostrConnConfig.
type NostrConnOption func(*NostrConnection)
// WithPrivateKey sets the private key for the NostrConnConfig.
func WithPrivateKey(privateKey string) NostrConnOption {
return func(config *NostrConnection) {
config.privateKey = privateKey
}
}
// WithSub is a function that returns a NostrConnOption. When this option is applied
// to a NostrConnConfig, it sets the 'sub' field to true, indicating that
// the connection will handle subscriptions.
func WithSub(...bool) NostrConnOption {
return func(connection *NostrConnection) {
connection.sub = true
go connection.handleSubscription()
}
}
// WithDst is a NostrConnOption function that sets the destination address for the Nostr connection configuration.
// It takes a string parameter `dst` and updates the `config.dst` field accordingly.
func WithDst(dst string) NostrConnOption {
return func(connection *NostrConnection) {
connection.dst = dst
}
}
// WithUUID sets the UUID option for creating a NostrConnConfig.
// It assigns the provided UUID to the config's uuid field.
func WithUUID(uuid uuid.UUID) NostrConnOption {
return func(connection *NostrConnection) {
connection.uuid = uuid
}
}
// handleSubscription handles the subscription channel for incoming events.
// It continuously listens for events on the subscription channel and performs necessary operations.
// If the event has a valid relay, it computes the shared key and decrypts the event content.
// The decrypted message is then written to the read buffer.
// If the context is canceled, the method returns.
func (nc *NostrConnection) handleSubscription() {
for {
select {
case event := <-nc.subscriptionChan:
if event.Relay == nil {
continue
}
sharedKey, err := nip04.ComputeSharedSecret(event.PubKey, nc.privateKey)
if err != nil {
continue
}
decodedMessage, err := nip04.Decrypt(event.Content, sharedKey)
if err != nil {
continue
}
nc.readBuffer.Write([]byte(decodedMessage))
case <-nc.ctx.Done():
return
}
}
}

63
netstr/dial.go Normal file
View File

@ -0,0 +1,63 @@
package netstr
import (
"context"
"fmt"
"github.com/asmogo/nws/protocol"
"github.com/google/uuid"
"github.com/nbd-wtf/go-nostr"
"log/slog"
"net"
"strings"
)
// DialSocks connects to a destination using the provided SimplePool and returns a Dialer function.
// It creates a new Connection using the specified context, private key, destination address, subscription flag, and connectionID.
// It parses the destination address to get the public key and relays.
// It creates a signed event using the private key, public key, and destination address.
// It ensures that the relays are available in the pool and publishes the signed event to each relay.
// Finally, it returns the Connection and nil error. If there are any errors, nil connection and the error are returned.
func DialSocks(pool *nostr.SimplePool) func(ctx context.Context, net_, addr string) (net.Conn, error) {
return func(ctx context.Context, net_, addr string) (net.Conn, error) {
addr = strings.ReplaceAll(addr, ".", "")
connectionID := uuid.New()
key := nostr.GeneratePrivateKey()
connection := NewConnection(ctx,
WithPrivateKey(key),
WithDst(addr),
WithSub(),
WithUUID(connectionID))
publicKey, relays, err := ParseDestination(addr)
if err != nil {
slog.Error("error parsing host", err)
return nil, fmt.Errorf("error parsing host: %w", err)
}
// create nostr signed event
signer, err := protocol.NewEventSigner(key)
if err != nil {
return nil, err
}
opts := []protocol.MessageOption{
protocol.WithType(protocol.MessageConnect),
protocol.WithUUID(connectionID),
protocol.WithDestination(addr),
}
ev, err := signer.CreateSignedEvent(publicKey,
nostr.Tags{nostr.Tag{"p", publicKey}},
opts...)
for _, relayUrl := range relays {
relay, err := pool.EnsureRelay(relayUrl)
if err != nil {
slog.Error("error creating relay", err)
continue
}
err = relay.Publish(ctx, ev)
if err != nil {
return nil, err
}
}
return connection, nil
}
}

13
netstr/dns.go Normal file
View File

@ -0,0 +1,13 @@
package netstr
import (
"context"
"net"
)
// NostrDNS does not resolve anything
type NostrDNS struct{}
func (d NostrDNS) Resolve(ctx context.Context, name string) (context.Context, net.IP, error) {
return ctx, net.IP{0, 0, 0, 0}, nil
}

BIN
nws.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

63
protocol/message.go Normal file
View File

@ -0,0 +1,63 @@
package protocol
import (
"encoding/json"
"github.com/google/uuid"
)
type MessageType string
var (
MessageTypeSocks5 = MessageType("SOCKS5")
MessageConnect = MessageType("CONNECT")
)
type Message struct {
Key uuid.UUID `json:"key,omitempty"`
Type MessageType `json:"type,omitempty"`
Data []byte `json:"data,omitempty"`
Destination string `json:"destination,omitempty"`
}
type MessageOption func(*Message)
func WithUUID(uuid uuid.UUID) MessageOption {
return func(m *Message) {
m.Key = uuid
}
}
func WithType(messageType MessageType) MessageOption {
return func(m *Message) {
m.Type = messageType
}
}
func WithDestination(destination string) MessageOption {
return func(m *Message) {
m.Destination = destination
}
}
func WithData(data []byte) MessageOption {
return func(m *Message) {
m.Data = data
}
}
func NewMessage(configs ...MessageOption) *Message {
m := &Message{}
for _, config := range configs {
config(m)
}
return m
}
func MarshalJSON(m *Message) ([]byte, error) {
return json.Marshal(m)
}
func UnmarshalJSON(data []byte) (*Message, error) {
m := NewMessage()
if err := json.Unmarshal(data, &m); err != nil {
return nil, err
}
return m, nil
}

75
protocol/signer.go Normal file
View File

@ -0,0 +1,75 @@
package protocol
import (
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip04"
"log/slog"
)
// KindEphemeralEvent represents the unique identifier for ephemeral events.
const KindEphemeralEvent int = 38333
// EventSigner represents a signer that can create and sign events.
//
// EventSigner provides methods for creating unsigned events, creating signed events,
// and decrypting and writing events received from an exit node.
type EventSigner struct {
PublicKey string
privateKey string
}
// NewEventSigner creates a new EventSigner
func NewEventSigner(privateKey string) (*EventSigner, error) {
myPublicKey, err := nostr.GetPublicKey(privateKey)
if err != nil {
slog.Error("could not generate pubkey")
return nil, err
}
signer := &EventSigner{
privateKey: privateKey,
PublicKey: myPublicKey,
}
return signer, nil
}
// CreateEvent creates a new Event with the provided tags. The Public Key and the
// current timestamp are set automatically. The Kind is set to KindEphemeralEvent.
func (s *EventSigner) CreateEvent(tags nostr.Tags) nostr.Event {
return nostr.Event{
PubKey: s.PublicKey,
CreatedAt: nostr.Now(),
Kind: KindEphemeralEvent,
Tags: tags,
}
}
// CreateSignedEvent creates a signed Nostr event with the provided target public key, tags, and options.
// It computes the shared key between the target public key and the private key of the EventSigner.
// Then, it creates a new message with the provided options.
// The message is serialized to JSON and encrypted using the shared key.
// The method then calls CreateEvent to create a new unsigned event with the provided tags.
// The encrypted message is set as the content of the event.
// Finally, the event is signed with the private key of the EventSigner, setting the event ID and event Sig fields.
// The signed event is returned along with any error that occurs.
func (s *EventSigner) CreateSignedEvent(targetPublicKey string, tags nostr.Tags, opts ...MessageOption) (nostr.Event, error) {
sharedKey, err := nip04.ComputeSharedSecret(targetPublicKey, s.privateKey)
if err != nil {
return nostr.Event{}, err
}
message := NewMessage(
opts...,
)
messageJson, err := MarshalJSON(message)
if err != nil {
return nostr.Event{}, err
}
encryptedMessage, err := nip04.Encrypt(string(messageJson), sharedKey)
ev := s.CreateEvent(tags)
ev.Content = encryptedMessage
// calling Sign sets the event ID field and the event Sig field
err = ev.Sign(s.privateKey)
if err != nil {
return nostr.Event{}, err
}
return ev, nil
}

68
proxy/proxy.go Normal file
View File

@ -0,0 +1,68 @@
package proxy
import (
"context"
"fmt"
"github.com/asmogo/nws/config"
"github.com/asmogo/nws/netstr"
"github.com/asmogo/nws/socks5"
"github.com/nbd-wtf/go-nostr"
"log"
"net"
"net/http"
_ "net/http/pprof"
)
type Proxy struct {
config *config.ProxyConfig // the configuration for the gateway
// a list of nostr relays to publish events to
relays []*nostr.Relay // deprecated -- should be used for default relay configuration
pool *nostr.SimplePool
socksServer *socks5.Server
}
func New(ctx context.Context, config *config.ProxyConfig) *Proxy {
// we need a webserver to get the pprof webserver
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
s := &Proxy{
config: config,
pool: nostr.NewSimplePool(ctx),
}
socksServer, err := socks5.New(&socks5.Config{
AuthMethods: nil,
Credentials: nil,
Resolver: netstr.NostrDNS{},
Rules: nil,
Rewriter: nil,
BindIP: net.IP{0, 0, 0, 0},
Logger: nil,
Dial: nil,
}, s.pool)
if err != nil {
panic(err)
}
s.socksServer = socksServer
// publish the event to two relays
for _, relayUrl := range config.NostrRelays {
relay, err := s.pool.EnsureRelay(relayUrl)
if err != nil {
fmt.Println(err)
continue
}
s.relays = append(s.relays, relay)
fmt.Printf("added relay connection to %s\n", relayUrl)
}
return s
}
// Start should start the server
func (s *Proxy) Start() error {
err := s.socksServer.ListenAndServe("tcp", "8882")
if err != nil {
panic(err)
}
return nil
}

20
socks5/LICENSE Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2014 Armon Dadgar
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

45
socks5/README.md Normal file
View File

@ -0,0 +1,45 @@
go-socks5 [![Build Status](https://travis-ci.org/armon/go-socks5.png)](https://travis-ci.org/armon/go-socks5)
=========
Provides the `socks5` package that implements a [SOCKS5 server](http://en.wikipedia.org/wiki/SOCKS).
SOCKS (Secure Sockets) is used to route traffic between a client and server through
an intermediate proxy layer. This can be used to bypass firewalls or NATs.
Feature
=======
The package has the following features:
* "No Auth" mode
* User/Password authentication
* Support for the CONNECT command
* Rules to do granular filtering of commands
* Custom DNS resolution
* Unit tests
TODO
====
The package still needs the following:
* Support for the BIND command
* Support for the ASSOCIATE command
Example
=======
Below is a simple example of usage
```go
// Create a SOCKS5 server
conf := &socks5.Config{}
server, err := socks5.New(conf)
if err != nil {
panic(err)
}
// Create SOCKS5 proxy on localhost port 8000
if err := server.ListenAndServe("tcp", "127.0.0.1:8000"); err != nil {
panic(err)
}
```

151
socks5/auth.go Normal file
View File

@ -0,0 +1,151 @@
package socks5
import (
"fmt"
"io"
)
const (
NoAuth = uint8(0)
noAcceptable = uint8(255)
UserPassAuth = uint8(2)
userAuthVersion = uint8(1)
authSuccess = uint8(0)
authFailure = uint8(1)
)
var (
UserAuthFailed = fmt.Errorf("User authentication failed")
NoSupportedAuth = fmt.Errorf("No supported authentication mechanism")
)
// A Request encapsulates authentication state provided
// during negotiation
type AuthContext struct {
// Provided auth method
Method uint8
// Payload provided during negotiation.
// Keys depend on the used auth method.
// For UserPassauth contains Username
Payload map[string]string
}
type Authenticator interface {
Authenticate(reader io.Reader, writer io.Writer) (*AuthContext, error)
GetCode() uint8
}
// NoAuthAuthenticator is used to handle the "No Authentication" mode
type NoAuthAuthenticator struct{}
func (a NoAuthAuthenticator) GetCode() uint8 {
return NoAuth
}
func (a NoAuthAuthenticator) Authenticate(reader io.Reader, writer io.Writer) (*AuthContext, error) {
_, err := writer.Write([]byte{socks5Version, NoAuth})
return &AuthContext{NoAuth, nil}, err
}
// UserPassAuthenticator is used to handle username/password based
// authentication
type UserPassAuthenticator struct {
Credentials CredentialStore
}
func (a UserPassAuthenticator) GetCode() uint8 {
return UserPassAuth
}
func (a UserPassAuthenticator) Authenticate(reader io.Reader, writer io.Writer) (*AuthContext, error) {
// Tell the client to use user/pass auth
if _, err := writer.Write([]byte{socks5Version, UserPassAuth}); err != nil {
return nil, err
}
// Get the version and username length
header := []byte{0, 0}
if _, err := io.ReadAtLeast(reader, header, 2); err != nil {
return nil, err
}
// Ensure we are compatible
if header[0] != userAuthVersion {
return nil, fmt.Errorf("Unsupported auth version: %v", header[0])
}
// Get the user name
userLen := int(header[1])
user := make([]byte, userLen)
if _, err := io.ReadAtLeast(reader, user, userLen); err != nil {
return nil, err
}
// Get the password length
if _, err := reader.Read(header[:1]); err != nil {
return nil, err
}
// Get the password
passLen := int(header[0])
pass := make([]byte, passLen)
if _, err := io.ReadAtLeast(reader, pass, passLen); err != nil {
return nil, err
}
// Verify the password
if a.Credentials.Valid(string(user), string(pass)) {
if _, err := writer.Write([]byte{userAuthVersion, authSuccess}); err != nil {
return nil, err
}
} else {
if _, err := writer.Write([]byte{userAuthVersion, authFailure}); err != nil {
return nil, err
}
return nil, UserAuthFailed
}
// Done
return &AuthContext{UserPassAuth, map[string]string{"Username": string(user)}}, nil
}
// authenticate is used to handle connection authentication
func (s *Server) authenticate(conn io.Writer, bufConn io.Reader) (*AuthContext, error) {
// Get the methods
methods, err := readMethods(bufConn)
if err != nil {
return nil, fmt.Errorf("Failed to get auth methods: %v", err)
}
// Select a usable method
for _, method := range methods {
cator, found := s.authMethods[method]
if found {
return cator.Authenticate(bufConn, conn)
}
}
// No usable method found
return nil, noAcceptableAuth(conn)
}
// noAcceptableAuth is used to handle when we have no eligible
// authentication mechanism
func noAcceptableAuth(conn io.Writer) error {
conn.Write([]byte{socks5Version, noAcceptable})
return NoSupportedAuth
}
// readMethods is used to read the number of methods
// and proceeding auth methods
func readMethods(r io.Reader) ([]byte, error) {
header := []byte{0}
if _, err := r.Read(header); err != nil {
return nil, err
}
numMethods := int(header[0])
methods := make([]byte, numMethods)
_, err := io.ReadAtLeast(r, methods, numMethods)
return methods, err
}

119
socks5/auth_test.go Normal file
View File

@ -0,0 +1,119 @@
package socks5
import (
"bytes"
"testing"
)
func TestNoAuth(t *testing.T) {
req := bytes.NewBuffer(nil)
req.Write([]byte{1, NoAuth})
var resp bytes.Buffer
s, _ := New(&Config{})
ctx, err := s.authenticate(&resp, req)
if err != nil {
t.Fatalf("err: %v", err)
}
if ctx.Method != NoAuth {
t.Fatal("Invalid Context Method")
}
out := resp.Bytes()
if !bytes.Equal(out, []byte{socks5Version, NoAuth}) {
t.Fatalf("bad: %v", out)
}
}
func TestPasswordAuth_Valid(t *testing.T) {
req := bytes.NewBuffer(nil)
req.Write([]byte{2, NoAuth, UserPassAuth})
req.Write([]byte{1, 3, 'f', 'o', 'o', 3, 'b', 'a', 'r'})
var resp bytes.Buffer
cred := StaticCredentials{
"foo": "bar",
}
cator := UserPassAuthenticator{Credentials: cred}
s, _ := New(&Config{AuthMethods: []Authenticator{cator}})
ctx, err := s.authenticate(&resp, req)
if err != nil {
t.Fatalf("err: %v", err)
}
if ctx.Method != UserPassAuth {
t.Fatal("Invalid Context Method")
}
val, ok := ctx.Payload["Username"]
if !ok {
t.Fatal("Missing key Username in auth context's payload")
}
if val != "foo" {
t.Fatal("Invalid Username in auth context's payload")
}
out := resp.Bytes()
if !bytes.Equal(out, []byte{socks5Version, UserPassAuth, 1, authSuccess}) {
t.Fatalf("bad: %v", out)
}
}
func TestPasswordAuth_Invalid(t *testing.T) {
req := bytes.NewBuffer(nil)
req.Write([]byte{2, NoAuth, UserPassAuth})
req.Write([]byte{1, 3, 'f', 'o', 'o', 3, 'b', 'a', 'z'})
var resp bytes.Buffer
cred := StaticCredentials{
"foo": "bar",
}
cator := UserPassAuthenticator{Credentials: cred}
s, _ := New(&Config{AuthMethods: []Authenticator{cator}})
ctx, err := s.authenticate(&resp, req)
if err != UserAuthFailed {
t.Fatalf("err: %v", err)
}
if ctx != nil {
t.Fatal("Invalid Context Method")
}
out := resp.Bytes()
if !bytes.Equal(out, []byte{socks5Version, UserPassAuth, 1, authFailure}) {
t.Fatalf("bad: %v", out)
}
}
func TestNoSupportedAuth(t *testing.T) {
req := bytes.NewBuffer(nil)
req.Write([]byte{1, NoAuth})
var resp bytes.Buffer
cred := StaticCredentials{
"foo": "bar",
}
cator := UserPassAuthenticator{Credentials: cred}
s, _ := New(&Config{AuthMethods: []Authenticator{cator}})
ctx, err := s.authenticate(&resp, req)
if err != NoSupportedAuth {
t.Fatalf("err: %v", err)
}
if ctx != nil {
t.Fatal("Invalid Context Method")
}
out := resp.Bytes()
if !bytes.Equal(out, []byte{socks5Version, noAcceptable}) {
t.Fatalf("bad: %v", out)
}
}

17
socks5/credentials.go Normal file
View File

@ -0,0 +1,17 @@
package socks5
// CredentialStore is used to support user/pass authentication
type CredentialStore interface {
Valid(user, password string) bool
}
// StaticCredentials enables using a map directly as a credential store
type StaticCredentials map[string]string
func (s StaticCredentials) Valid(user, password string) bool {
pass, ok := s[user]
if !ok {
return false
}
return password == pass
}

View File

@ -0,0 +1,24 @@
package socks5
import (
"testing"
)
func TestStaticCredentials(t *testing.T) {
creds := StaticCredentials{
"foo": "bar",
"baz": "",
}
if !creds.Valid("foo", "bar") {
t.Fatalf("expect valid")
}
if !creds.Valid("baz", "") {
t.Fatalf("expect valid")
}
if creds.Valid("foo", "") {
t.Fatalf("expect invalid")
}
}

380
socks5/request.go Normal file
View File

@ -0,0 +1,380 @@
package socks5
import (
"bufio"
"context"
"fmt"
"github.com/asmogo/nws/netstr"
"io"
"net"
"strconv"
"strings"
)
const (
ConnectCommand = uint8(1)
BindCommand = uint8(2)
AssociateCommand = uint8(3)
ipv4Address = uint8(1)
fqdnAddress = uint8(3)
ipv6Address = uint8(4)
)
const (
successReply uint8 = iota
serverFailure
ruleFailure
networkUnreachable
hostUnreachable
connectionRefused
ttlExpired
commandNotSupported
addrTypeNotSupported
)
var (
unrecognizedAddrType = fmt.Errorf("Unrecognized address type")
)
// AddressRewriter is used to rewrite a destination transparently
type AddressRewriter interface {
Rewrite(ctx context.Context, request *Request) (context.Context, *AddrSpec)
}
// AddrSpec is used to return the target AddrSpec
// which may be specified as IPv4, IPv6, or a FQDN
type AddrSpec struct {
FQDN string
IP net.IP
Port int
}
func (a *AddrSpec) String() string {
if a.FQDN != "" {
return fmt.Sprintf("%s (%s):%d", a.FQDN, a.IP, a.Port)
}
return fmt.Sprintf("%s:%d", a.IP, a.Port)
}
// Address returns a string suitable to dial; prefer returning IP-based
// address, fallback to FQDN
func (a AddrSpec) Address() string {
if 0 != len(a.IP) {
return net.JoinHostPort(a.IP.String(), strconv.Itoa(a.Port))
}
return net.JoinHostPort(a.FQDN, strconv.Itoa(a.Port))
}
// A Request represents request received by a server
type Request struct {
// Protocol version
Version uint8
// Requested command
Command uint8
// AuthContext provided during negotiation
AuthContext *AuthContext
// AddrSpec of the the network that sent the request
RemoteAddr *AddrSpec
// AddrSpec of the desired destination
DestAddr *AddrSpec
// AddrSpec of the actual destination (might be affected by rewrite)
realDestAddr *AddrSpec
BufConn *bufio.Reader
}
func (r Request) Buffer(c net.Conn) {
payload, err := r.BufConn.Peek(4096)
if err != nil {
panic(err)
}
c.Write(payload)
}
/*
type conn interface {
Write([]byte) (int, error)
RemoteAddr() net.Addr
}
*/
// NewRequest creates a new Request from the tcp connection
func NewRequest(bufConn io.Reader) (*Request, error) {
// Read the version byte
header := []byte{0, 0, 0}
if _, err := io.ReadAtLeast(bufConn, header, 3); err != nil {
return nil, fmt.Errorf("Failed to get command version: %v", err)
}
// Ensure we are compatible
if header[0] != socks5Version {
return nil, fmt.Errorf("Unsupported command version: %v", header[0])
}
// Read in the destination address
dest, err := readAddrSpec(bufConn)
if err != nil {
return nil, err
}
request := &Request{
Version: socks5Version,
Command: header[1],
DestAddr: dest,
BufConn: bufConn.(*bufio.Reader),
}
return request, nil
}
// handleRequest is used for request processing after authentication
func (s *Server) handleRequest(req *Request, conn net.Conn) error {
ctx := context.Background()
// Resolve the address if we have a FQDN
dest := req.DestAddr
if dest.FQDN != "" {
ctx_, addr, err := s.config.Resolver.Resolve(ctx, dest.FQDN)
if err != nil {
if err := SendReply(conn, hostUnreachable, nil); err != nil {
return fmt.Errorf("Failed to send reply: %v", err)
}
return fmt.Errorf("Failed to resolve destination '%v': %v", dest.FQDN, err)
}
ctx = ctx_
dest.IP = addr
}
// Apply any address rewrites
req.realDestAddr = req.DestAddr
if s.config.Rewriter != nil {
ctx, req.realDestAddr = s.config.Rewriter.Rewrite(ctx, req)
}
// Switch on the command
switch req.Command {
case ConnectCommand:
return s.handleConnect(ctx, conn, req)
case BindCommand:
return s.handleBind(ctx, conn, req)
case AssociateCommand:
return s.handleAssociate(ctx, conn, req)
default:
if err := SendReply(conn, commandNotSupported, nil); err != nil {
return fmt.Errorf("failed to send reply: %v", err)
}
return fmt.Errorf("unsupported command: %d", req.Command)
}
}
// handleConnect is used to handle a connect command
func (s *Server) handleConnect(ctx context.Context, conn net.Conn, req *Request) error {
// Check if this is allowed
if ctx_, ok := s.config.Rules.Allow(ctx, req); !ok {
if err := SendReply(conn, ruleFailure, nil); err != nil {
return fmt.Errorf("Failed to send reply: %v", err)
}
return fmt.Errorf("Connect to %v blocked by rules", req.DestAddr)
} else {
ctx = ctx_
}
// Attempt to connect
dial := s.config.Dial
if dial == nil {
dial = netstr.DialSocks(s.pool)
}
target, err := dial(ctx, "tcp", req.realDestAddr.FQDN)
if err != nil {
msg := err.Error()
resp := hostUnreachable
if strings.Contains(msg, "refused") {
resp = connectionRefused
} else if strings.Contains(msg, "network is unreachable") {
resp = networkUnreachable
}
if err := SendReply(conn, resp, nil); err != nil {
return fmt.Errorf("failed to send reply: %v", err)
}
return fmt.Errorf("connect to %v failed: %v", req.DestAddr, err)
}
defer target.Close()
// Send success
local := target.LocalAddr().(*net.TCPAddr)
bind := AddrSpec{IP: local.IP, Port: local.Port}
if err := SendReply(conn, successReply, &bind); err != nil {
return fmt.Errorf("failed to send reply: %v", err)
}
// Start proxying
errCh := make(chan error, 2)
go Proxy(target, conn, errCh)
go Proxy(conn, target, errCh)
// Wait
for i := 0; i < 2; i++ {
e := <-errCh
if e != nil {
// return from this function closes target (and conn).
return e
}
}
return nil
}
// handleBind is used to handle a connect command
func (s *Server) handleBind(ctx context.Context, conn net.Conn, req *Request) error {
// Check if this is allowed
if ctx_, ok := s.config.Rules.Allow(ctx, req); !ok {
if err := SendReply(conn, ruleFailure, nil); err != nil {
return fmt.Errorf("Failed to send reply: %v", err)
}
return fmt.Errorf("Bind to %v blocked by rules", req.DestAddr)
} else {
ctx = ctx_
}
// TODO: Support bind
if err := SendReply(conn, commandNotSupported, nil); err != nil {
return fmt.Errorf("Failed to send reply: %v", err)
}
return nil
}
// handleAssociate is used to handle a connect command
func (s *Server) handleAssociate(ctx context.Context, conn net.Conn, req *Request) error {
// Check if this is allowed
if ctx_, ok := s.config.Rules.Allow(ctx, req); !ok {
if err := SendReply(conn, ruleFailure, nil); err != nil {
return fmt.Errorf("Failed to send reply: %v", err)
}
return fmt.Errorf("Associate to %v blocked by rules", req.DestAddr)
} else {
ctx = ctx_
}
// TODO: Support associate
if err := SendReply(conn, commandNotSupported, nil); err != nil {
return fmt.Errorf("Failed to send reply: %v", err)
}
return nil
}
// readAddrSpec is used to read AddrSpec.
// Expects an address type byte, follwed by the address and port
func readAddrSpec(r io.Reader) (*AddrSpec, error) {
d := &AddrSpec{}
// Get the address type
addrType := []byte{0}
if _, err := r.Read(addrType); err != nil {
return nil, err
}
// Handle on a per type basis
switch addrType[0] {
case ipv4Address:
addr := make([]byte, 4)
if _, err := io.ReadAtLeast(r, addr, len(addr)); err != nil {
return nil, err
}
d.IP = net.IP(addr)
case ipv6Address:
addr := make([]byte, 16)
if _, err := io.ReadAtLeast(r, addr, len(addr)); err != nil {
return nil, err
}
d.IP = net.IP(addr)
case fqdnAddress:
if _, err := r.Read(addrType); err != nil {
return nil, err
}
addrLen := int(addrType[0])
fqdn := make([]byte, addrLen)
if _, err := io.ReadAtLeast(r, fqdn, addrLen); err != nil {
return nil, err
}
d.FQDN = string(fqdn)
default:
return nil, unrecognizedAddrType
}
// Read the port
port := []byte{0, 0}
if _, err := io.ReadAtLeast(r, port, 2); err != nil {
return nil, err
}
d.Port = (int(port[0]) << 8) | int(port[1])
return d, nil
}
// SendReply is used to send a reply message
func SendReply(w io.Writer, resp uint8, addr *AddrSpec) error {
// Format the address
var addrType uint8
var addrBody []byte
var addrPort uint16
switch {
case addr == nil:
addrType = ipv4Address
addrBody = []byte{0, 0, 0, 0}
addrPort = 0
case addr.FQDN != "":
addrType = fqdnAddress
addrBody = append([]byte{byte(len(addr.FQDN))}, addr.FQDN...)
addrPort = uint16(addr.Port)
case addr.IP.To4() != nil:
addrType = ipv4Address
addrBody = []byte(addr.IP.To4())
addrPort = uint16(addr.Port)
case addr.IP.To16() != nil:
addrType = ipv6Address
addrBody = []byte(addr.IP.To16())
addrPort = uint16(addr.Port)
default:
return fmt.Errorf("Failed to format address: %v", addr)
}
// Format the message
msg := make([]byte, 6+len(addrBody))
msg[0] = socks5Version
msg[1] = resp
msg[2] = 0 // Reserved
msg[3] = addrType
copy(msg[4:], addrBody)
msg[4+len(addrBody)] = byte(addrPort >> 8)
msg[4+len(addrBody)+1] = byte(addrPort & 0xff)
// Send the message
_, err := w.Write(msg)
return err
}
type closeWriter interface {
CloseWrite() error
}
// Proxy is used to shuffle data from src to destination, and sends errors
// down a dedicated channel
func Proxy(dst io.Writer, src io.Reader, errCh chan error) {
_, err := io.Copy(dst, src)
if tcpConn, ok := dst.(closeWriter); ok {
tcpConn.CloseWrite()
}
if conn, ok := dst.(io.Closer); ok {
conn.Close()
}
if conn, ok := src.(io.Closer); ok {
conn.Close()
}
if errCh != nil {
errCh <- err
}
}

169
socks5/request_test.go Normal file
View File

@ -0,0 +1,169 @@
package socks5
import (
"bytes"
"encoding/binary"
"io"
"log"
"net"
"os"
"strings"
"testing"
)
type MockConn struct {
buf bytes.Buffer
}
func (m *MockConn) Write(b []byte) (int, error) {
return m.buf.Write(b)
}
func (m *MockConn) RemoteAddr() net.Addr {
return &net.TCPAddr{IP: []byte{127, 0, 0, 1}, Port: 65432}
}
func TestRequest_Connect(t *testing.T) {
// Create a local listener
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("err: %v", err)
}
go func() {
conn, err := l.Accept()
if err != nil {
t.Fatalf("err: %v", err)
}
defer conn.Close()
buf := make([]byte, 4)
if _, err := io.ReadAtLeast(conn, buf, 4); err != nil {
t.Fatalf("err: %v", err)
}
if !bytes.Equal(buf, []byte("ping")) {
t.Fatalf("bad: %v", buf)
}
conn.Write([]byte("pong"))
}()
lAddr := l.Addr().(*net.TCPAddr)
// Make server
s := &Server{config: &Config{
Rules: PermitAll(),
Resolver: DNSResolver{},
Logger: log.New(os.Stdout, "", log.LstdFlags),
}}
// Create the connect request
buf := bytes.NewBuffer(nil)
buf.Write([]byte{5, 1, 0, 1, 127, 0, 0, 1})
port := []byte{0, 0}
binary.BigEndian.PutUint16(port, uint16(lAddr.Port))
buf.Write(port)
// Send a ping
buf.Write([]byte("ping"))
// Handle the request
resp := &MockConn{}
req, err := NewRequest(buf)
if err != nil {
t.Fatalf("err: %v", err)
}
if err := s.handleRequest(req, resp); err != nil {
t.Fatalf("err: %v", err)
}
// Verify response
out := resp.buf.Bytes()
expected := []byte{
5,
0,
0,
1,
127, 0, 0, 1,
0, 0,
'p', 'o', 'n', 'g',
}
// Ignore the port for both
out[8] = 0
out[9] = 0
if !bytes.Equal(out, expected) {
t.Fatalf("bad: %v %v", out, expected)
}
}
func TestRequest_Connect_RuleFail(t *testing.T) {
// Create a local listener
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("err: %v", err)
}
go func() {
conn, err := l.Accept()
if err != nil {
t.Fatalf("err: %v", err)
}
defer conn.Close()
buf := make([]byte, 4)
if _, err := io.ReadAtLeast(conn, buf, 4); err != nil {
t.Fatalf("err: %v", err)
}
if !bytes.Equal(buf, []byte("ping")) {
t.Fatalf("bad: %v", buf)
}
conn.Write([]byte("pong"))
}()
lAddr := l.Addr().(*net.TCPAddr)
// Make server
s := &Server{config: &Config{
Rules: PermitNone(),
Resolver: DNSResolver{},
Logger: log.New(os.Stdout, "", log.LstdFlags),
}}
// Create the connect request
buf := bytes.NewBuffer(nil)
buf.Write([]byte{5, 1, 0, 1, 127, 0, 0, 1})
port := []byte{0, 0}
binary.BigEndian.PutUint16(port, uint16(lAddr.Port))
buf.Write(port)
// Send a ping
buf.Write([]byte("ping"))
// Handle the request
resp := &MockConn{}
req, err := NewRequest(buf)
if err != nil {
t.Fatalf("err: %v", err)
}
if err := s.handleRequest(req, resp); !strings.Contains(err.Error(), "blocked by rules") {
t.Fatalf("err: %v", err)
}
// Verify response
out := resp.buf.Bytes()
expected := []byte{
5,
2,
0,
1,
0, 0, 0, 0,
0, 0,
}
if !bytes.Equal(out, expected) {
t.Fatalf("bad: %v %v", out, expected)
}
}

23
socks5/resolver.go Normal file
View File

@ -0,0 +1,23 @@
package socks5
import (
"net"
"context"
)
// NameResolver is used to implement custom name resolution
type NameResolver interface {
Resolve(ctx context.Context, name string) (context.Context, net.IP, error)
}
// DNSResolver uses the system DNS to resolve host names
type DNSResolver struct{}
func (d DNSResolver) Resolve(ctx context.Context, name string) (context.Context, net.IP, error) {
addr, err := net.ResolveIPAddr("ip", name)
if err != nil {
return ctx, nil, err
}
return ctx, addr.IP, err
}

21
socks5/resolver_test.go Normal file
View File

@ -0,0 +1,21 @@
package socks5
import (
"testing"
"context"
)
func TestDNSResolver(t *testing.T) {
d := DNSResolver{}
ctx := context.Background()
_, addr, err := d.Resolve(ctx, "localhost")
if err != nil {
t.Fatalf("err: %v", err)
}
if !addr.IsLoopback() {
t.Fatalf("expected loopback")
}
}

41
socks5/ruleset.go Normal file
View File

@ -0,0 +1,41 @@
package socks5
import (
"context"
)
// RuleSet is used to provide custom rules to allow or prohibit actions
type RuleSet interface {
Allow(ctx context.Context, req *Request) (context.Context, bool)
}
// PermitAll returns a RuleSet which allows all types of connections
func PermitAll() RuleSet {
return &PermitCommand{true, true, true}
}
// PermitNone returns a RuleSet which disallows all types of connections
func PermitNone() RuleSet {
return &PermitCommand{false, false, false}
}
// PermitCommand is an implementation of the RuleSet which
// enables filtering supported commands
type PermitCommand struct {
EnableConnect bool
EnableBind bool
EnableAssociate bool
}
func (p *PermitCommand) Allow(ctx context.Context, req *Request) (context.Context, bool) {
switch req.Command {
case ConnectCommand:
return ctx, p.EnableConnect
case BindCommand:
return ctx, p.EnableBind
case AssociateCommand:
return ctx, p.EnableAssociate
}
return ctx, false
}

24
socks5/ruleset_test.go Normal file
View File

@ -0,0 +1,24 @@
package socks5
import (
"testing"
"context"
)
func TestPermitCommand(t *testing.T) {
ctx := context.Background()
r := &PermitCommand{true, false, false}
if _, ok := r.Allow(ctx, &Request{Command: ConnectCommand}); !ok {
t.Fatalf("expect connect")
}
if _, ok := r.Allow(ctx, &Request{Command: BindCommand}); ok {
t.Fatalf("do not expect bind")
}
if _, ok := r.Allow(ctx, &Request{Command: AssociateCommand}); ok {
t.Fatalf("do not expect associate")
}
}

199
socks5/socks5.go Normal file
View File

@ -0,0 +1,199 @@
package socks5
import (
"bufio"
"fmt"
"github.com/nbd-wtf/go-nostr"
"log"
"net"
"os"
"context"
)
const (
socks5Version = uint8(5)
)
// Config is used to setup and configure a Server
type Config struct {
// AuthMethods can be provided to implement custom authentication
// By default, "auth-less" mode is enabled.
// For password-based auth use UserPassAuthenticator.
AuthMethods []Authenticator
// If provided, username/password authentication is enabled,
// by appending a UserPassAuthenticator to AuthMethods. If not provided,
// and AUthMethods is nil, then "auth-less" mode is enabled.
Credentials CredentialStore
// Resolver can be provided to do custom name resolution.
// Defaults to DNSResolver if not provided.
Resolver NameResolver
// Rules is provided to enable custom logic around permitting
// various commands. If not provided, PermitAll is used.
Rules RuleSet
// Rewriter can be used to transparently rewrite addresses.
// This is invoked before the RuleSet is invoked.
// Defaults to NoRewrite.
Rewriter AddressRewriter
// BindIP is used for bind or udp associate
BindIP net.IP
// Logger can be used to provide a custom log target.
// Defaults to stdout.
Logger *log.Logger
// Optional function for dialing out
Dial func(ctx context.Context, network, addr string) (net.Conn, error)
}
var ErrorNoServerAvailable = fmt.Errorf("no socks server available")
// Server is reponsible for accepting connections and handling
// the details of the SOCKS5 protocol
type Server struct {
config *Config
authMethods map[uint8]Authenticator
pool *nostr.SimplePool
}
// New creates a new Server and potentially returns an error
func New(conf *Config, pool *nostr.SimplePool) (*Server, error) {
// Ensure we have at least one authentication method enabled
if len(conf.AuthMethods) == 0 {
if conf.Credentials != nil {
conf.AuthMethods = []Authenticator{&UserPassAuthenticator{conf.Credentials}}
} else {
conf.AuthMethods = []Authenticator{&NoAuthAuthenticator{}}
}
}
// Ensure we have a DNS resolver
if conf.Resolver == nil {
conf.Resolver = DNSResolver{}
}
// Ensure we have a rule set
if conf.Rules == nil {
conf.Rules = PermitAll()
}
// Ensure we have a log target
if conf.Logger == nil {
conf.Logger = log.New(os.Stdout, "", log.LstdFlags)
}
server := &Server{
config: conf,
pool: pool,
}
server.authMethods = make(map[uint8]Authenticator)
for _, a := range conf.AuthMethods {
server.authMethods[a.GetCode()] = a
}
return server, nil
}
func (s *Server) Configuration() (*Config, error) {
if s.config != nil {
return s.config, nil
}
return nil, fmt.Errorf("socks: configuration not set yet")
}
// ListenAndServe is used to create a listener and serve on it
func (s *Server) ListenAndServe(network, port string) error {
bind := net.JoinHostPort(s.config.BindIP.String(), port)
l, err := net.Listen(network, bind)
if err != nil {
return err
}
return s.Serve(l)
}
// Serve is used to serve connections from a listener
func (s *Server) Serve(l net.Listener) error {
for {
conn, err := l.Accept()
if err != nil {
return err
}
go s.ServeConn(conn)
}
return nil
}
// GetAuthContext is used to retrieve the auth context from connection
func (s *Server) GetAuthContext(conn net.Conn, bufConn *bufio.Reader) (*AuthContext, error) {
// Read the version byte
version := []byte{0}
if _, err := bufConn.Read(version); err != nil {
s.config.Logger.Printf("[ERR] socks: Failed to get version byte: %v", err)
return nil, err
}
// Ensure we are compatible
if version[0] != socks5Version {
err := fmt.Errorf("Unsupported SOCKS version: %v", version)
s.config.Logger.Printf("[ERR] socks: %v", err)
return nil, err
}
// Authenticate the connection
authContext, err := s.authenticate(conn, bufConn)
if err != nil {
err = fmt.Errorf("Failed to authenticate: %v", err)
s.config.Logger.Printf("[ERR] socks: %v", err)
return nil, err
}
return authContext, nil
}
// GetRequest is used to retrieve Request from connection
func (s *Server) GetRequest(conn net.Conn, bufConn *bufio.Reader) (*Request, error) {
request, err := NewRequest(bufConn)
if err != nil {
if err == unrecognizedAddrType {
if err := SendReply(conn, addrTypeNotSupported, nil); err != nil {
return nil, fmt.Errorf("Failed to send reply: %v", err)
}
}
return nil, fmt.Errorf("Failed to read destination address: %v", err)
}
return request, nil
}
// ServeConn is used to serve a single connection.
func (s *Server) ServeConn(conn net.Conn) error {
s.config.Logger.Print("[INFO] serving socks5 connection")
defer conn.Close()
bufConn := bufio.NewReader(conn)
authContext, err := s.GetAuthContext(conn, bufConn)
if err != nil {
return err
}
request, err := s.GetRequest(conn, bufConn)
if err != nil {
return err
}
request.AuthContext = authContext
if client, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
request.RemoteAddr = &AddrSpec{IP: client.IP, Port: client.Port}
}
s.config.Logger.Printf("[INFO] handling request from %s", request.RemoteAddr.IP)
// Process the client request
if err := s.handleRequest(request, conn); err != nil {
err = fmt.Errorf("failed to handle request: %v", err)
s.config.Logger.Printf("[ERR] socks: %v", err)
return err
}
return nil
}

110
socks5/socks5_test.go Normal file
View File

@ -0,0 +1,110 @@
package socks5
import (
"bytes"
"encoding/binary"
"io"
"log"
"net"
"os"
"testing"
"time"
)
func TestSOCKS5_Connect(t *testing.T) {
// Create a local listener
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("err: %v", err)
}
go func() {
conn, err := l.Accept()
if err != nil {
t.Fatalf("err: %v", err)
}
defer conn.Close()
buf := make([]byte, 4)
if _, err := io.ReadAtLeast(conn, buf, 4); err != nil {
t.Fatalf("err: %v", err)
}
if !bytes.Equal(buf, []byte("ping")) {
t.Fatalf("bad: %v", buf)
}
conn.Write([]byte("pong"))
}()
lAddr := l.Addr().(*net.TCPAddr)
// Create a socks server
creds := StaticCredentials{
"foo": "bar",
}
cator := UserPassAuthenticator{Credentials: creds}
conf := &Config{
AuthMethods: []Authenticator{cator},
Logger: log.New(os.Stdout, "", log.LstdFlags),
}
serv, err := New(conf)
if err != nil {
t.Fatalf("err: %v", err)
}
// Start listening
go func() {
if err := serv.ListenAndServe("tcp", "127.0.0.1:12365"); err != nil {
t.Fatalf("err: %v", err)
}
}()
time.Sleep(10 * time.Millisecond)
// Get a local conn
conn, err := net.Dial("tcp", "127.0.0.1:12365")
if err != nil {
t.Fatalf("err: %v", err)
}
// Connect, auth and connec to local
req := bytes.NewBuffer(nil)
req.Write([]byte{5})
req.Write([]byte{2, NoAuth, UserPassAuth})
req.Write([]byte{1, 3, 'f', 'o', 'o', 3, 'b', 'a', 'r'})
req.Write([]byte{5, 1, 0, 1, 127, 0, 0, 1})
port := []byte{0, 0}
binary.BigEndian.PutUint16(port, uint16(lAddr.Port))
req.Write(port)
// Send a ping
req.Write([]byte("ping"))
// Send all the bytes
conn.Write(req.Bytes())
// Verify response
expected := []byte{
socks5Version, UserPassAuth,
1, authSuccess,
5,
0,
0,
1,
127, 0, 0, 1,
0, 0,
'p', 'o', 'n', 'g',
}
out := make([]byte, len(expected))
conn.SetDeadline(time.Now().Add(time.Second))
if _, err := io.ReadAtLeast(conn, out, len(out)); err != nil {
t.Fatalf("err: %v", err)
}
// Ignore the port
out[12] = 0
out[13] = 0
if !bytes.Equal(out, expected) {
t.Fatalf("bad: %v", out)
}
}