Merge pull request #22 from asmogo/reverse_connect

Implement reverse connection handling in SOCKS5 server
This commit is contained in:
asmogo 2024-07-27 19:55:12 +02:00 committed by GitHub
commit c957f9144c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 195 additions and 41 deletions

View File

@ -36,7 +36,12 @@ To set up using Docker Compose, run the following command:
docker compose up -d --build
```
This will start an example environment, including the entry node, exit node, and a backend service.
This will start an example environment, including:
* Entry node
* Exit node
* Exit node with https reverse proxy
* [Cashu Nutshell](https://github.com/cashubtc/nutshell) (backend service)
* [nostr-relay](https://github.com/scsibug/nostr-rs-relay)
You can run the following commands to receive your nprofiles:
@ -65,9 +70,9 @@ When using https, the entry node can be used as a service, since the operator wi
## Build from source
The exit node must be set up to make the services reachable via Nostr.
The exit node must be set up to make your services reachable via Nostr.
### Configuration
### Exit node Configuration
Configuration should be completed using environment variables.
Alternatively, you can create a `.env` file in the current working directory with the following content:
@ -77,7 +82,7 @@ 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
- `NOSTR_RELAYS`: A list of nostr relays to publish events to. Will only be used if there was no relay data in the
request.
- `NOSTR_PRIVATE_KEY`: The private key to sign the events
- `BACKEND_HOST`: The host of the backend to forward requests to
@ -92,18 +97,18 @@ If your backend services support TLS, your service can now start using TLS encry
---
### Entry node Configuration
To run an entry node for accessing NWS services behind exit nodes, use the following command:
```
go run cmd/entry/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:
If you don't want to use the `PUBLIC_ADDRESS` feature, no further configuration is needed.
```
NOSTR_RELAYS = 'ws://localhost:6666;wss://relay.com'
PUBLIC_ADDRESS = '<public_ip>:<port>'
```
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.
- `PUBLIC_ADDRESS`: This can be set if the entry node is publicly available. When set, the entry node will additionally bind to this address. Exit node discovery will still be done using Nostr. Once a connection is established, this public address will be used to transmit further data.
- `NOSTR_RELAYS`: A list of nostr relays to publish events to. Will only be used if there was no relay data in the
request.

View File

