Added public exit nodes with clearnet support

This commit is contained in:
dd dd 2024-08-01 14:35:28 +02:00
parent d2ccde45ec
commit d2be9c000e
13 changed files with 295 additions and 141 deletions

View File

@ -1,7 +1,7 @@
# Nostr Web Services (NWS) # Nostr Web Services (NWS)
NWS replaces the IP layer in TCP transport using Nostr, enabling a secure connection between NWS replaces the IP layer in TCP transport using Nostr, enabling secure connections between clients and backend services.
clients and backend services.
Exit node [domain names](#nws-domain-names) make private services accessible to entry nodes. Exit node [domain names](#nws-domain-names) make private services accessible to entry nodes.
@ -14,39 +14,38 @@ Exit node [domain names](#nws-domain-names) make private services accessible to
### NWS main components ### NWS main components
1. **Exit node**: It is a TCP reverse proxy that listens for incoming Nostr subscriptions and forwards the payload to your designated backend service. 1. **Exit node**: A TCP reverse proxy that listens for incoming Nostr subscriptions and forwards the payload to your designated backend service.
2. **Entry node**: It forwards tcp packets to the exit node using a SOCKS proxy and creates encrypted events for the exit node. 2. **Entry node**: Forwards TCP packets to the exit node using a SOCKS proxy and creates encrypted events for the exit node.
<img src="nws.png" width="900"/> <img src="nws.png" width="900"/>
### NWS domain names ### NWS domain names
There are two types of domain names resolved by NWS entry nodes: There are two types of domain names resolved by NWS entry nodes:
1. `.nostr` domains have base32 encoded public key hostnames and base32 encoded relays as subdomains. 1. `.nostr` domains, which have base32 encoded public key hostnames and base32 encoded relays as subdomains.
2. [nprofiles](https://nostr-nips.com/nip-19) are combinations of a Nostr public key and multiple relays. 2. [nprofiles](https://nostr-nips.com/nip-19), which are combinations of a Nostr public key and multiple relays.
Both types of domains will be generated and printed in the console on startup Both types of domains will be generated and printed in the console on startup
## Quickstart ## 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 to run NWS is recommended. For instructions on running NWS on your local machine, refer to the [Build from source](#build-from-source) section.
### Using Docker-Compose ### Using Docker-Compose
Please navigate to the `docker-compose.yaml` file and set `NOSTR_PRIVATE_KEY` to your own private key. Navigate to the `docker-compose.yaml` file and set `NOSTR_PRIVATE_KEY` to your private key. Leaving it empty will generate a new private key upon startup.
Leaving it empty will generate a new private key on startup.
To set up using Docker Compose, run the following command: To set up using Docker Compose, run the following command:
``` ```bash
docker compose up -d --build docker compose up -d --build
``` ```
This will start an example environment, including: This will start an example environment, including:
* Entry node - Entry node
* Exit node - Exit node
* Exit node with https reverse proxy - Exit node with HTTPS reverse proxy
* [Cashu Nutshell](https://github.com/cashubtc/nutshell) (backend service) - [Cashu Nutshell](https://github.com/cashubtc/nutshell) (backend service)
* [nostr-relay](https://github.com/scsibug/nostr-rs-relay) - [nostr-relay](https://github.com/scsibug/nostr-rs-relay)
You can run the following commands to receive your NWS domain: You can run the following commands to receive your NWS domain:
@ -55,47 +54,48 @@ docker logs exit-https 2>&1 | awk -F'domain=' '{if ($2) print $2}' | awk '{print
``` ```
```bash ```bash
docker logs exit 2>&1 | awk -F'domain=' '{if ($2) print $2}' | awk '{print $1}` docker logs exit 2>&1 | awk -F'domain=' '{if ($2) print $2}' | awk '{print $1}'
``` ```
### Sending requests to the entry node ### Sending requests to the entry node
With the log information from the previous step, you can use the following command to send a request to the exit node domain: With the log information from the previous step, you can use the following command to send a request to the exit node domain:
``` ```bash
curl -v -x socks5h://localhost:8882 http://"$(docker logs exit 2>&1 | awk -F'domain=' '{if ($2) print $2}' | awk '{print $1}' | tail -n 1)"/v1/info --insecure curl -v -x socks5h://localhost:8882 http://"$(docker logs exit 2>&1 | awk -F'domain=' '{if ($2) print $2}' | awk '{print $1}' | tail -n 1)"/v1/info --insecure
``` ```
If the exit node supports TLS, you can choose to connect using https scheme If the exit node supports TLS, you can choose to connect using the HTTPS scheme:
``` ```bash
curl -v -x socks5h://localhost:8882 https://"$(docker logs exit-https 2>&1 | awk -F'domain=' '{if ($2) print $2}' | awk '{print $1}' | tail -n 1)"/v1/info --insecure curl -v -x socks5h://localhost:8882 https://"$(docker logs exit-https 2>&1 | awk -F'domain=' '{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. When using HTTPS, the entry node can be used as a service, as the operator will not be able to see the request data.
## Build from source ## Build from Source
The exit node must be set up to make your services reachable via Nostr. To make your services reachable via Nostr, set up the exit node.
### Exit node ### Exit node
Configuration should be completed using environment variables. Configuration can be completed using environment variables. Alternatively, you can create a `.env` file in the current working directory with the following content:
Alternatively, you can create a `.env` file in the current working directory with the following content:
``` ```
NOSTR_RELAYS = 'ws://localhost:6666;wss://relay.domain.com' NOSTR_RELAYS='ws://localhost:6666;wss://relay.domain.com'
NOSTR_PRIVATE_KEY = "EXITPRIVATEHEX" NOSTR_PRIVATE_KEY="EXITPRIVATEHEX"
BACKEND_HOST = 'localhost:3338' BACKEND_HOST='localhost:3338'
PUBLIC=false
``` ```
- `NOSTR_RELAYS`: A list of nostr relays to publish events to. Will only be used if there was no relay data in the - `NOSTR_RELAYS`: A list of Nostr relays to publish events to. Used only if there is no relay data in the request.
request. - `NOSTR_PRIVATE_KEY`: The private key to sign the events.
- `NOSTR_PRIVATE_KEY`: The private key to sign the events - `BACKEND_HOST`: The host of the backend to forward requests to.
- `BACKEND_HOST`: The host of the backend to forward requests to - `PUBLIC`: If set to true, the exit node will announce itself on the Nostr network, enabling other entry nodes to discover it for public internet traffic relaying.
To start the exit node, use this command: To start the exit node, use this command:
``` ```bash
go run cmd/exit/exit.go go run cmd/exit/exit.go
``` ```
@ -106,15 +106,16 @@ If your backend services support TLS, your service can now start using TLS encry
### Entry node ### Entry node
To run an entry node for accessing NWS services behind exit nodes, use the following command: To run an entry node for accessing NWS services behind exit nodes, use the following command:
```
```bash
go run cmd/entry/main.go go run cmd/entry/main.go
``` ```
If you don't want to use the `PUBLIC_ADDRESS` feature, no further configuration is needed. If you don't want to use the `PUBLIC_ADDRESS` feature, no further configuration is needed.
``` ```
PUBLIC_ADDRESS = '<public_ip>:<port>' PUBLIC_ADDRESS='<public_ip>:<port>'
``` ```
- `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. - `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 - `NOSTR_RELAYS`: A list of Nostr relays to publish events to. Used only if there is no relay data in the request.
request.

View File

@ -2,3 +2,4 @@
NOSTR_RELAYS = 'ws://localhost:6666' NOSTR_RELAYS = 'ws://localhost:6666'
NOSTR_PRIVATE_KEY = "" NOSTR_PRIVATE_KEY = ""
BACKEND_HOST = 'localhost:3338' BACKEND_HOST = 'localhost:3338'
PUBLIC = false

View File

@ -21,6 +21,7 @@ type ExitConfig struct {
BackendScheme string `env:"BACKEND_SCHEME"` BackendScheme string `env:"BACKEND_SCHEME"`
HttpsPort int32 HttpsPort int32
HttpsTarget string HttpsTarget string
Public bool `env:"PUBLIC"`
} }
// load the and marshal Configuration from .env file from the UserHomeDir // load the and marshal Configuration from .env file from the UserHomeDir

View File

@ -117,6 +117,10 @@ func NewExit(ctx context.Context, exitNodeConfig *config.ExitConfig) *Exit {
if err != nil { if err != nil {
panic(err) panic(err)
} }
err = exit.announceExitNode(ctx)
if err != nil {
panic(err)
}
return exit return exit
} }
@ -235,6 +239,13 @@ func (e *Exit) processMessage(ctx context.Context, msg nostr.IncomingEvent) {
slog.Error("could not unmarshal message") slog.Error("could not unmarshal message")
return return
} }
destination, err := protocol.Parse(protocolMessage.Destination)
if err != nil {
return
}
if destination.TLD == "nostr" {
protocolMessage.Destination = e.config.BackendHost
}
switch protocolMessage.Type { switch protocolMessage.Type {
case protocol.MessageConnect: case protocol.MessageConnect:
e.handleConnect(ctx, msg, protocolMessage) e.handleConnect(ctx, msg, protocolMessage)
@ -268,8 +279,9 @@ func (e *Exit) handleConnect(
netstr.WithDst(receiver), netstr.WithDst(receiver),
netstr.WithUUID(protocolMessage.Key), netstr.WithUUID(protocolMessage.Key),
) )
var dst net.Conn var dst net.Conn
dst, err = net.Dial("tcp", e.config.BackendHost) dst, err = net.Dial("tcp", protocolMessage.Destination)
if err != nil { if err != nil {
slog.Error("could not connect to backend", "error", err) slog.Error("could not connect to backend", "error", err)
return return
@ -282,23 +294,34 @@ func (e *Exit) handleConnect(
} }
func (e *Exit) handleConnectReverse(protocolMessage *protocol.Message) { func (e *Exit) handleConnectReverse(protocolMessage *protocol.Message) {
e.mutexMap.Lock(protocolMessage.Key.String()) e.mutexMap.Lock(protocolMessage.Key.String())
defer e.mutexMap.Unlock(protocolMessage.Key.String()) defer e.mutexMap.Unlock(protocolMessage.Key.String())
connection, err := net.Dial("tcp", protocolMessage.Destination) connection, err := net.Dial("tcp", protocolMessage.EntryPublicAddress)
if err != nil { if err != nil {
return return
} }
var dst net.Conn
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())) _, err = connection.Write([]byte(protocolMessage.Key.String()))
if err != nil { if err != nil {
return return
} }
// read single byte from the connection
readbuffer := make([]byte, 1)
_, err = connection.Read(readbuffer)
if err != nil {
return
}
if readbuffer[0] != 1 {
return
}
var dst net.Conn
dst, err = net.Dial("tcp", protocolMessage.Destination)
if err != nil {
slog.Error("could not connect to backend", "error", err)
return
}
go socks5.Proxy(dst, connection, nil) go socks5.Proxy(dst, connection, nil)
go socks5.Proxy(connection, dst, nil) go socks5.Proxy(connection, dst, nil)
} }

View File

@ -21,30 +21,6 @@ import (
"time" "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 { func (e *Exit) StartReverseProxy(httpTarget string, port int32) error {
ctx := context.Background() ctx := context.Background()
ev := e.pool.QuerySingle(ctx, e.config.NostrRelays, nostr.Filter{ ev := e.pool.QuerySingle(ctx, e.config.NostrRelays, nostr.Filter{

59
exit/nostr.go Normal file
View File

@ -0,0 +1,59 @@
package exit
import (
"context"
"log/slog"
"github.com/nbd-wtf/go-nostr"
)
func (e *Exit) announceExitNode(ctx context.Context) error {
if !e.config.Public {
return nil
}
// create a event
event := nostr.Event{
PubKey: e.publicKey,
CreatedAt: nostr.Now(),
Kind: nostr.KindTextNote,
Content: "",
Tags: nostr.Tags{nostr.Tag{"n", "nws"}},
}
err := event.Sign(e.config.NostrPrivateKey)
if err != nil {
return err
}
// publish the event
for _, relay := range e.relays {
err = relay.Publish(ctx, event)
if err != nil {
slog.Error("could not publish event", "error", err)
// do not return here, try to publish the event to other relays
}
}
return nil
}
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
}

View File

@ -62,7 +62,9 @@ type NostrConnection struct {
sentBytes [][]byte sentBytes [][]byte
// sub represents a boolean value indicating if a connection should subscribe to a response when writing. // sub represents a boolean value indicating if a connection should subscribe to a response when writing.
sub bool sub bool
defaultRelays []string
targetPublicKey string
} }
// WriteNostrEvent writes the incoming event to the subscription channel of the NostrConnection. // WriteNostrEvent writes the incoming event to the subscription channel of the NostrConnection.
@ -149,8 +151,8 @@ func (nc *NostrConnection) handleNostrRead(b []byte, n int) (int, error) {
// Write writes data to the connection. // Write writes data to the connection.
// It delegates the writing logic to handleNostrWrite method. // It delegates the writing logic to handleNostrWrite method.
// The number of bytes written and error (if any) are returned. // The number of bytes written and error (if any) are returned.
func (nc *NostrConnection) Write(b []byte) (n int, err error) { func (nc *NostrConnection) Write(b []byte) (int, error) {
return nc.handleNostrWrite(b, err) return nc.handleNostrWrite(b)
} }
// handleNostrWrite handles the writing of a Nostr event. // handleNostrWrite handles the writing of a Nostr event.
@ -158,10 +160,9 @@ func (nc *NostrConnection) Write(b []byte) (n int, err error) {
// creates a message signer, creates message options, signs the event, // 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. // 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. // The method returns the number of bytes written and any error that occurred.
func (nc *NostrConnection) handleNostrWrite(b []byte, err error) (int, error) { func (nc *NostrConnection) handleNostrWrite(b []byte) (int, error) {
// check if we have already sent this event // check if we have already sent this event
publicKey, relays, err := nc.parseDestination()
publicKey, relays, err := ParseDestination(nc.dst)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -173,9 +174,15 @@ func (nc *NostrConnection) handleNostrWrite(b []byte, err error) (int, error) {
opts := []protocol.MessageOption{ opts := []protocol.MessageOption{
protocol.WithUUID(nc.uuid), protocol.WithUUID(nc.uuid),
protocol.WithType(protocol.MessageTypeSocks5), protocol.WithType(protocol.MessageTypeSocks5),
protocol.WithDestination(nc.dst),
protocol.WithData(b), protocol.WithData(b),
} }
ev, err := signer.CreateSignedEvent(publicKey, protocol.KindEphemeralEvent, nostr.Tags{nostr.Tag{"p", publicKey}}, opts...) ev, err := signer.CreateSignedEvent(
publicKey,
protocol.KindEphemeralEvent,
nostr.Tags{nostr.Tag{"p", publicKey}},
opts...,
)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -190,12 +197,16 @@ func (nc *NostrConnection) handleNostrWrite(b []byte, err error) (int, error) {
now := nostr.Now() now := nostr.Now()
incomingEventChannel := nc.pool.SubMany(nc.ctx, relays, incomingEventChannel := nc.pool.SubMany(nc.ctx, relays,
nostr.Filters{ nostr.Filters{
{Kinds: []int{protocol.KindEphemeralEvent}, {
Kinds: []int{protocol.KindEphemeralEvent},
Authors: []string{publicKey}, Authors: []string{publicKey},
Since: &now, Since: &now,
Tags: nostr.TagMap{ Tags: nostr.TagMap{
"p": []string{ev.PubKey}, "p": []string{ev.PubKey},
}}}) },
},
},
)
nc.subscriptionChan = incomingEventChannel nc.subscriptionChan = incomingEventChannel
} }
for _, responseRelay := range relays { for _, responseRelay := range relays {
@ -214,44 +225,58 @@ func (nc *NostrConnection) handleNostrWrite(b []byte, err error) (int, error) {
return len(b), nil return len(b), nil
} }
// ParseDestination takes a destination string and returns a public key and relays. // parseDestination takes a destination string and returns a public key and relays.
// The destination can be "npub" or "nprofile". // The destination can be "npub" or "nprofile".
// If the prefix is "npub", the public key is extracted. // If the prefix is "npub", the public key is extracted.
// If the prefix is "nprofile", the public key and relays are extracted. // If the prefix is "nprofile", the public key and relays are extracted.
// Returns the public key, relays (if any), and any error encountered. // Returns the public key, relays (if any), and any error encountered.
func ParseDestination(destination string) (string, []string, error) { func (nc *NostrConnection) parseDestination() (string, []string, error) {
// check if destination ends with .nostr // check if destination ends with .nostr
if strings.HasSuffix(destination, ".nostr") { if strings.HasPrefix(nc.dst, "npub") || strings.HasPrefix(nc.dst, "nprofile") {
return ParseDestinationDomain(destination) // destination can be npub or nprofile
} prefix, pubKey, err := nip19.Decode(nc.dst)
// destination can be npub or nprofile
prefix, pubKey, err := nip19.Decode(destination)
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
var relays []string var relays []string
var publicKey string var publicKey string
switch prefix { switch prefix {
case "npub": case "npub":
publicKey = pubKey.(string) publicKey = pubKey.(string)
case "nprofile": case "nprofile":
profilePointer := pubKey.(nostr.ProfilePointer) profilePointer := pubKey.(nostr.ProfilePointer)
publicKey = profilePointer.PublicKey publicKey = profilePointer.PublicKey
relays = profilePointer.Relays relays = profilePointer.Relays
}
return publicKey, relays, nil
} }
return publicKey, relays, nil return nc.parseDestinationDomain()
} }
func ParseDestinationDomain(destination string) (string, []string, error) { func (nc *NostrConnection) parseDestinationDomain() (string, []string, error) {
url, err := protocol.Parse(destination) url, err := protocol.Parse(nc.dst)
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
if !url.IsDomain { if !url.IsDomain {
// try to parse as ip
ip := net.ParseIP(url.Name)
if ip != nil {
return nc.targetPublicKey, nc.defaultRelays, nil
}
return "", nil, fmt.Errorf("destination is not a domain") return "", nil, fmt.Errorf("destination is not a domain")
}
if url.TLD != "nostr" {
// parse public key
/*pubKey,err := nostr.GetPublicKey(nc.privateKey)
if err != nil {
return "", nil, err
}*/
return nc.targetPublicKey, nc.defaultRelays, nil
} }
var subdomains []string var subdomains []string
split := strings.Split(url.SubName, ".") split := strings.Split(url.SubName, ".")
@ -312,6 +337,20 @@ func WithPrivateKey(privateKey string) NostrConnOption {
} }
} }
// WithPrivateKey sets the private key for the NostrConnConfig.
func WithDefaultRelays(defaultRelays []string) NostrConnOption {
return func(config *NostrConnection) {
config.defaultRelays = defaultRelays
}
}
// WithTargetPublicKey sets the private key for the NostrConnConfig.
func WithTargetPublicKey(pubKey string) NostrConnOption {
return func(config *NostrConnection) {
config.targetPublicKey = pubKey
}
}
// WithSub is a function that returns a NostrConnOption. When this option is applied // 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 // to a NostrConnConfig, it sets the 'sub' field to true, indicating that
// the connection will handle subscriptions. // the connection will handle subscriptions.

View File

@ -3,6 +3,7 @@ package netstr
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/asmogo/nws/config"
"github.com/asmogo/nws/protocol" "github.com/asmogo/nws/protocol"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
@ -11,31 +12,41 @@ import (
) )
type DialOptions struct { type DialOptions struct {
Pool *nostr.SimplePool Pool *nostr.SimplePool
PublicAddress string PublicAddress string
ConnectionID uuid.UUID ConnectionID uuid.UUID
MessageType protocol.MessageType MessageType protocol.MessageType
TargetPublicKey string
} }
// DialSocks connects to a destination using the provided SimplePool and returns a Dialer function. // 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 creates a new Connection using the specified context, private key, destination address,
// It parses the destination address to get the public key and relays. // 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 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. // 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. // Finally, it returns the Connection and nil error. If there are any errors, nil connection and the error are returned.
func DialSocks(options DialOptions) func(ctx context.Context, net_, addr string) (net.Conn, error) { func DialSocks(options DialOptions, config *config.EntryConfig) func(ctx context.Context, net_, addr string) (net.Conn, error) {
return func(ctx context.Context, net_, addr string) (net.Conn, error) { return func(ctx context.Context, net_, addr string) (net.Conn, error) {
key := nostr.GeneratePrivateKey() key := nostr.GeneratePrivateKey()
connection := NewConnection(ctx, connection := NewConnection(ctx,
WithPrivateKey(key), WithPrivateKey(key),
WithDst(addr), WithDst(addr),
WithSub(), WithSub(),
WithDefaultRelays(config.NostrRelays),
WithTargetPublicKey(options.TargetPublicKey),
WithUUID(options.ConnectionID)) WithUUID(options.ConnectionID))
publicKey, relays, err := ParseDestination(addr) var publicKey string
if err != nil { var relays []string
slog.Error("error parsing host", err) var err error
return nil, fmt.Errorf("error parsing host: %w", err) if options.TargetPublicKey != "" {
publicKey, relays = options.TargetPublicKey, config.NostrRelays
} else {
publicKey, relays, err = connection.parseDestination()
if err != nil {
slog.Error("error parsing host", err)
return nil, fmt.Errorf("error parsing host: %w", err)
}
} }
// create nostr signed event // create nostr signed event
signer, err := protocol.NewEventSigner(key) signer, err := protocol.NewEventSigner(key)
@ -47,10 +58,10 @@ func DialSocks(options DialOptions) func(ctx context.Context, net_, addr string)
protocol.WithUUID(options.ConnectionID), protocol.WithUUID(options.ConnectionID),
} }
if options.PublicAddress != "" { if options.PublicAddress != "" {
opts = append(opts, protocol.WithDestination(options.PublicAddress)) opts = append(opts, protocol.WithEntryPublicAddress(options.PublicAddress))
} else {
opts = append(opts, protocol.WithDestination(addr)) // todo -- use public key instead
} }
opts = append(opts, protocol.WithDestination(addr))
ev, err := signer.CreateSignedEvent(publicKey, protocol.KindEphemeralEvent, ev, err := signer.CreateSignedEvent(publicKey, protocol.KindEphemeralEvent,
nostr.Tags{nostr.Tag{"p", publicKey}}, nostr.Tags{nostr.Tag{"p", publicKey}},
opts...) opts...)

View File

@ -2,12 +2,33 @@ package netstr
import ( import (
"context" "context"
"fmt"
"github.com/nbd-wtf/go-nostr"
"net" "net"
"strings"
) )
// NostrDNS does not resolve anything // NostrDNS does not resolve anything
type NostrDNS struct{} type NostrDNS struct {
Pool *nostr.SimplePool
NostrRelays []string
}
func (d NostrDNS) Resolve(ctx context.Context, name string) (context.Context, net.IP, error) { func (d NostrDNS) Resolve(ctx context.Context, name string) (context.Context, net.IP, error) {
return ctx, net.IP{0, 0, 0, 0}, nil if strings.HasSuffix(name, ".nostr") || strings.HasPrefix(name, "npub") || strings.HasPrefix(name, "nprofile") {
return ctx, nil, nil
}
addr, err := net.ResolveIPAddr("ip", name)
if err != nil {
return ctx, nil, err
}
ev := d.Pool.QuerySingle(ctx, d.NostrRelays, nostr.Filter{
Tags: nostr.TagMap{"n": []string{"nws"}},
})
if ev == nil {
return ctx, nil, fmt.Errorf("failed to find exit node event")
}
ctx = context.WithValue(ctx, "publicKey", ev.PubKey)
return ctx, addr.IP, err
} }

View File

@ -14,10 +14,11 @@ var (
) )
type Message struct { type Message struct {
Key uuid.UUID `json:"key,omitempty"` Key uuid.UUID `json:"key,omitempty"`
Type MessageType `json:"type,omitempty"` Type MessageType `json:"type,omitempty"`
Data []byte `json:"data,omitempty"` Data []byte `json:"data,omitempty"`
Destination string `json:"destination,omitempty"` Destination string `json:"destination,omitempty"`
EntryPublicAddress string `json:"entryPublicAddress,omitempty"`
} }
type MessageOption func(*Message) type MessageOption func(*Message)
@ -33,11 +34,18 @@ func WithType(messageType MessageType) MessageOption {
m.Type = messageType m.Type = messageType
} }
} }
func WithDestination(destination string) MessageOption { func WithDestination(destination string) MessageOption {
return func(m *Message) { return func(m *Message) {
m.Destination = destination m.Destination = destination
} }
} }
func WithEntryPublicAddress(entryPublicAddress string) MessageOption {
return func(m *Message) {
m.EntryPublicAddress = entryPublicAddress
}
}
func WithData(data []byte) MessageOption { func WithData(data []byte) MessageOption {
return func(m *Message) { return func(m *Message) {
m.Data = data m.Data = data

View File

@ -26,12 +26,15 @@ func New(ctx context.Context, config *config.EntryConfig) *Proxy {
socksServer, err := socks5.New(&socks5.Config{ socksServer, err := socks5.New(&socks5.Config{
AuthMethods: nil, AuthMethods: nil,
Credentials: nil, Credentials: nil,
Resolver: netstr.NostrDNS{}, Resolver: netstr.NostrDNS{
Rules: nil, Pool: s.pool,
Rewriter: nil, NostrRelays: config.NostrRelays,
BindIP: net.IP{0, 0, 0, 0}, },
Logger: nil, Rules: nil,
Dial: nil, Rewriter: nil,
BindIP: net.IP{0, 0, 0, 0},
Logger: nil,
Dial: nil,
}, s.pool, config) }, s.pool, config)
if err != nil { if err != nil {
panic(err) panic(err)

View File

@ -60,6 +60,9 @@ func (a *AddrSpec) String() string {
// Address returns a string suitable to dial; prefer returning IP-based // Address returns a string suitable to dial; prefer returning IP-based
// address, fallback to FQDN // address, fallback to FQDN
func (a AddrSpec) Address() string { func (a AddrSpec) Address() string {
if a.IP == nil {
return a.FQDN
}
if 0 != len(a.IP) { if 0 != len(a.IP) {
return net.JoinHostPort(a.IP.String(), strconv.Itoa(a.Port)) return net.JoinHostPort(a.IP.String(), strconv.Itoa(a.Port))
} }
@ -121,6 +124,7 @@ func (s *Server) handleRequest(req *Request, conn net.Conn) error {
// Resolve the address if we have a FQDN // Resolve the address if we have a FQDN
dest := req.DestAddr dest := req.DestAddr
var targetPublicKey string
if dest.FQDN != "" { if dest.FQDN != "" {
ctx_, addr, err := s.config.Resolver.Resolve(ctx, dest.FQDN) ctx_, addr, err := s.config.Resolver.Resolve(ctx, dest.FQDN)
if err != nil { if err != nil {
@ -131,6 +135,9 @@ func (s *Server) handleRequest(req *Request, conn net.Conn) error {
} }
ctx = ctx_ ctx = ctx_
dest.IP = addr dest.IP = addr
if pubKey := ctx.Value("publicKey"); pubKey != nil {
targetPublicKey = pubKey.(string)
}
} }
// Apply any address rewrites // Apply any address rewrites
@ -142,7 +149,13 @@ func (s *Server) handleRequest(req *Request, conn net.Conn) error {
// Switch on the command // Switch on the command
switch req.Command { switch req.Command {
case ConnectCommand: case ConnectCommand:
return s.handleConnect(ctx, conn, req) options := netstr.DialOptions{
Pool: s.pool,
PublicAddress: s.config.entryConfig.PublicAddress,
ConnectionID: uuid.New(),
TargetPublicKey: targetPublicKey,
}
return s.handleConnect(ctx, conn, req, options)
case BindCommand: case BindCommand:
return s.handleBind(ctx, conn, req) return s.handleBind(ctx, conn, req)
case AssociateCommand: case AssociateCommand:
@ -156,7 +169,7 @@ func (s *Server) handleRequest(req *Request, conn net.Conn) error {
} }
// handleConnect is used to handle a connect command // handleConnect is used to handle a connect command
func (s *Server) handleConnect(ctx context.Context, conn net.Conn, req *Request) error { func (s *Server) handleConnect(ctx context.Context, conn net.Conn, req *Request, options netstr.DialOptions) error {
// Check if this is allowed // Check if this is allowed
if ctx_, ok := s.config.Rules.Allow(ctx, req); !ok { if ctx_, ok := s.config.Rules.Allow(ctx, req); !ok {
if err := SendReply(conn, ruleFailure, nil); err != nil { if err := SendReply(conn, ruleFailure, nil); err != nil {
@ -168,23 +181,20 @@ func (s *Server) handleConnect(ctx context.Context, conn net.Conn, req *Request)
} }
ch := make(chan net.Conn) ch := make(chan net.Conn)
// Attempt to connect // Attempt to connect
connectionID := uuid.New()
options := netstr.DialOptions{
Pool: s.pool,
PublicAddress: s.config.entryConfig.PublicAddress,
ConnectionID: connectionID,
}
dial := s.config.Dial dial := s.config.Dial
if dial == nil { if dial == nil {
if s.tcpListener != nil { if s.tcpListener != nil {
s.tcpListener.AddConnectChannel(connectionID, ch) s.tcpListener.AddConnectChannel(options.ConnectionID, ch)
options.MessageType = protocol.MessageConnectReverse options.MessageType = protocol.MessageConnectReverse
} else { } else {
options.MessageType = protocol.MessageConnect options.MessageType = protocol.MessageConnect
} }
dial = netstr.DialSocks(options)
dial = netstr.DialSocks(options, s.config.entryConfig)
} }
target, err := dial(ctx, "tcp", req.realDestAddr.FQDN)
target, err := dial(ctx, "tcp", req.realDestAddr.Address())
if err != nil { if err != nil {
msg := err.Error() msg := err.Error()
resp := hostUnreachable resp := hostUnreachable

View File

@ -49,13 +49,14 @@ func (l *TCPListener) handleConnection(conn net.Conn) {
if err != nil { if err != nil {
return return
} }
// check if uuid is in the map // check if uuid is in the map
ch, ok := l.connectChannels.Load(string(readbuffer)) ch, ok := l.connectChannels.Load(string(readbuffer))
if !ok { if !ok {
slog.Error("uuid not found in map") slog.Error("uuid not found in map")
return continue
} }
slog.Info("uuid found in map")
conn.Write([]byte{1})
// send the connection to the channel // send the connection to the channel
ch <- conn ch <- conn
return return