mirror of
https://github.com/asmogo/nws.git
synced 2024-12-12 18:36:22 +00:00
Initial commit
This commit is contained in:
commit
081f678a10
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
localhost.crt
|
||||
localhost.key
|
||||
.idea
|
||||
nostr
|
21
LICENSE
Normal file
21
LICENSE
Normal 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
97
README.md
Normal 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
35
cmd/echo/echo.go
Normal 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
4
cmd/exit/.env
Normal 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
17
cmd/exit/Dockerfile
Normal 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
23
cmd/exit/exit.go
Normal 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
3
cmd/proxy/.env
Normal 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
17
cmd/proxy/Dockerfile
Normal 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
25
cmd/proxy/proxy.go
Normal 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
68
config/config.go
Normal 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
60
docker-compose.yaml
Normal 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
219
exit/exit.go
Normal 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
40
exit/mutex.go
Normal 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
36
go.mod
Normal 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
147
go.sum
Normal 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
16
netstr/address.go
Normal 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
326
netstr/conn.go
Normal 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
63
netstr/dial.go
Normal 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
13
netstr/dns.go
Normal 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
|
||||
}
|
63
protocol/message.go
Normal file
63
protocol/message.go
Normal 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
75
protocol/signer.go
Normal 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
68
proxy/proxy.go
Normal 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
20
socks5/LICENSE
Normal 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
45
socks5/README.md
Normal 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
151
socks5/auth.go
Normal 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
119
socks5/auth_test.go
Normal 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
17
socks5/credentials.go
Normal 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
|
||||
}
|
24
socks5/credentials_test.go
Normal file
24
socks5/credentials_test.go
Normal 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
380
socks5/request.go
Normal 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
169
socks5/request_test.go
Normal 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
23
socks5/resolver.go
Normal 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
21
socks5/resolver_test.go
Normal 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
41
socks5/ruleset.go
Normal 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
24
socks5/ruleset_test.go
Normal 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
199
socks5/socks5.go
Normal 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
110
socks5/socks5_test.go
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user