linted stuff in exit package

This commit is contained in:
dd dd 2024-09-01 21:21:23 +02:00
parent 59ff5034f9
commit a8d3de5a93
4 changed files with 133 additions and 89 deletions

View File

@ -23,7 +23,7 @@ import (
const ( const (
startingReverseProxyMessage = "starting exit node with https reverse proxy" 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." generateKeyMessage = "Generated new private key. Please set your environment using the new key, otherwise your key will be lost." //nolint: lll
) )
// Exit represents a structure that holds information related to an exit node. // Exit represents a structure that holds information related to an exit node.
@ -43,7 +43,7 @@ type Exit struct {
// It is used to establish and maintain connections between the Exit node and the backend host. // It is used to establish and maintain connections between the Exit node and the backend host.
nostrConnectionMap *xsync.MapOf[string, *netstr.NostrConnection] 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 is a field in the Exit struct used for synchronizing access to resources based on a string key.
mutexMap *MutexMap mutexMap *MutexMap
// incomingChannel represents a channel used to receive incoming events from relays. // incomingChannel represents a channel used to receive incoming events from relays.
@ -87,26 +87,25 @@ func NewExit(ctx context.Context, exitNodeConfig *config.ExitConfig) *Exit {
// start reverse proxy if https port is set // start reverse proxy if https port is set
if exitNodeConfig.HttpsPort != 0 { if exitNodeConfig.HttpsPort != 0 {
exitNodeConfig.BackendHost = fmt.Sprintf(":%d", exitNodeConfig.HttpsPort) exitNodeConfig.BackendHost = fmt.Sprintf(":%d", exitNodeConfig.HttpsPort)
go func(cfg *config.ExitConfig) { go func(ctx context.Context, cfg *config.ExitConfig) {
slog.Info(startingReverseProxyMessage, "port", cfg.HttpsPort) slog.Info(startingReverseProxyMessage, "port", cfg.HttpsPort)
err := exit.StartReverseProxy(cfg.HttpsTarget, cfg.HttpsPort) err := exit.StartReverseProxy(ctx, cfg.HttpsTarget, cfg.HttpsPort)
if err != nil { if err != nil {
panic(err) panic(err)
} }
}(exitNodeConfig) }(ctx, exitNodeConfig)
} }
// set config // set config
exit.config = exitNodeConfig exit.config = exitNodeConfig
// add relays to the pool // add relays to the pool
for _, relayUrl := range exitNodeConfig.NostrRelays { for _, relayURL := range exitNodeConfig.NostrRelays {
relay, err := exit.pool.EnsureRelay(relayUrl) relay, err := exit.pool.EnsureRelay(relayURL)
if err != nil { if err != nil {
fmt.Println(err) slog.Error("failed to ensure relay", "url", relayURL, "error", err)
continue continue
} }
exit.relays = append(exit.relays, relay) exit.relays = append(exit.relays, relay)
fmt.Printf("added relay connection to %s\n", relayUrl) slog.Info("added relay connection", "url", relayURL) //nolint:forbidigo
} }
domain, err := exit.getDomain() domain, err := exit.getDomain()
if err != nil { if err != nil {
@ -127,17 +126,18 @@ func NewExit(ctx context.Context, exitNodeConfig *config.ExitConfig) *Exit {
// getDomain returns the domain string used by the Exit node for communication with the Nostr relays. // getDomain returns the domain string used by the Exit node for communication with the Nostr relays.
// It concatenates the relay URLs using base32 encoding with no padding, separated by dots. // It concatenates the relay URLs using base32 encoding with no padding, separated by dots.
// The resulting domain is then appended with the base32 encoded public key obtained using the configured Nostr private key. // The domain is then appended with the base32 encoded public key obtained using the configured Nostr private key.
// The final domain string is converted to lowercase and returned. // The final domain string is converted to lowercase and returned.
// If any errors occur during the process, they are returned along with an // If any errors occur during the process, they are returned along with an
func (e *Exit) getDomain() (string, error) { func (e *Exit) getDomain() (string, error) {
var domain string var domain string
// first lets build the subdomains // first lets build the subdomains
for _, relayUrl := range e.config.NostrRelays { for _, relayURL := range e.config.NostrRelays {
if domain == "" { if domain == "" {
domain = base32.HexEncoding.WithPadding(base32.NoPadding).EncodeToString([]byte(relayUrl)) domain = base32.HexEncoding.WithPadding(base32.NoPadding).EncodeToString([]byte(relayURL))
} else { } else {
domain = fmt.Sprintf("%s.%s", domain, base32.HexEncoding.WithPadding(base32.NoPadding).EncodeToString([]byte(relayUrl))) domain = fmt.Sprintf("%s.%s",
domain, base32.HexEncoding.WithPadding(base32.NoPadding).EncodeToString([]byte(relayURL)))
} }
} }
// create base32 encoded public key // create base32 encoded public key
@ -173,21 +173,20 @@ func GetPublicKeyBase32(sk string) (string, error) {
// setSubscriptions sets up subscriptions for the Exit node to receive incoming events from the specified relays. // 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. // 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. // 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. // This method runs in a separate goroutine and continuously handles the incoming events by calling `processMessage`
// If the context is canceled before the subscription is established, it returns the context error. // 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. // If any errors occur during the process, they are returned.
// This method should be called once when starting the Exit node. // This method should be called once when starting the Exit node.
func (e *Exit) setSubscriptions(ctx context.Context) error { func (e *Exit) setSubscriptions(ctx context.Context) error {
pubKey, err := nostr.GetPublicKey(e.config.NostrPrivateKey) pubKey, err := nostr.GetPublicKey(e.config.NostrPrivateKey)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to get public key: %w", err)
} }
now := nostr.Now() now := nostr.Now()
if err = e.handleSubscription(ctx, pubKey, now); err != nil { if err = e.handleSubscription(ctx, pubKey, now); err != nil {
return err return fmt.Errorf("failed to handle subscription: %w", err)
} }
return nil return nil
} }
// handleSubscription handles the subscription to incoming events from relays based on the provided filters. // handleSubscription handles the subscription to incoming events from relays based on the provided filters.
@ -195,7 +194,8 @@ func (e *Exit) setSubscriptions(ctx context.Context) error {
// It returns an error if there is any issue with the subscription. // 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 { func (e *Exit) handleSubscription(ctx context.Context, pubKey string, since nostr.Timestamp) error {
incomingEventChannel := e.pool.SubMany(ctx, e.config.NostrRelays, nostr.Filters{ incomingEventChannel := e.pool.SubMany(ctx, e.config.NostrRelays, nostr.Filters{
{Kinds: []int{protocol.KindEphemeralEvent}, {
Kinds: []int{protocol.KindEphemeralEvent},
Since: &since, Since: &since,
Tags: nostr.TagMap{ Tags: nostr.TagMap{
"p": []string{pubKey}, "p": []string{pubKey},
@ -239,7 +239,7 @@ func (e *Exit) processMessage(ctx context.Context, msg nostr.IncomingEvent) {
} }
protocolMessage, err := protocol.UnmarshalJSON([]byte(decodedMessage)) protocolMessage, err := protocol.UnmarshalJSON([]byte(decodedMessage))
if err != nil { if err != nil {
slog.Error("could not unmarshal message") slog.Error("could not unmarshal message", "error", err)
return return
} }
destination, err := protocol.Parse(protocolMessage.Destination) destination, err := protocol.Parse(protocolMessage.Destination)
@ -289,7 +289,10 @@ func (e *Exit) handleConnect(
dst, err = net.Dial("tcp", protocolMessage.Destination) 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)
connection.Close() err = connection.Close()
if err != nil {
slog.Error("could not close connection", "error", err)
}
return return
} }

View File

@ -8,6 +8,7 @@ import (
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/pem" "encoding/pem"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"math/big" "math/big"
@ -22,90 +23,123 @@ import (
"github.com/nbd-wtf/go-nostr/nip04" "github.com/nbd-wtf/go-nostr/nip04"
) )
func (e *Exit) StartReverseProxy(httpTarget string, port int32) error { const (
ctx := context.Background() headerTimeout = 5 * time.Second
ev := e.pool.QuerySingle(ctx, e.config.NostrRelays, nostr.Filter{ )
var (
errNoCertificateEvent = errors.New("failed to find encrypted direct message")
)
func (e *Exit) StartReverseProxy(ctx context.Context, httpTarget string, port int32) error {
incomingEvent := e.pool.QuerySingle(ctx, e.config.NostrRelays, nostr.Filter{
Authors: []string{e.publicKey}, Authors: []string{e.publicKey},
Kinds: []int{protocol.KindCertificateEvent}, Kinds: []int{protocol.KindCertificateEvent},
Tags: nostr.TagMap{"p": []string{e.publicKey}}, Tags: nostr.TagMap{"p": []string{e.publicKey}},
}) })
var cert tls.Certificate var cert tls.Certificate
if ev == nil { var err error
if incomingEvent == nil {
certificate, err := e.createAndStoreCertificateData(ctx) certificate, err := e.createAndStoreCertificateData(ctx)
if err != nil { if err != nil {
return err return err
} }
cert = *certificate cert = *certificate
} else { } else {
slog.Info("found certificate event", "certificate", ev.Content) cert, err = e.handleCertificateEvent(incomingEvent, ctx, cert)
// load private key from file
privateKeyEvent := e.pool.QuerySingle(ctx, e.config.NostrRelays, nostr.Filter{
Authors: []string{e.publicKey},
Kinds: []int{protocol.KindPrivateKeyEvent},
Tags: nostr.TagMap{"p": []string{e.publicKey}},
})
if privateKeyEvent == nil {
return fmt.Errorf("failed to find encrypted direct message")
}
sharedKey, err := nip04.ComputeSharedSecret(privateKeyEvent.PubKey, e.config.NostrPrivateKey)
if err != nil { if err != nil {
return err 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) target, _ := url.Parse(httpTarget)
httpsConfig := &http.Server{ httpsConfig := &http.Server{
Addr: fmt.Sprintf(":%d", port), ReadHeaderTimeout: headerTimeout,
TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}}, Addr: fmt.Sprintf(":%d", port),
Handler: http.HandlerFunc(httputil.NewSingleHostReverseProxy(target).ServeHTTP), TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}},
Handler: http.HandlerFunc(httputil.NewSingleHostReverseProxy(target).ServeHTTP),
} }
return httpsConfig.ListenAndServeTLS("", "") return httpsConfig.ListenAndServeTLS("", "")
} }
func (e *Exit) handleCertificateEvent(incomingEvent *nostr.IncomingEvent, ctx context.Context, cert tls.Certificate) (tls.Certificate, error) {
slog.Info("found certificate event", "certificate", incomingEvent.Content)
// load private key from file
privateKeyEvent := e.pool.QuerySingle(ctx, e.config.NostrRelays, nostr.Filter{
Authors: []string{e.publicKey},
Kinds: []int{protocol.KindPrivateKeyEvent},
Tags: nostr.TagMap{"p": []string{e.publicKey}},
})
if privateKeyEvent == nil {
return tls.Certificate{}, errNoCertificateEvent
}
sharedKey, err := nip04.ComputeSharedSecret(privateKeyEvent.PubKey, e.config.NostrPrivateKey)
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to compute shared key: %w", err)
}
decodedMessage, err := nip04.Decrypt(privateKeyEvent.Content, sharedKey)
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to decrypt private key: %w", err)
}
message, err := protocol.UnmarshalJSON([]byte(decodedMessage))
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to unmarshal message: %w", err)
}
block, _ := pem.Decode(message.Data)
if block == nil {
_, err = fmt.Fprintf(os.Stderr, "error: failed to decode PEM block containing private key\n")
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to write error: %w", err)
}
os.Exit(1)
}
if got, want := block.Type, "RSA PRIVATE KEY"; got != want {
_, err = fmt.Fprintf(os.Stderr, "error: decoded PEM block of type %s, but wanted %s", got, want)
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to write error: %w", err)
}
os.Exit(1)
}
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to parse private key: %w", err)
}
certBlock, _ := pem.Decode([]byte(incomingEvent.Content))
if certBlock == nil {
_, err = fmt.Fprintf(os.Stderr, "Failed to parse certificate PEM.")
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to write error: %w", err)
}
os.Exit(1)
}
parsedCert, err := x509.ParseCertificate(certBlock.Bytes)
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to parse certificate: %w", err)
}
cert = tls.Certificate{
Certificate: [][]byte{certBlock.Bytes},
PrivateKey: priv,
Leaf: parsedCert,
}
return cert, nil
}
const (
tenYears = 0 * 365 * 24 * time.Hour
keySize = 2048
limit = 128
chmod = 0644
)
func (e *Exit) createAndStoreCertificateData(ctx context.Context) (*tls.Certificate, error) { func (e *Exit) createAndStoreCertificateData(ctx context.Context) (*tls.Certificate, error) {
priv, _ := rsa.GenerateKey(rand.Reader, 2048) priv, _ := rsa.GenerateKey(rand.Reader, keySize)
notBefore := time.Now() notBefore := time.Now()
notAfter := notBefore.Add(10 * 365 * 24 * time.Hour) notAfter := notBefore.Add(tenYears)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), limit)
serialNumber, _ := rand.Int(rand.Reader, serialNumberLimit) serialNumber, _ := rand.Int(rand.Reader, serialNumberLimit)
domain, _ := e.getDomain() domain, _ := e.getDomain()
@ -126,7 +160,7 @@ func (e *Exit) createAndStoreCertificateData(ctx context.Context) (*tls.Certific
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes}) certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
// save key pem to file // save key pem to file
err := os.WriteFile(fmt.Sprintf("%s.key", e.publicKey), keyPEM, 0644) err := os.WriteFile(fmt.Sprintf("%s.key", e.publicKey), keyPEM, chmod)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -1,7 +1,7 @@
package exit package exit
import ( import (
"fmt" "log/slog"
"sync" "sync"
) )
@ -33,8 +33,8 @@ func (mm *MutexMap) Unlock(id string) {
mutex, ok := mm.m[id] mutex, ok := mm.m[id]
mm.mu.Unlock() mm.mu.Unlock()
if !ok { if !ok {
panic(fmt.Sprintf("tried to unlock mutex for non-existent id %s", id)) slog.Error("mutex not found", "id", id)
return
} }
mutex.Unlock() mutex.Unlock()
} }

View File

@ -2,6 +2,8 @@ package exit
import ( import (
"context" "context"
"errors"
"fmt"
"log/slog" "log/slog"
"strconv" "strconv"
"time" "time"
@ -12,9 +14,11 @@ import (
const ten = 10 const ten = 10
var errNoPublicKey = errors.New("no public key found")
func (e *Exit) announceExitNode(ctx context.Context) error { func (e *Exit) announceExitNode(ctx context.Context) error {
if !e.config.Public { if !e.config.Public {
return nil return errNoPublicKey
} }
go func() { go func() {
for { for {
@ -45,25 +49,28 @@ func (e *Exit) announceExitNode(ctx context.Context) error {
return nil return nil
} }
func (e *Exit) DeleteEvent(ctx context.Context, ev *nostr.Event) error { func (e *Exit) DeleteEvent(ctx context.Context, event *nostr.Event) error {
for _, responseRelay := range e.config.NostrRelays { for _, responseRelay := range e.config.NostrRelays {
var relay *nostr.Relay var relay *nostr.Relay
relay, err := e.pool.EnsureRelay(responseRelay) relay, err := e.pool.EnsureRelay(responseRelay)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to ensure relay: %w", err)
} }
event := nostr.Event{ event := nostr.Event{
CreatedAt: nostr.Now(), CreatedAt: nostr.Now(),
PubKey: e.publicKey, PubKey: e.publicKey,
Kind: nostr.KindDeletion, Kind: nostr.KindDeletion,
Tags: nostr.Tags{ Tags: nostr.Tags{
nostr.Tag{"e", ev.ID}, nostr.Tag{"e", event.ID},
}, },
} }
err = event.Sign(e.config.NostrPrivateKey) err = event.Sign(e.config.NostrPrivateKey)
if err != nil {
return fmt.Errorf("failed to sign event: %w", err)
}
err = relay.Publish(ctx, event) err = relay.Publish(ctx, event)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to publish event: %w", err)
} }
} }
return nil return nil