diff --git a/node/http.go b/node/http.go
index ac468ddd7fb8a8c4dbdafdce46d734c1cb5fece3..f1bf24a2f81da254dca3f270ac89d9407d2e39a8 100644
--- a/node/http.go
+++ b/node/http.go
@@ -63,7 +63,7 @@ func newHTTPHandler(n *Node, icecastPort int, domain string) http.Handler {
 	// there). Using command-line flags it is possible to disable
 	// the default debug pages, or to restrict access to localhost.
 	if !*disableDebugHandlers {
-		http.Handle("/debug/lbv2", n.lb.lb)
+		mux.Handle("/debug/lbv2", n.lb.lb)
 		var h http.Handler = http.DefaultServeMux
 		if *restrictDebugHandlers {
 			h = withLocalhost(h)
diff --git a/node/server_test.go b/node/server_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..3dbe603bcfc049f6a421c9641a49d43c0cbaecc8
--- /dev/null
+++ b/node/server_test.go
@@ -0,0 +1,167 @@
+package node
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"log"
+	"net"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"os"
+	"strconv"
+	"testing"
+	"time"
+
+	"go.etcd.io/etcd/clientv3/concurrency"
+	"go.etcd.io/etcd/embed"
+	"go.etcd.io/etcd/etcdserver/api/v3client"
+
+	"git.autistici.org/ale/autoradio/client"
+	pb "git.autistici.org/ale/autoradio/proto"
+)
+
+var testAudio = []byte("trust me, I'm an mp3!")
+
+func fakeAudioServer() (*httptest.Server, int) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+		w.Write(testAudio)
+	}))
+	// Extract the port from the URL.
+	port := 80
+	u, _ := url.Parse(s.URL)
+	if _, portStr, _ := net.SplitHostPort(u.Host); portStr != "" {
+		port, _ = strconv.Atoi(portStr)
+	}
+	return s, port
+}
+
+func fakeDialer(real string) *http.Transport {
+	d := new(net.Dialer)
+	return &http.Transport{
+		DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+			return d.DialContext(ctx, network, real)
+		},
+	}
+}
+
+func TestServer(t *testing.T) {
+	cfg := embed.NewConfig()
+	cfg.Dir = "default.etcd"
+	defer os.RemoveAll(cfg.Dir)
+	e, err := embed.StartEtcd(cfg)
+	if err != nil {
+		t.Fatalf("StartEtcd: %v", err)
+	}
+	defer e.Close()
+	<-e.Server.ReadyNotify()
+
+	cli := v3client.New(e.Server)
+	session, _ := concurrency.NewSession(cli, concurrency.WithTTL(2))
+	ctx, cancel := context.WithCancel(context.Background())
+
+	upstream, icecastPort := fakeAudioServer()
+	defer upstream.Close()
+
+	var nodes []*Node
+	var servers []*Server
+
+	for i := 0; i < 2; i++ {
+		conf := &Config{
+			Domain:      "localhost",
+			Nameservers: []string{"127.0.0.1"},
+			DNSPort:     4004 + i,
+			HTTPPort:    4104 + i,
+			MetricsPort: 4204 + i,
+			GossipPort:  4404 + i,
+			IcecastPort: icecastPort,
+			PeerAddr:    net.ParseIP("127.0.0.1"),
+		}
+
+		n, err := New(
+			ctx,
+			session,
+			&fakeIcecast{},
+			fmt.Sprintf("node%d", i+1),
+			[]net.IP{net.ParseIP("127.0.0.1")},
+			net.ParseIP("127.0.0.1"),
+			conf.GossipPort,
+			"random",
+			0, 0,
+		)
+		if err != nil {
+			t.Fatalf("NewNode: %v", err)
+		}
+		srv, err := NewServer(ctx, cli, n, conf)
+		if err != nil {
+			t.Fatalf("NewServer: %v", err)
+		}
+
+		nodes = append(nodes, n)
+		servers = append(servers, srv)
+	}
+
+	go func() {
+		time.Sleep(10 * time.Second)
+		log.Printf("stopping everything")
+		cancel()
+	}()
+
+	// Ping a node until it's alive.
+	ok := false
+	deadline := time.Now().Add(10 * time.Second)
+	for time.Now().Before(deadline) {
+		resp, err := http.Get("http://localhost:4104/")
+		if err == nil {
+			resp.Body.Close()
+			log.Printf("node0 healthz response status: %s", resp.Status)
+			ok = true
+			break
+		}
+		log.Printf("node0 healthz failure: %v", err)
+
+		time.Sleep(1 * time.Second)
+	}
+	if !ok {
+		t.Fatal("timed out waiting for node0 to become healthy")
+	}
+
+	// Add a stream via the client API.
+	ac := client.New(cli)
+	if err := ac.SetMount(ctx, &pb.Mount{
+		Path:           "/stream.ogg",
+		SourceUsername: "user1",
+		SourcePassword: "password1",
+	}); err != nil {
+		t.Fatalf("SetMount: %v", err)
+	}
+	log.Printf("stream /stream.ogg created successfully")
+
+	// Try to read from the stream and verify the upstream data.
+	// Force connections always to node1 (the HTTP redirect will
+	// have a DNS name, so we need to override that).
+	hc := &http.Client{
+		Transport: fakeDialer("127.0.0.1:4104"),
+	}
+	resp, err := hc.Get("http://localhost:4104/stream.ogg")
+	if err != nil {
+		t.Fatalf("Get(/stream.ogg): %v", err)
+	}
+	defer resp.Body.Close()
+	data := make([]byte, 1024)
+	n, _ := resp.Body.Read(data)
+	if !bytes.Equal(testAudio, data[:n]) {
+		t.Fatalf("bad response: %s", data[:n])
+	}
+
+	// Ok we're done, exit early.
+	cancel()
+
+	for _, s := range servers {
+		s.Wait()
+	}
+	for _, n := range nodes {
+		n.Wait()
+	}
+}