~emersion/soju-dev

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch

[PATCH] Support upstream RPL_BOUNCE

Details
Message ID
<20220902133717.16929-1-delthas@dille.cc>
DKIM signature
pass
Download raw message
Patch: +69 -17
RPL_BOUNCE ("010") enables IRC server operators to add a load balancer
in front of multiple IRC server instances in order to redirect a client
to another server.

This is easier to use than DNS load balancing because it does not
require all servers to have a TLS certificate for the same domain.

We only process RPL_BOUNCE if received before being registered, which
should always be the case.

If the RPL_BOUNCE port field starts with a '+', we consider it to be
TLS.
---
Tested on my machine against a test server returning 010.

 upstream.go | 45 ++++++++++++++++++++++++++++++++++++++++-----
 user.go     | 41 +++++++++++++++++++++++++++++------------
 2 files changed, 69 insertions(+), 17 deletions(-)

diff --git a/upstream.go b/upstream.go
index d0b0e10..47eda60 100644
--- a/upstream.go
+++ b/upstream.go
@@ -11,6 +11,7 @@ import (
	"fmt"
	"io"
	"net"
	"net/url"
	"strconv"
	"strings"
	"time"
@@ -43,6 +44,14 @@ var permanentUpstreamCaps = map[string]bool{
	"draft/extended-monitor":     true,
}

type redirectionError struct {
	url *url.URL
}

func (err redirectionError) Error() string {
	return fmt.Sprintf("redirected to %v", err.url)
}

type registrationError struct {
	*irc.Message
}
@@ -163,7 +172,7 @@ type upstreamConn struct {
	hasDesiredNick bool
}

func connectToUpstream(ctx context.Context, network *network) (*upstreamConn, error) {
func connectToUpstream(ctx context.Context, network *network, u *url.URL) (*upstreamConn, error) {
	logger := &prefixLogger{network.user.logger, fmt.Sprintf("upstream %q: ", network.GetName())}

	ctx, cancel := context.WithTimeout(ctx, connectTimeout)
@@ -171,9 +180,12 @@ func connectToUpstream(ctx context.Context, network *network) (*upstreamConn, er

	var dialer net.Dialer

	u, err := network.URL()
	if err != nil {
		return nil, err
	if u == nil {
		var err error
		u, err = network.URL()
		if err != nil {
			return nil, err
		}
	}

	var netConn net.Conn
@@ -243,6 +255,7 @@ func connectToUpstream(ctx context.Context, network *network) (*upstreamConn, er
		}
	case "irc+unix", "unix":
		logger.Printf("connecting to Unix socket at path %q", u.Path)
		var err error
		netConn, err = dialer.DialContext(ctx, "unix", u.Path)
		if err != nil {
			return nil, fmt.Errorf("failed to connect to Unix socket %q: %v", u.Path, err)
@@ -773,6 +786,26 @@ func (uc *upstreamConn) handleMessage(ctx context.Context, msg *irc.Message) err

			dc.SendMessage(msg)
		}
	case "010": // RPL_BOUNCE
		if uc.registered {
			uc.logger.Printf("redirection received after registration")
			break
		}
		var host, port string
		if err := parseMessageParams(msg, nil, &host, &port); err != nil {
			return err
		}
		var u url.URL
		if strings.HasPrefix(port, "+") {
			port = strings.TrimPrefix(port, "+")
			u.Scheme = "ircs"
		} else {
			u.Scheme = "irc+insecure"
		}
		u.Host = net.JoinHostPort(host, port)
		return redirectionError{
			url: &u,
		}
	case irc.RPL_WELCOME:
		if err := parseMessageParams(msg, &uc.nick); err != nil {
			return err
@@ -2053,7 +2086,9 @@ func (uc *upstreamConn) runUntilRegistered(ctx context.Context) error {
		}

		if err := uc.handleMessage(ctx, msg); err != nil {
			if _, ok := err.(registrationError); ok {
			if _, ok := err.(redirectionError); ok {
				return err
			} else if _, ok := err.(registrationError); ok {
				return err
			} else {
				msg.Tags = nil // prevent message tags from cluttering logs
diff --git a/user.go b/user.go
index 6d93bc2..406e6e8 100644
--- a/user.go
+++ b/user.go
@@ -9,6 +9,7 @@ import (
	"fmt"
	"math/big"
	"net"
	"net/url"
	"sort"
	"strings"
	"time"
@@ -208,24 +209,40 @@ func (net *network) runConn(ctx context.Context) error {
	ctx, cancel := context.WithTimeout(ctx, time.Minute)
	defer cancel()

	uc, err := connectToUpstream(ctx, net)
	if err != nil {
		return fmt.Errorf("failed to connect: %w", err)
	var uc *upstreamConn
	var redirectURL *url.URL
	for {
		var err error
		uc, err = connectToUpstream(ctx, net, redirectURL)
		if err != nil {
			return fmt.Errorf("failed to connect: %w", err)
		}

		if net.user.srv.Identd != nil {
			net.user.srv.Identd.Store(uc.RemoteAddr().String(), uc.LocalAddr().String(), userIdent(&net.user.User))
		}

		// TODO: this is racy, we're not running in the user goroutine yet
		// uc.register accesses user/network DB records
		uc.register(ctx)
		if err := uc.runUntilRegistered(ctx); err != nil {
			if re, ok := err.(redirectionError); ok {
				uc.Close()
				if net.user.srv.Identd != nil {
					net.user.srv.Identd.Delete(uc.RemoteAddr().String(), uc.LocalAddr().String())
				}
				redirectURL = re.url
				continue
			}
			return fmt.Errorf("failed to register: %w", err)
		}
		break
	}
	defer uc.Close()

	if net.user.srv.Identd != nil {
		net.user.srv.Identd.Store(uc.RemoteAddr().String(), uc.LocalAddr().String(), userIdent(&net.user.User))
		defer net.user.srv.Identd.Delete(uc.RemoteAddr().String(), uc.LocalAddr().String())
	}

	// TODO: this is racy, we're not running in the user goroutine yet
	// uc.register accesses user/network DB records
	uc.register(ctx)
	if err := uc.runUntilRegistered(ctx); err != nil {
		return fmt.Errorf("failed to register: %w", err)
	}

	// TODO: this is racy with net.stopped. If the network is stopped
	// before the user goroutine receives eventUpstreamConnected, the
	// connection won't be closed.

base-commit: f4af7975d3cb2f01d26c581ad72cd30d569fdbbd
-- 
2.17.1
Reply to thread Export thread (mbox)