Merge pull request #21 from asmogo/http_reverse_proxy

Add HTTPS reverse proxy support
This commit is contained in:
asmogo 2024-07-25 20:12:55 +02:00 committed by GitHub
commit 88ce7ed92b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 571 additions and 386 deletions

9
.github/codecov.yml vendored Normal file
View File

@ -0,0 +1,9 @@
coverage:
status:
patch: off
project:
default:
target: auto
# adjust accordingly based on how flaky your tests are
# this allows a 10% drop from the previous base commit coverage
threshold: 10%

29
.github/workflows/master.yml vendored Normal file
View File

@ -0,0 +1,29 @@
name: gobuild
on:
push:
branches: [ "master" ]
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Docker Setup Buildx
uses: docker/setup-buildx-action@v2.0.0
- name: Build and push
uses: docker/build-push-action@v3
with:
platforms: linux/amd64,linux/arm64
push: true
tags: asmogo/nws-exit-node:latest

35
.github/workflows/tag.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: goreleaser
on:
push:
# run only against tags
tags:
- '*'
permissions:
contents: write
# packages: write
# issues: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- run: git fetch --force --tags
- uses: actions/setup-go@v3
with:
go-version: '>=1.21.2'
cache: true
# More assembly might be required: Docker logins, GPG, etc. It all depends
# on your needs.
- uses: goreleaser/goreleaser-action@v2
with:
# either 'goreleaser' (default) or 'goreleaser-pro'
distribution: goreleaser
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

18
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,18 @@
name: tests
on: [ push, pull_request ]
jobs:
golang:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.21
- name: Golang run tests
run: go test -coverprofile=coverage.txt -covermode=atomic -v ./...
- uses: codecov/codecov-action@v3
with:
verbose: true # optional (default = false)

View File