@ -9,7 +9,7 @@ import (
func main() {
// load the configuration
// from the environment
cfg, err := config.LoadConfig[config.ProxyConfig]()
cfg, err := config.LoadConfig[config.EntryConfig]()
if err != nil {
panic(err)
}

View File

@ -9,8 +9,9 @@ import (
"github.com/joho/godotenv"
)
type ProxyConfig struct {
NostrRelays []string `env:"NOSTR_RELAYS" envSeparator:";"`
type EntryConfig struct {
NostrRelays []string `env:"NOSTR_RELAYS" envSeparator:";"`
PublicAddress string `env:"PUBLIC_ADDRESS"`
}
type ExitConfig struct {

View File

@ -183,6 +183,8 @@ func (e *Exit) processMessage(ctx context.Context, msg nostr.IncomingEvent) {
switch protocolMessage.Type {
case protocol.MessageConnect:
e.handleConnect(ctx, msg, protocolMessage, false)
case protocol.MessageConnectReverse:
e.handleConnectReverse(ctx, protocolMessage, false)
case protocol.MessageTypeSocks5:
e.handleSocks5ProxyMessage(msg, protocolMessage)
}
@ -226,6 +228,33 @@ func (e *Exit) handleConnect(ctx context.Context, msg nostr.IncomingEvent, proto
go socks5.Proxy(connection, dst, nil)
}
func (e *Exit) handleConnectReverse(ctx context.Context, protocolMessage *protocol.Message, isTLS bool) {
e.mutexMap.Lock(protocolMessage.Key.String())
defer e.mutexMap.Unlock(protocolMessage.Key.String())
connection, err := net.Dial("tcp", protocolMessage.Destination)
if err != nil {
return
}
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
}
_, err = connection.Write([]byte(protocolMessage.Key.String()))
if err != nil {
return
}
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.
//

5
go.mod
View File

@ -11,7 +11,7 @@ require (
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
golang.org/x/net v0.27.0
)
require (
@ -26,6 +26,8 @@ require (
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/kr/pretty v0.1.0 // indirect
github.com/kr/text v0.2.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
@ -35,5 +37,6 @@ require (
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/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

14
go.sum
View File

@ -26,6 +26,7 @@ github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46f
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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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=
@ -69,6 +70,12 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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=
@ -114,8 +121,8 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73r
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/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
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=
@ -143,8 +150,9 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
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/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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=

View File

@ -11,22 +11,28 @@ import (
"strings"
)
type DialOptions struct {
Pool *nostr.SimplePool
PublicAddress string
ConnectionID uuid.UUID
MessageType protocol.MessageType
}
// 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) {
func DialSocks(options DialOptions) 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))
WithUUID(options.ConnectionID))
publicKey, relays, err := ParseDestination(addr)
if err != nil {
@ -39,16 +45,20 @@ func DialSocks(pool *nostr.SimplePool) func(ctx context.Context, net_, addr stri
return nil, err
}
opts := []protocol.MessageOption{
protocol.WithType(protocol.MessageConnect),
protocol.WithUUID(connectionID),
protocol.WithDestination(addr),
protocol.WithType(options.MessageType),
protocol.WithUUID(options.ConnectionID),
}
if options.PublicAddress != "" {
opts = append(opts, protocol.WithDestination(options.PublicAddress))
} else {
opts = append(opts, protocol.WithDestination(addr)) // todo -- use public key instead
}
ev, err := signer.CreateSignedEvent(publicKey, protocol.KindEphemeralEvent,
nostr.Tags{nostr.Tag{"p", publicKey}},
opts...)
for _, relayUrl := range relays {
relay, err := pool.EnsureRelay(relayUrl)
relay, err := options.Pool.EnsureRelay(relayUrl)
if err != nil {
slog.Error("error creating relay", err)
continue

View File

@ -8,8 +8,9 @@ import (
type MessageType string
var (
MessageTypeSocks5 = MessageType("SOCKS5")
MessageConnect = MessageType("CONNECT")
MessageTypeSocks5 = MessageType("SOCKS5")
MessageConnect = MessageType("CONNECT")
MessageConnectReverse = MessageType("CONNECTR")
)
type Message struct {

View File

@ -11,8 +11,7 @@ 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.
// EventSigner provides methods for creating unsigned events, creating signed events
type EventSigner struct {
PublicKey string
privateKey string

View File

@ -11,14 +11,14 @@ import (
)
type Proxy struct {
config *config.ProxyConfig // the configuration for the gateway
config *config.EntryConfig // 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 {
func New(ctx context.Context, config *config.EntryConfig) *Proxy {
s := &Proxy{
config: config,
pool: nostr.NewSimplePool(ctx),
@ -32,7 +32,7 @@ func New(ctx context.Context, config *config.ProxyConfig) *Proxy {
BindIP: net.IP{0, 0, 0, 0},
Logger: nil,
Dial: nil,
}, s.pool)
}, s.pool, config)
if err != nil {
panic(err)
}

View File

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

View File

@ -4,6 +4,8 @@ import (
"context"
"fmt"
"github.com/asmogo/nws/netstr"
"github.com/asmogo/nws/protocol"
"github.com/google/uuid"
"io"
"net"
"strconv"
@ -164,11 +166,23 @@ func (s *Server) handleConnect(ctx context.Context, conn net.Conn, req *Request)
} else {
ctx = ctx_
}
ch := make(chan net.Conn)
// Attempt to connect
connectionID := uuid.New()
options := netstr.DialOptions{
Pool: s.pool,
PublicAddress: s.config.entryConfig.PublicAddress,
ConnectionID: connectionID,
}
dial := s.config.Dial
if dial == nil {
dial = netstr.DialSocks(s.pool)
if s.tcpListener != nil {
s.tcpListener.AddConnectChannel(connectionID, ch)
options.MessageType = protocol.MessageConnectReverse
} else {
options.MessageType = protocol.MessageConnect
}
dial = netstr.DialSocks(options)
}
target, err := dial(ctx, "tcp", req.realDestAddr.FQDN)
if err != nil {
@ -192,7 +206,13 @@ func (s *Server) handleConnect(ctx context.Context, conn net.Conn, req *Request)
if err := SendReply(conn, successReply, &bind); err != nil {
return fmt.Errorf("failed to send reply: %v", err)
}
// read
if options.MessageType == protocol.MessageConnectReverse {
// wait for the connection
// in this case, our target needs to be the reversed tcp connection
target = <-ch
defer target.Close()
}
// Start proxying
errCh := make(chan error, 2)
go Proxy(target, conn, errCh)

View File

@ -3,6 +3,7 @@ package socks5
import (
"bufio"
"fmt"
"github.com/asmogo/nws/config"
"github.com/nbd-wtf/go-nostr"
"log"
"net"
@ -49,6 +50,8 @@ type Config struct {
// Optional function for dialing out
Dial func(ctx context.Context, network, addr string) (net.Conn, error)
entryConfig *config.EntryConfig
}
var ErrorNoServerAvailable = fmt.Errorf("no socks server available")
@ -59,10 +62,11 @@ type Server struct {
config *Config
authMethods map[uint8]Authenticator
pool *nostr.SimplePool
tcpListener *TCPListener
}
// New creates a new Server and potentially returns an error
func New(conf *Config, pool *nostr.SimplePool) (*Server, error) {
func New(conf *Config, pool *nostr.SimplePool, config *config.EntryConfig) (*Server, error) {
// Ensure we have at least one authentication method enabled
if len(conf.AuthMethods) == 0 {
if conf.Credentials != nil {
@ -86,12 +90,22 @@ func New(conf *Config, pool *nostr.SimplePool) (*Server, error) {
if conf.Logger == nil {
conf.Logger = log.New(os.Stdout, "", log.LstdFlags)
}
if conf.entryConfig == nil {
conf.entryConfig = config
}
server := &Server{
config: conf,
pool: pool,
}
if conf.entryConfig.PublicAddress != "" {
listener, err := NewTCPListener(conf.entryConfig.PublicAddress)
if err != nil {
return nil, err
}
go listener.Start()
server.tcpListener = listener
}
server.authMethods = make(map[uint8]Authenticator)
for _, a := range conf.AuthMethods {

63
socks5/tcp.go Normal file
View File

@ -0,0 +1,63 @@
package socks5
import (
"github.com/google/uuid"
"github.com/puzpuzpuz/xsync/v3"
"log/slog"
"net"
)
type TCPListener struct {
listener net.Listener
connectChannels *xsync.MapOf[string, chan net.Conn] // todo -- use [16]byte for uuid instead of string
}
func NewTCPListener(address string) (*TCPListener, error) {
l, err := net.Listen("tcp", address)
if err != nil {
return nil, err
}
return &TCPListener{
listener: l,
connectChannels: xsync.NewMapOf[string, chan net.Conn](),
}, nil
}
func (l *TCPListener) AddConnectChannel(uuid uuid.UUID, ch chan net.Conn) {
l.connectChannels.Store(uuid.String(), ch)
}
// Start starts the listener
func (l *TCPListener) Start() {
for {
conn, err := l.listener.Accept()
if err != nil {
return
}
go l.handleConnection(conn)
}
}
// handleConnection handles the connection
func (l *TCPListener) handleConnection(conn net.Conn) {
//defer conn.Close()
for {
// read uuid from the connection
readbuffer := make([]byte, 36)
_, err := conn.Read(readbuffer)
if err != nil {
return
}
// check if uuid is in the map
ch, ok := l.connectChannels.Load(string(readbuffer))
if !ok {
slog.Error("uuid not found in map")
return
}
// send the connection to the channel
ch <- conn
return
}
}