diff --git a/cmd/radiod/radiod.go b/cmd/radiod/radiod.go
new file mode 100644
index 0000000000000000000000000000000000000000..13d2c1cd6d441f4391b8aad6c96e22f86d6c44d3
--- /dev/null
+++ b/cmd/radiod/radiod.go
@@ -0,0 +1,34 @@
+package main
+
+import (
+	"flag"
+	"log"
+	"os"
+	"os/signal"
+	"syscall"
+
+	"git.autistici.org/ale/radioai"
+)
+
+var (
+	publicIp = flag.String("ip", "127.0.0.1", "Public IP for this machine")
+)
+
+
+func main() {
+	flag.Parse()
+
+	client := radioai.NewEtcdClient()
+	node := radioai.NewRadioNode(*publicIp, client)
+
+	// Set up a clean shutdown function on SIGTERM.
+	stopch := make(chan os.Signal)
+	go func() {
+		<- stopch
+		log.Printf("terminating...")
+		node.Stop()
+	}()
+	signal.Notify(stopch, syscall.SIGTERM, syscall.SIGINT)
+
+	node.Run()
+}
\ No newline at end of file
diff --git a/cmd/redirectord/redirectord.go b/cmd/redirectord/redirectord.go
new file mode 100644
index 0000000000000000000000000000000000000000..386eea1b6892eeb529828fbdeea6e64c758f58b4
--- /dev/null
+++ b/cmd/redirectord/redirectord.go
@@ -0,0 +1,32 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+	"net/http"
+	"time"
+
+	"git.autistici.org/ale/radioai"
+)
+
+var (
+	httpPort = flag.Int("port", 80, "TCP port to bind to")
+)
+
+func main() {
+	flag.Parse()
+
+	client := radioai.NewEtcdClient()
+	api := radioai.NewRadioAPI(client)
+	red := radioai.NewHttpRedirector(api)
+
+	server := &http.Server{
+		Addr: fmt.Sprintf(":%d", *httpPort),
+		Handler: red,
+		ReadTimeout: 10 * time.Second,
+		WriteTimeout: 10 * time.Second,
+	}
+	
+	log.Fatal(server.ListenAndServe())
+}
\ No newline at end of file
diff --git a/http.go b/http.go
index ed4504dd74ec030f7a9dda674178759df3a95ba9..9fe3a01cb3813fc0576bf4ba2a76019ed2daedcd 100644
--- a/http.go
+++ b/http.go
@@ -22,6 +22,13 @@ type activeNodesCache struct {
 
 var activeNodesTtl = 500 * time.Millisecond
 
+func newActiveNodesCache(client *RadioAPI) *activeNodesCache {
+	return &activeNodesCache{
+		client: client,
+		nodes: []string{},
+	}
+}
+
 func (anc *activeNodesCache) GetNodes() []string {
 	anc.lock.Lock()
 	defer anc.lock.Unlock()
@@ -50,6 +57,13 @@ type HttpRedirector struct {
 	nodeCache *activeNodesCache
 }
 
+func NewHttpRedirector(client *RadioAPI) *HttpRedirector {
+	return &HttpRedirector{
+		client: client,
+		nodeCache: newActiveNodesCache(client),
+	}
+}
+
 // Return an active node, chosen randomly.
 func (h *HttpRedirector) pickActiveNode() string {
 	nodes := h.nodeCache.GetNodes()
@@ -117,8 +131,8 @@ func (h *HttpRedirector) serveSource(w http.ResponseWriter, r *http.Request) {
 
 func (h *HttpRedirector) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	if r.Method == "SOURCE" {
-		serveSource(w, r)
+		h.serveSource(w, r)
 	} else {
-		serveRelay(w, r)
+		h.serveRelay(w, r)
 	}
 }