@ -28,25 +28,37 @@ Running NWS using Docker is recommended. For instructions on running NWS on your
### Using Docker Compose
Please navigate to the `docker-compose.yaml` file and set `NOSTR_PRIVATE_KEY` to your own private key.
Leaving it empty will generate a new private key on startup.
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.
This will start an example environment, including the entry node, exit node, and a backend service.
You can run the following commands to receive your nprofiles:
```bash
docker logs exit-https 2>&1 | awk -F'profile=' '{if ($2) print $2}' | awk '{print $1}'
```
```bash
docker logs exit 2>&1 | awk -F'profile=' '{if ($2) print $2}' | awk '{print $1}`
```
### Sending Requests to the Entry node
You can use the following command to send a request to the nprofile:
With the log information from the previous step, you can use the following command to send a request to the nprofile:
```
curl -v -x socks5h://localhost:8882 http://nprofile1qqsp98rnlp7sn4xuf7meyec48njp2qyfch0jktwvfuqx8vdqgexkg8gpz4mhxw309ahx7um5wgkhyetvv9un5wps8qcqggauk8/v1/info --insecure
curl -v -x socks5h://localhost:8882 http://"$(docker logs exit 2>&1 | awk -F'profile=' '{if ($2) print $2}' | awk '{print $1}' | tail -n 1)"/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
curl -v -x socks5h://localhost:8882 https://"$(docker logs exit-https 2>&1 | awk -F'profile=' '{if ($2) print $2}' | awk '{print $1}' | tail -n 1)"/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.
@ -57,7 +69,7 @@ The exit node must be set up to make the services reachable via Nostr.
### Configuration
Configuration can be completed using environment variables.
Configuration should 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'
@ -82,7 +94,7 @@ If your backend services support TLS, your service can now start using TLS encry
To run an entry node for accessing NWS services behind exit nodes, use the following command:
```
go run cmd/proxy/proxy.go
go run cmd/entry/main.go
```
#### Entry node Configuration
@ -91,7 +103,7 @@ 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'
NOSTR_RELAYS = 'ws://localhost:6666;wss://relay.com'
```
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.

View File

@ -1,35 +0,0 @@
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)
}
}

3
cmd/entry/.env Normal file
View File

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

View File

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

View File

@ -3,21 +3,42 @@ package main
import (
"github.com/asmogo/nws/config"
"github.com/asmogo/nws/exit"
"github.com/spf13/cobra"
)
"golang.org/x/net/context"
var httpsPort int32
var httpTarget string
const (
usagePort = "set the https reverse proxy port"
usageTarget = "set https reverse proxy target (your local service)"
)
func main() {
rootCmd := &cobra.Command{Use: "exit", Run: startExitNode}
rootCmd.Flags().Int32VarP(&httpsPort, "port", "p", 0, usagePort)
rootCmd.Flags().StringVarP(&httpTarget, "target", "t", "", usageTarget)
err := rootCmd.Execute()
if err != nil {
panic(err)
}
}
// updateConfigFlag updates the configuration with the provided flags.
func updateConfigFlag(cfg *config.ExitConfig) {
cfg.HttpsPort = httpsPort
cfg.HttpsTarget = httpTarget
}
func startExitNode(cmd *cobra.Command, args []string) {
// 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()
updateConfigFlag(cfg)
ctx := cmd.Context()
exitNode := exit.NewExit(ctx, cfg)
exitNode.ListenAndServe(ctx)
}

View File

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

View File

@ -18,6 +18,8 @@ type ExitConfig struct {
NostrPrivateKey string `env:"NOSTR_PRIVATE_KEY"`
BackendHost string `env:"BACKEND_HOST"`
BackendScheme string `env:"BACKEND_SCHEME"`
HttpsPort int32
HttpsTarget string
}
// load the and marshal Configuration from .env file from the UserHomeDir

1
coverage.txt Normal file
View File

@ -0,0 +1 @@
mode: atomic

View File

@ -32,20 +32,31 @@ services:
nostr:
environment:
- NOSTR_RELAYS=ws://nostr-relay:8080
- NOSTR_PRIVATE_KEY=003632642b6df1bb7f150c25aae079d590e6cfcceca924304154fbc2a3a938e3
- NOSTR_PRIVATE_KEY=
- BACKEND_HOST=mint:3338
proxy:
exit-https:
build:
context: .
dockerfile: cmd/proxy/Dockerfile
container_name: proxy
dockerfile: cmd/exit/Dockerfile
container_name: exit-https
command: ["./exit", "--port", "4443", "--target", "http://mint:3338"]
networks:
nostr:
environment:
- NOSTR_RELAYS=ws://nostr-relay:8080
- NOSTR_PRIVATE_KEY=
- BACKEND_HOST=:4443
entry:
build:
context: .
dockerfile: cmd/entry/Dockerfile
container_name: entry
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
@ -57,4 +68,4 @@ services:
volumes:
- ./nostr/data:/usr/src/app/db:Z
- ./nostr/config/config.toml:/usr/src/app/config.toml:ro,Z
user: 100:100
user: 100:100

View File

@ -12,11 +12,13 @@ import (
"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"
)
const (
startingReverseProxyMessage = "starting exit node with https reverse proxy"
generateKeyMessage = "Generated new private key. Please set your environment using the new key, otherwise your key will be lost."
)
// Exit represents a structure that holds information related to an exit node.
@ -41,24 +43,56 @@ type Exit struct {
// incomingChannel represents a channel used to receive incoming events from relays.
incomingChannel chan nostr.IncomingEvent
nprofile string
publicKey string
}
// 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))
}()
// This function will currently panic if there is an error while creating the Exit node.
func NewExit(ctx context.Context, exitNodeConfig *config.ExitConfig) *Exit {
// generate new private key if it is not set
if exitNodeConfig.NostrPrivateKey == "" {
// generate new private key
exitNodeConfig.NostrPrivateKey = nostr.GeneratePrivateKey()
slog.Warn(generateKeyMessage, "key", exitNodeConfig.NostrPrivateKey)
}
// get public key from private key
pubKey, err := nostr.GetPublicKey(exitNodeConfig.NostrPrivateKey)
if err != nil {
panic(err)
}
// encode profile
profile, err := nip19.EncodeProfile(pubKey,
exitNodeConfig.NostrRelays)
if err != nil {
panic(err)
}
// create a new pool
pool := nostr.NewSimplePool(ctx)
exit := &Exit{
nostrConnectionMap: xsync.NewMapOf[string, *netstr.NostrConnection](),
config: config,
pool: pool,
mutexMap: NewMutexMap(),
publicKey: pubKey,
nprofile: profile,
}
for _, relayUrl := range config.NostrRelays {
// start reverse proxy if https port is set
if exitNodeConfig.HttpsPort != 0 {
exitNodeConfig.BackendHost = fmt.Sprintf(":%d", exitNodeConfig.HttpsPort)
go func(cfg *config.ExitConfig) {
slog.Info(startingReverseProxyMessage, "port", cfg.HttpsPort)
err := exit.StartReverseProxy(cfg.HttpsTarget, cfg.HttpsPort)
if err != nil {
panic(err)
}
}(exitNodeConfig)
}
// set config
exit.config = exitNodeConfig
// add relays to the pool
for _, relayUrl := range exitNodeConfig.NostrRelays {
relay, err := exit.pool.EnsureRelay(relayUrl)
if err != nil {
fmt.Println(err)
@ -67,16 +101,9 @@ func NewExit(ctx context.Context, config *config.ExitConfig) *Exit {
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)
// setup subscriptions
err = exit.setSubscriptions(ctx)
if err != nil {
panic(err)

216
exit/https.go Normal file
View File

@ -0,0 +1,216 @@
package exit
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"github.com/asmogo/nws/protocol"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip04"
"log/slog"
"math/big"
"net/http"
"net/http/httputil"
"net/url"
"os"
"time"
)
func (e *Exit) DeleteEvent(ctx context.Context, ev *nostr.Event) error {
for _, responseRelay := range e.config.NostrRelays {
var relay *nostr.Relay
relay, err := e.pool.EnsureRelay(responseRelay)
if err != nil {
return err
}
event := nostr.Event{
CreatedAt: nostr.Now(),
PubKey: e.publicKey,
Kind: nostr.KindDeletion,
Tags: nostr.Tags{
nostr.Tag{"e", ev.ID},
},
}
err = event.Sign(e.config.NostrPrivateKey)
err = relay.Publish(ctx, event)
if err != nil {
return err
}
}
return nil
}
func (e *Exit) StartReverseProxy(httpTarget string, port int32) error {
ctx := context.Background()
ev := e.pool.QuerySingle(ctx, e.config.NostrRelays, nostr.Filter{
Authors: []string{e.publicKey},
Kinds: []int{nostr.KindTextNote},
Tags: nostr.TagMap{"p": []string{e.nprofile}},
})
var cert tls.Certificate
if ev == nil {
certificate, err := e.createAndStoreCertificateData(ctx)
if err != nil {
return err
}
cert = *certificate
} else {
slog.Info("found certificate event", "certificate", ev.Content)
// load private key from file
privateKeyEvent := e.pool.QuerySingle(ctx, e.config.NostrRelays, nostr.Filter{
Authors: []string{e.publicKey},
Kinds: []int{nostr.KindEncryptedDirectMessage},
Tags: nostr.TagMap{"p": []string{e.nprofile}},
})
if privateKeyEvent == nil {
return fmt.Errorf("failed to find encrypted direct message")
}
sharedKey, err := nip04.ComputeSharedSecret(privateKeyEvent.PubKey, e.config.NostrPrivateKey)
if err != nil {
return err
}
decodedMessage, err := nip04.Decrypt(privateKeyEvent.Content, sharedKey)
if err != nil {
return err
}
message, err := protocol.UnmarshalJSON([]byte(decodedMessage))
if err != nil {
return err
}
block, _ := pem.Decode(message.Data)
if block == nil {
fmt.Fprintf(os.Stderr, "error: failed to decode PEM block containing private key\n")
os.Exit(1)
}
if got, want := block.Type, "RSA PRIVATE KEY"; got != want {
fmt.Fprintf(os.Stderr, "error: decoded PEM block of type %s, but wanted %s", got, want)
os.Exit(1)
}
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return err
}
certBlock, _ := pem.Decode([]byte(ev.Content))
if certBlock == nil {
fmt.Fprintf(os.Stderr, "Failed to parse certificate PEM.")
os.Exit(1)
}
parsedCert, err := x509.ParseCertificate(certBlock.Bytes)
if err != nil {
return err
}
cert = tls.Certificate{
Certificate: [][]byte{certBlock.Bytes},
PrivateKey: priv,
Leaf: parsedCert,
}
}
target, _ := url.Parse(httpTarget)
httpsConfig := &http.Server{
Addr: fmt.Sprintf(":%d", port),
TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}},
Handler: http.HandlerFunc(httputil.NewSingleHostReverseProxy(target).ServeHTTP),
}
return httpsConfig.ListenAndServeTLS("", "")
}
func (e *Exit) createAndStoreCertificateData(ctx context.Context) (*tls.Certificate, error) {
priv, _ := rsa.GenerateKey(rand.Reader, 2048)
notBefore := time.Now()
notAfter := notBefore.Add(10 * 365 * 24 * time.Hour)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, _ := rand.Int(rand.Reader, serialNumberLimit)
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"NWS"},
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
certBytes, _ := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
// save key pem to file
err := os.WriteFile(fmt.Sprintf("%s.key", e.nprofile), keyPEM, 0644)
if err != nil {
return nil, err
}
cert, _ := tls.X509KeyPair(certPEM, keyPEM)
certificate, err := e.storeCertificate(ctx, certPEM)
if err != nil {
return certificate, err
}
err = e.storePrivateKey(ctx, keyPEM)
if err != nil {
return certificate, err
}
return &cert, nil
}
func (e *Exit) storePrivateKey(ctx context.Context, keyPEM []byte) error {
s, err := protocol.NewEventSigner(e.config.NostrPrivateKey)
if err != nil {
return err
}
event, err := s.CreateSignedEvent(e.publicKey, nostr.KindEncryptedDirectMessage, nostr.Tags{
nostr.Tag{"p", e.nprofile},
}, protocol.WithData(keyPEM))
if err != nil {
return err
}
for _, responseRelay := range e.config.NostrRelays {
var relay *nostr.Relay
relay, err = e.pool.EnsureRelay(responseRelay)
if err != nil {
return err
}
err = relay.Publish(ctx, event)
if err != nil {
return err
}
}
return nil
}
func (e *Exit) storeCertificate(ctx context.Context, certPEM []byte) (*tls.Certificate, error) {
event := nostr.Event{
CreatedAt: nostr.Now(),
PubKey: e.publicKey,
Kind: nostr.KindTextNote,
Content: string(certPEM),
Tags: nostr.Tags{
nostr.Tag{"p", e.nprofile},
},
}
err := event.Sign(e.config.NostrPrivateKey)
if err != nil {
return nil, err
}
for _, responseRelay := range e.config.NostrRelays {
var relay *nostr.Relay
relay, err = e.pool.EnsureRelay(responseRelay)
if err != nil {
return nil, err
}
err = relay.Publish(ctx, event)
if err != nil {
return nil, err
}
}
return nil, nil
}

3
go.mod
View File

@ -9,6 +9,7 @@ require (
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/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0
golang.org/x/net v0.23.0
)
@ -23,9 +24,11 @@ require (
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/inconshreveable/mousetrap v1.1.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/spf13/pflag v1.0.5 // 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

8
go.sum
View File

@ -25,6 +25,7 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtE
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/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
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=
@ -58,6 +59,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
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=
@ -83,8 +86,13 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/lo v1.45.0 h1:TPK85Y30Lv9Jh8s3TrJeA94u1hwcbFA9JObx/vT6lYU=
github.com/samber/lo v1.45.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
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=

View File

@ -171,7 +171,7 @@ func (nc *NostrConnection) handleNostrWrite(b []byte, err error) (int, error) {
protocol.WithType(protocol.MessageTypeSocks5),
protocol.WithData(b),
}
ev, err := signer.CreateSignedEvent(publicKey, nostr.Tags{nostr.Tag{"p", publicKey}}, opts...)
ev, err := signer.CreateSignedEvent(publicKey, protocol.KindEphemeralEvent, nostr.Tags{nostr.Tag{"p", publicKey}}, opts...)
if err != nil {
return 0, err
}

124
netstr/conn_test.go Normal file
View File

@ -0,0 +1,124 @@
package netstr
import (
"context"
"github.com/nbd-wtf/go-nostr"
"runtime"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func TestNostrConnection_Read(t *testing.T) {
tests := []struct {
name string
event nostr.IncomingEvent
nc func() *NostrConnection
wantN int
wantErr bool
}{
{
name: "Read invalid relay",
event: nostr.IncomingEvent{Relay: nil},
nc: func() *NostrConnection {
ctx, cancelFunc := context.WithCancel(context.Background())
return &NostrConnection{
uuid: uuid.New(),
ctx: ctx,
cancel: cancelFunc,
subscriptionChan: make(chan nostr.IncomingEvent, 1),
privateKey: "788de536151854213cc28dff9c3042e7897f0a1d59b391ddbbc1619d7e716e78",
}
},
wantN: 0,
wantErr: false,
},
{
name: "Read",
event: nostr.IncomingEvent{
Relay: &nostr.Relay{URL: "wss://relay.example.com"},
Event: &nostr.Event{
ID: "eventID",
PubKey: "8f97a664471f0b6d599a1e4a781c9a25f39902d96fb462c08df48697bb851611",
Content: "BnHzzyrUhKjDcDPOGfXJDYijUsgxw0hUZq2m+bX5QFI=?iv=NrEqv/jL+SASB2YTjo9i9Q=="}},
nc: func() *NostrConnection {
ctx, cancelFunc := context.WithCancel(context.Background())
return &NostrConnection{
uuid: uuid.New(),
ctx: ctx,
cancel: cancelFunc,
subscriptionChan: make(chan nostr.IncomingEvent, 1),
privateKey: "788de536151854213cc28dff9c3042e7897f0a1d59b391ddbbc1619d7e716e78",
}
},
wantN: 11, // hello world
wantErr: false,
},
// Add more cases here to cover more corner situations
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
nc := tt.nc()
defer nc.Close()
b := make([]byte, 1024)
nc.subscriptionChan <- tt.event
gotN, err := nc.Read(b)
if (err != nil) != tt.wantErr {
t.Errorf("Read() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotN != tt.wantN {
t.Errorf("Read() gotN = %v, want %v", gotN, tt.wantN)
}
})
}
func() {
// Prevent goroutine leak
for range make([]struct{}, 1000) {
runtime.Gosched()
}
}()
}
func TestNewConnection(t *testing.T) {
testCases := []struct {
name string
opts []NostrConnOption
expectedID string
}{
{
name: "NoOptions",
},
{
name: "WithPrivateKey",
opts: []NostrConnOption{WithPrivateKey("privateKey")},
},
{
name: "WithSub",
opts: []NostrConnOption{WithSub(true)},
},
{
name: "WithDst",
opts: []NostrConnOption{WithDst("destination")},
},
{
name: "WithUUID",
opts: []NostrConnOption{WithUUID(uuid.New())},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
connection := NewConnection(ctx, tc.opts...)
assert.NotNil(t, connection)
assert.NotNil(t, connection.pool)
assert.NotNil(t, connection.ctx)
assert.NotNil(t, connection.cancel)
assert.NotNil(t, connection.subscriptionChan)
})
}
}

View File

@ -43,7 +43,7 @@ func DialSocks(pool *nostr.SimplePool) func(ctx context.Context, net_, addr stri
protocol.WithUUID(connectionID),
protocol.WithDestination(addr),
}
ev, err := signer.CreateSignedEvent(publicKey,
ev, err := signer.CreateSignedEvent(publicKey, protocol.KindEphemeralEvent,
nostr.Tags{nostr.Tag{"p", publicKey}},
opts...)

View File

@ -34,11 +34,11 @@ func NewEventSigner(privateKey string) (*EventSigner, error) {
// 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 {
func (s *EventSigner) CreateEvent(kind int, tags nostr.Tags) nostr.Event {
return nostr.Event{
PubKey: s.PublicKey,
CreatedAt: nostr.Now(),
Kind: KindEphemeralEvent,
Kind: kind,
Tags: tags,
}
}
@ -51,7 +51,7 @@ func (s *EventSigner) CreateEvent(tags nostr.Tags) nostr.Event {
// 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) {
func (s *EventSigner) CreateSignedEvent(targetPublicKey string, kind int, tags nostr.Tags, opts ...MessageOption) (nostr.Event, error) {
sharedKey, err := nip04.ComputeSharedSecret(targetPublicKey, s.privateKey)
if err != nil {
return nostr.Event{}, err
@ -64,7 +64,7 @@ func (s *EventSigner) CreateSignedEvent(targetPublicKey string, tags nostr.Tags,
return nostr.Event{}, err
}
encryptedMessage, err := nip04.Encrypt(string(messageJson), sharedKey)
ev := s.CreateEvent(tags)
ev := s.CreateEvent(kind, tags)
ev.Content = encryptedMessage
// calling Sign sets the event ID field and the event Sig field
err = ev.Sign(s.privateKey)

View File

@ -7,10 +7,7 @@ import (
"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 {
@ -22,10 +19,6 @@ type Proxy struct {
}
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),

View File

@ -2,6 +2,7 @@ package socks5
import (
"bytes"
"github.com/nbd-wtf/go-nostr"
"testing"
)
@ -10,7 +11,7 @@ func TestNoAuth(t *testing.T) {
req.Write([]byte{1, NoAuth})
var resp bytes.Buffer
s, _ := New(&Config{})
s, _ := New(&Config{}, &nostr.SimplePool{})
ctx, err := s.authenticate(&resp, req)
if err != nil {
t.Fatalf("err: %v", err)
@ -38,7 +39,7 @@ func TestPasswordAuth_Valid(t *testing.T) {
cator := UserPassAuthenticator{Credentials: cred}
s, _ := New(&Config{AuthMethods: []Authenticator{cator}})
s, _ := New(&Config{AuthMethods: []Authenticator{cator}}, &nostr.SimplePool{})
ctx, err := s.authenticate(&resp, req)
if err != nil {
@ -74,7 +75,7 @@ func TestPasswordAuth_Invalid(t *testing.T) {
"foo": "bar",
}
cator := UserPassAuthenticator{Credentials: cred}
s, _ := New(&Config{AuthMethods: []Authenticator{cator}})
s, _ := New(&Config{AuthMethods: []Authenticator{cator}}, &nostr.SimplePool{})
ctx, err := s.authenticate(&resp, req)
if err != UserAuthFailed {
@ -101,7 +102,7 @@ func TestNoSupportedAuth(t *testing.T) {
}
cator := UserPassAuthenticator{Credentials: cred}
s, _ := New(&Config{AuthMethods: []Authenticator{cator}})
s, _ := New(&Config{AuthMethods: []Authenticator{cator}}, &nostr.SimplePool{})
ctx, err := s.authenticate(&resp, req)
if err != NoSupportedAuth {

View File

@ -1,7 +1,6 @@
package socks5
import (
"bufio"
"context"
"fmt"
"github.com/asmogo/nws/netstr"
@ -79,15 +78,6 @@ type Request struct {
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)
}
/*
@ -119,7 +109,6 @@ func NewRequest(bufConn io.Reader) (*Request, error) {
Version: socks5Version,
Command: header[1],
DestAddr: dest,
BufConn: bufConn.(*bufio.Reader),
}
return request, nil
}

View File

@ -1,169 +0,0 @@
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)
}
}

View File

@ -1,110 +0,0 @@
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)
}
}