git » gofer » commit 9290b00

config: Move to YAML

author Alberto Bertogli
2020-06-01 01:08:29 UTC
committer Alberto Bertogli
2020-06-01 21:01:46 UTC
parent b3349f342cbe5d0b94ffb89f0f40146a581f2e01

config: Move to YAML

This patch moves the configuration language to YAML, which is more
practical for this type of complex configuration, and can be easily
augmented with other tools such as cue.

.gitignore +2 -2
config/config.go +17 -70
config/config_test.go +32 -52
etc/systemd/gofer.service +1 -1
go.mod +1 -1
go.sum +4 -2
gofer.conf.example +0 -42
gofer.go +7 -12
gofer.yaml.example +33 -0
gofer_test.go +1 -1
proxy/http.go +40 -45
proxy/proxy_test.go +17 -15
proxy/raw.go +6 -6
test/01-be.conf +0 -11
test/01-be.yaml +12 -0
test/01-fe.conf +0 -39
test/01-fe.yaml +37 -0
test/test.sh +2 -2

diff --git a/.gitignore b/.gitignore
index 0d967e7..f73a217 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,6 @@ gofer
 test/util/exp
 
 # Configuration and certificates, to prevent accidents.
-*.conf
+*.yaml
 *.pem
-!test/*.conf
+!test/*.yaml
diff --git a/config/config.go b/config/config.go
index 8f47736..7b9f432 100644
--- a/config/config.go
+++ b/config/config.go
@@ -2,36 +2,31 @@
 package config
 
 import (
-	"bytes"
 	"fmt"
 	"io/ioutil"
-	"strings"
 
-	"github.com/BurntSushi/toml"
+	"gopkg.in/yaml.v3"
 )
 
 type Config struct {
-	ControlAddr string `toml:"control_addr"`
+	ControlAddr string `yaml:"control_addr"`
 
-	HTTP  []*HTTP
-	HTTPS []*HTTPS
-	Raw   []Raw
-
-	// Map of name -> routes for HTTP(S).
-	Routes map[string]RouteTable
-
-	// Undecoded fields - private so we don't serialize them.
-	undecoded []string
+	// Map address -> config.
+	HTTP  map[string]HTTP
+	HTTPS map[string]HTTPS
+	Raw   map[string]Raw
 }
 
 type HTTP struct {
-	Addr       string
-	RouteTable RouteTable `toml:"routes",omitempty`
-	BaseRoutes string     `toml:"base_routes"`
+	Proxy    map[string]string
+	Dir      map[string]string
+	Static   map[string]string
+	Redirect map[string]string
+	CGI      map[string]string
 }
 
 type HTTPS struct {
-	HTTP
+	HTTP  `yaml:",inline"`
 	Certs string
 }
 
@@ -39,23 +34,7 @@ type Raw struct {
 	Addr  string
 	Certs string
 	To    string
-	ToTLS bool `toml:"to_tls",omitempty`
-}
-
-type RouteTable map[string]string
-
-// mergeRoutes merges the table src into dst, by adding the entries in src
-// that are missing from dst.
-func mergeRoutes(src, dst RouteTable) {
-	for k, v := range src {
-		if _, ok := dst[k]; !ok {
-			dst[k] = v
-		}
-	}
-}
-
-func (c Config) Undecoded() []string {
-	return c.undecoded
+	ToTLS bool `yaml:"to_tls"`
 }
 
 func (c Config) String() string {
@@ -67,12 +46,8 @@ func (c Config) String() string {
 }
 
 func (c Config) ToString() (string, error) {
-	buf := new(bytes.Buffer)
-	if err := toml.NewEncoder(buf).Encode(c); err != nil {
-		return "", err
-	}
-
-	return buf.String(), nil
+	d, err := yaml.Marshal(&c)
+	return string(d), err
 }
 
 func Load(filename string) (*Config, error) {
@@ -85,34 +60,6 @@ func Load(filename string) (*Config, error) {
 
 func LoadString(contents string) (*Config, error) {
 	conf := &Config{}
-	md, err := toml.Decode(contents, conf)
-	if err != nil {
-		return nil, fmt.Errorf("error parsing config: %v", err)
-	}
-
-	// Save undecoded keys so they can be accessed later (e.g. for debugging
-	// or checking).
-	for _, key := range md.Undecoded() {
-		conf.undecoded = append(conf.undecoded, strings.Join(key, "."))
-	}
-
-	// Link routes.
-	for _, https := range conf.HTTPS {
-		if https.RouteTable == nil {
-			https.RouteTable = RouteTable{}
-		}
-		if https.BaseRoutes != "" {
-			mergeRoutes(conf.Routes[https.BaseRoutes], https.RouteTable)
-		}
-	}
-	for _, http := range conf.HTTP {
-		if http.RouteTable == nil {
-			http.RouteTable = RouteTable{}
-		}
-		if http.BaseRoutes != "" {
-			mergeRoutes(conf.Routes[http.BaseRoutes], http.RouteTable)
-		}
-	}
-
-	return conf, nil
+	err := yaml.Unmarshal([]byte(contents), conf)
+	return conf, err
 }
diff --git a/config/config_test.go b/config/config_test.go
index 1ec9782..22d17c0 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -7,64 +7,59 @@ import (
 )
 
 func TestSimple(t *testing.T) {
+	// Note: no TABs in the contents, they are not valid indentation in yaml
+	// and the parser complains about "found character that cannot start any
+	// token".
 	const contents = `
-control_addr = "127.0.0.1:9081"
+control_addr: "127.0.0.1:9081"
 
-[[raw]]
-addr = ":995"
-certs = "/etc/letsencrypt/live/"
-to = "blerg.com:1995"
-to_tls = true
+raw:
+  ":995":
+    certs: "/etc/letsencrypt/live/"
+    to: "blerg.com:1995"
+    to_tls: true
 
-[[http]]
-addr = ":http"
-base_routes = "default"
+_proxy: &proxy
+  "/": "http://def/"
+  "/common": "http://common/"
 
-  [http.routes]
-    "/srv" = "http://srv/"
+http:
+  ":http":
+    proxy:
+      <<: *proxy
+      "/srv": "http://srv/"
 
-[[https]]
-addr = ":https"
-certs = "/etc/letsencrypt/live/"
-base_routes = "default"
-unknown_field = "x"
-
-  [https.routes]
-    "/" = "http://tlsoverrides/"
-    "/srv" = "http://srv2/"
-
-[routes.default]
-"/" = "http://def/"
-"/common" = "http://common/"
+https:
+  ":https":
+    certs: "/etc/letsencrypt/live/"
+    proxy:
+      <<: *proxy
+      "/": "http://tlsoverrides/"
+      "/srv": "http://srv2/"
 `
 
 	expected := Config{
 		ControlAddr: "127.0.0.1:9081",
-		Raw: []Raw{
-			Raw{
-				Addr:  ":995",
+		Raw: map[string]Raw{
+			":995": {
 				Certs: "/etc/letsencrypt/live/",
 				To:    "blerg.com:1995",
 				ToTLS: true,
 			},
 		},
-		HTTP: []*HTTP{
-			&HTTP{
-				Addr:       ":http",
-				BaseRoutes: "default",
-				RouteTable: RouteTable{
+		HTTP: map[string]HTTP{
+			":http": {
+				Proxy: map[string]string{
 					"/":       "http://def/",
 					"/common": "http://common/",
 					"/srv":    "http://srv/",
 				},
 			},
 		},
-		HTTPS: []*HTTPS{
-			&HTTPS{
+		HTTPS: map[string]HTTPS{
+			":https": {
 				HTTP: HTTP{
-					Addr:       ":https",
-					BaseRoutes: "default",
-					RouteTable: RouteTable{
+					Proxy: map[string]string{
 						"/":       "http://tlsoverrides/",
 						"/common": "http://common/",
 						"/srv":    "http://srv2/",
@@ -73,14 +68,6 @@ unknown_field = "x"
 				Certs: "/etc/letsencrypt/live/",
 			},
 		},
-		Routes: map[string]RouteTable{
-			"default": RouteTable{
-				"/":       "http://def/",
-				"/common": "http://common/",
-			},
-		},
-
-		undecoded: []string{"https.unknown_field"},
 	}
 
 	conf, err := LoadString(contents)
@@ -93,11 +80,4 @@ unknown_field = "x"
 		t.Errorf("  expected: %v", expected.String())
 		t.Errorf("  got:      %v", conf.String())
 	}
-
-	if !reflect.DeepEqual(conf.Undecoded(), expected.Undecoded()) {
-		t.Errorf("undecoded is not as expected")
-		t.Errorf("  expected: %q", expected.Undecoded())
-		t.Errorf("  got:      %q", conf.Undecoded())
-	}
-
 }
diff --git a/etc/systemd/gofer.service b/etc/systemd/gofer.service
index f707de7..659bf93 100644
--- a/etc/systemd/gofer.service
+++ b/etc/systemd/gofer.service
@@ -3,7 +3,7 @@ Description=gofer proxy
 Requires=gofer-http.socket gofer-https.socket
 
 [Service]
-ExecStart=/usr/local/bin/gofer -configfile=/etc/gofer.conf
+ExecStart=/usr/local/bin/gofer -configfile=/etc/gofer.yaml
 
 Type=simple
 Restart=always
diff --git a/go.mod b/go.mod
index 7c0aec9..217307b 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,6 @@ go 1.14
 require (
 	blitiri.com.ar/go/log v0.0.0-20171003035348-6cd06f6ca2f8
 	blitiri.com.ar/go/systemd v0.0.0-20171003041308-cdc4fd023aa4
-	github.com/BurntSushi/toml v0.3.1
 	golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0
+	gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86
 )
diff --git a/go.sum b/go.sum
index 5b34b45..f24db3c 100644
--- a/go.sum
+++ b/go.sum
@@ -2,8 +2,6 @@ blitiri.com.ar/go/log v0.0.0-20171003035348-6cd06f6ca2f8 h1:1lsgqZmMh8DYLb/ZaBeS
 blitiri.com.ar/go/log v0.0.0-20171003035348-6cd06f6ca2f8/go.mod h1:xOW3xCYp3dEVSQWNKiiKIqBtjVN4cinE+0HypCpGC+E=
 blitiri.com.ar/go/systemd v0.0.0-20171003041308-cdc4fd023aa4 h1:ceTBe2TiHNkhA7q/TAHyukC5Jc1iUb7On/++f1Mszwk=
 blitiri.com.ar/go/systemd v0.0.0-20171003041308-cdc4fd023aa4/go.mod h1:FmDkVlYnOzDHOhtSwtLHh6z9WVVx+aPjrHkPtfA3qhI=
-github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U=
 golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
@@ -11,3 +9,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86 h1:OfFoIUYv/me30yv7XlMy4F9RJw8DEm8WQ6QG1Ph4bH0=
+gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/gofer.conf.example b/gofer.conf.example
deleted file mode 100644
index ea979eb..0000000
--- a/gofer.conf.example
+++ /dev/null
@@ -1,42 +0,0 @@
-
-# Address for the control/debug http.
-control_addr = "127.0.0.1:8081"
-
-# HTTP(s) proxy
-[[https]]
-
-# Address to listen on.
-# systemd socket passing is supported, use "&name" to indicate that you've set
-# up a systemd socket unit with "FileDescriptorName=name".
-addr = ":https"
-
-certs = "/etc/letsencrypt/live/"
-
-# Take the routes from the definition below as a baseline.
-base_routes = "default"
-
-  # Extend the base routes.
-  [https.routes]
-  "/local/" = "http://localhost:99/"
-
-
-[[http]]
-addr = ":http"
-base_routes = "default"
-
-
-[routes.default]
-"/" = "http://localhost:8080/"
-
-
-[[raw]]
-addr = ":995"
-
-# If this is present, we will listen on a TLS socket; otherwise it will be a
-# plain socket.
-certs = "/etc/letsencrypt/live/"
-
-# Where to connect to. If to_tls is true, then we will do TLS against the
-# backend.
-to = "example.com:1995"
-to_tls = true
diff --git a/gofer.go b/gofer.go
index 7fa7d0c..cf5cf6c 100644
--- a/gofer.go
+++ b/gofer.go
@@ -12,8 +12,7 @@ import (
 
 // Flags.
 var (
-	configfile = flag.String("configfile", "gofer.conf",
-		"Configuration file")
+	configfile = flag.String("configfile", "gofer.yaml", "Configuration file")
 )
 
 func main() {
@@ -25,20 +24,16 @@ func main() {
 		log.Fatalf("error reading config file: %v", err)
 	}
 
-	for _, k := range conf.Undecoded() {
-		log.Infof("warning: undecoded config key: %q", k)
+	for addr, https := range conf.HTTPS {
+		go proxy.HTTPS(addr, https)
 	}
 
-	for _, https := range conf.HTTPS {
-		go proxy.HTTPS(*https)
+	for addr, http := range conf.HTTP {
+		go proxy.HTTP(addr, http)
 	}
 
-	for _, http := range conf.HTTP {
-		go proxy.HTTP(*http)
-	}
-
-	for _, raw := range conf.Raw {
-		go proxy.Raw(raw)
+	for addr, raw := range conf.Raw {
+		go proxy.Raw(addr, raw)
 	}
 
 	if conf.ControlAddr != "" {
diff --git a/gofer.yaml.example b/gofer.yaml.example
new file mode 100644
index 0000000..fd626e2
--- /dev/null
+++ b/gofer.yaml.example
@@ -0,0 +1,33 @@
+
+# Address for the control/debug http.
+control_addr: "127.0.0.1:8081"
+
+# HTTP(s) proxy
+https:
+  # Address to listen on.
+  # systemd socket passing is supported, use "&name" to indicate that you've
+  # set up a systemd socket unit with "FileDescriptorName=name".
+  ":https":
+    # Location of the certificates, for TLS.
+    certs: "/etc/letsencrypt/live/"
+
+    proxy:
+      "/": "http://localhost:8080/"
+      "/local/": "http://localhost:99/"
+
+http:
+  ":http":
+    proxy:
+      "/": "http://localhost:8080/"
+
+
+raw:
+  ":995":
+    # If this is present, we will listen on a TLS socket; otherwise it will be
+    # a plain socket.
+    certs: "/etc/letsencrypt/live/"
+
+    # Where to connect to. If to_tls is true, then we will do TLS against the
+    # backend.
+    to: "example.com:1995"
+    to_tls: true
diff --git a/gofer_test.go b/gofer_test.go
index 1b21c81..74440da 100644
--- a/gofer_test.go
+++ b/gofer_test.go
@@ -12,7 +12,7 @@ import (
 )
 
 func TestDumpConfig(t *testing.T) {
-	conf, err := config.Load("gofer.conf.example")
+	conf, err := config.Load("gofer.yaml.example")
 	if err != nil {
 		t.Fatalf("error loading config example: %v", err)
 	}
diff --git a/proxy/http.go b/proxy/http.go
index 4721ec4..4527efa 100644
--- a/proxy/http.go
+++ b/proxy/http.go
@@ -20,11 +20,11 @@ import (
 	"blitiri.com.ar/go/systemd"
 )
 
-func httpServer(conf config.HTTP) *http.Server {
-	ev := trace.NewEventLog("httpserver", conf.Addr)
+func httpServer(addr string, conf config.HTTP) *http.Server {
+	ev := trace.NewEventLog("httpserver", addr)
 
 	srv := &http.Server{
-		Addr: conf.Addr,
+		Addr: addr,
 
 		ReadTimeout:  30 * time.Second,
 		WriteTimeout: 30 * time.Second,
@@ -35,56 +35,57 @@ func httpServer(conf config.HTTP) *http.Server {
 	// Load route table.
 	mux := http.NewServeMux()
 	srv.Handler = mux
-	for from, to := range conf.RouteTable {
-		toURL, err := url.Parse(to)
-		if err != nil {
-			log.Fatalf("route %q -> %q: destination is not a valid URL: %v",
-				from, to, err)
-		}
-		log.Infof("%s route %q -> %q", srv.Addr, from, toURL)
-		switch toURL.Scheme {
-		case "http", "https":
-			mux.Handle(from, makeProxy(from, *toURL))
-		case "dir":
-			mux.Handle(from, makeDir(from, *toURL))
-		case "static":
-			mux.Handle(from, makeStatic(from, *toURL))
-		case "redirect":
-			mux.Handle(from, makeRedirect(from, *toURL))
-		case "cgi":
-			mux.Handle(from, makeCGI(from, *toURL))
-		default:
-			log.Fatalf("route %q -> %q: invalid destination scheme %q",
-				from, to, toURL.Scheme)
+
+	routes := []struct {
+		name        string
+		table       map[string]string
+		makeHandler func(string, url.URL) http.Handler
+	}{
+		{"proxy", conf.Proxy, makeProxy},
+		{"dir", conf.Dir, makeDir},
+		{"static", conf.Static, makeStatic},
+		{"redirect", conf.Redirect, makeRedirect},
+		{"cgi", conf.CGI, makeCGI},
+	}
+	for _, r := range routes {
+		for from, to := range r.table {
+			toURL, err := url.Parse(to)
+			if err != nil {
+				log.Fatalf(
+					"route %s %q -> %q: destination is not a valid URL: %v",
+					r.name, from, to, err)
+			}
+			log.Infof("%s route %q -> %s %q", srv.Addr, from, r.name, toURL)
+			mux.Handle(from, r.makeHandler(from, *toURL))
 		}
 	}
 
 	return srv
 }
 
-func HTTP(conf config.HTTP) {
-	srv := httpServer(conf)
-	lis, err := systemd.Listen("tcp", conf.Addr)
+func HTTP(addr string, conf config.HTTP) {
+	srv := httpServer(addr, conf)
+	lis, err := systemd.Listen("tcp", addr)
 	if err != nil {
-		log.Fatalf("%s error listening: %v", conf.Addr, err)
+		log.Fatalf("%s error listening: %v", addr, err)
 	}
-	log.Infof("%s http proxy starting on %q", conf.Addr, lis.Addr())
+	log.Infof("%s http proxy starting on %q", addr, lis.Addr())
 	err = srv.Serve(lis)
-	log.Fatalf("%s http proxy exited: %v", conf.Addr, err)
+	log.Fatalf("%s http proxy exited: %v", addr, err)
 }
 
-func HTTPS(conf config.HTTPS) {
+func HTTPS(addr string, conf config.HTTPS) {
 	var err error
-	srv := httpServer(conf.HTTP)
+	srv := httpServer(addr, conf.HTTP)
 
 	srv.TLSConfig, err = util.LoadCerts(conf.Certs)
 	if err != nil {
-		log.Fatalf("%s error loading certs: %v", conf.Addr, err)
+		log.Fatalf("%s error loading certs: %v", addr, err)
 	}
 
-	rawLis, err := systemd.Listen("tcp", conf.Addr)
+	rawLis, err := systemd.Listen("tcp", addr)
 	if err != nil {
-		log.Fatalf("%s error listening: %v", conf.Addr, err)
+		log.Fatalf("%s error listening: %v", addr, err)
 	}
 
 	// We need to set the NextProtos manually before creating the TLS
@@ -93,9 +94,9 @@ func HTTPS(conf config.HTTPS) {
 		"h2", "http/1.1")
 	lis := tls.NewListener(rawLis, srv.TLSConfig)
 
-	log.Infof("%s https proxy starting on %q", conf.Addr, lis.Addr())
+	log.Infof("%s https proxy starting on %q", addr, lis.Addr())
 	err = srv.Serve(lis)
-	log.Fatalf("%s https proxy exited: %v", conf.Addr, err)
+	log.Fatalf("%s https proxy exited: %v", addr, err)
 }
 
 func makeProxy(from string, to url.URL) http.Handler {
@@ -276,18 +277,12 @@ func queryToArgs(query string) []string {
 func makeRedirect(from string, to url.URL) http.Handler {
 	from = stripDomain(from)
 
-	dst, err := url.Parse(to.Opaque)
-	if err != nil {
-		log.Fatalf("Invalid destination %q for redirect route: %v",
-			to.Opaque, err)
-	}
-
 	return WithLogging("http:redirect",
 		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 			tr, _ := trace.FromContext(r.Context())
-			target := *dst
+			target := to
 			target.RawQuery = r.URL.RawQuery
-			target.Path = adjustPath(r.URL.Path, from, dst.Path)
+			target.Path = adjustPath(r.URL.Path, from, to.Path)
 			tr.Printf("redirect to %q", target.String())
 
 			http.Redirect(w, r, target.String(), http.StatusTemporaryRedirect)
diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go
index e188fd3..8ce5132 100644
--- a/proxy/proxy_test.go
+++ b/proxy/proxy_test.go
@@ -72,19 +72,21 @@ func TestMain(m *testing.M) {
 	pwd, _ := os.Getwd()
 
 	const configTemplate = `
-[[raw]]
-addr = "$RAW_ADDR"
-to = "$BACKEND_ADDR"
-
-[[http]]
-addr = "$HTTP_ADDR"
-
-[http.routes]
-"/be/" = "$BACKEND_URL"
-"localhost/xy/" = "$BACKEND_URL"
-"/static/hola" = "static:$PWD/testdata/hola"
-"/dir/" = "dir:$PWD/testdata/"
-"/redir/" = "redirect:http://$HTTP_ADDR/dir/"
+raw:
+  "$RAW_ADDR":
+    to: "$BACKEND_ADDR"
+
+http:
+  "$HTTP_ADDR":
+    proxy:
+      "/be/": "$BACKEND_URL"
+      "localhost/xy/": "$BACKEND_URL"
+    static:
+      "/static/hola": "$PWD/testdata/hola"
+    dir:
+      "/dir/": "$PWD/testdata/"
+    redirect:
+      "/redir/": "http://$HTTP_ADDR/dir/"
 `
 	configStr := strings.NewReplacer(
 		"$RAW_ADDR", rawAddr,
@@ -99,8 +101,8 @@ addr = "$HTTP_ADDR"
 		log.Fatalf("error loading test config: %v", err)
 	}
 
-	go Raw(conf.Raw[0])
-	go HTTP(*conf.HTTP[0])
+	go Raw(rawAddr, conf.Raw[rawAddr])
+	go HTTP(httpAddr, conf.HTTP[httpAddr])
 
 	waitForHTTPServer(httpAddr)
 	waitForHTTPServer(rawAddr)
diff --git a/proxy/raw.go b/proxy/raw.go
index 68ba7e0..29e2be0 100644
--- a/proxy/raw.go
+++ b/proxy/raw.go
@@ -12,7 +12,7 @@ import (
 	"blitiri.com.ar/go/systemd"
 )
 
-func Raw(conf config.Raw) {
+func Raw(addr string, conf config.Raw) {
 	var err error
 
 	var tlsConfig *tls.Config
@@ -25,20 +25,20 @@ func Raw(conf config.Raw) {
 
 	var lis net.Listener
 	if tlsConfig != nil {
-		lis, err = systemd.Listen("tcp", conf.Addr)
+		lis, err = systemd.Listen("tcp", addr)
 		lis = tls.NewListener(lis, tlsConfig)
 	} else {
-		lis, err = systemd.Listen("tcp", conf.Addr)
+		lis, err = systemd.Listen("tcp", addr)
 	}
 	if err != nil {
-		log.Fatalf("Raw proxy error listening on %q: %v", conf.Addr, err)
+		log.Fatalf("Raw proxy error listening on %q: %v", addr, err)
 	}
 
-	log.Infof("Raw proxy on %q (%q)", conf.Addr, lis.Addr())
+	log.Infof("Raw proxy on %q (%q)", addr, lis.Addr())
 	for {
 		conn, err := lis.Accept()
 		if err != nil {
-			log.Fatalf("%s error accepting: %v", conf.Addr, err)
+			log.Fatalf("%s error accepting: %v", addr, err)
 		}
 
 		go forward(conn, conf.To, conf.ToTLS)
diff --git a/test/01-be.conf b/test/01-be.conf
deleted file mode 100644
index c60d021..0000000
--- a/test/01-be.conf
+++ /dev/null
@@ -1,11 +0,0 @@
-
-control_addr = "127.0.0.1:8459"
-
-[[http]]
-addr = ":8450"
-
-[http.routes]
-"/dir/" = "dir:testdata/dir"
-"/file" = "static:testdata/file"
-"/file/second" = "static:testdata/dir/ñaca"
-"/cgi/" = "cgi:testdata/cgi.sh?param 1;param 2"
diff --git a/test/01-be.yaml b/test/01-be.yaml
new file mode 100644
index 0000000..1a25b0d
--- /dev/null
+++ b/test/01-be.yaml
@@ -0,0 +1,12 @@
+
+control_addr: "127.0.0.1:8459"
+
+http:
+  ":8450":
+    dir:
+      "/dir": "testdata/dir"
+    static:
+      "/file": "testdata/file"
+      "/file/second": "testdata/dir/ñaca"
+    cgi:
+      "/cgi": "testdata/cgi.sh?param 1;param 2"
diff --git a/test/01-fe.conf b/test/01-fe.conf
deleted file mode 100644
index bfc8d07..0000000
--- a/test/01-fe.conf
+++ /dev/null
@@ -1,39 +0,0 @@
-
-control_addr = "127.0.0.1:8440"
-
-[[http]]
-addr = ":8441"
-base_routes = "default"
-
-[[https]]
-addr = ":8442"
-certs = ".certs"
-base_routes = "default"
-
-[https.routes]
-"/dar/" = "http://localhost:8450/dir/"
-
-[routes.default]
-"/dir/" = "http://localhost:8450/dir/"
-"/file" = "http://localhost:8450/file"
-"/cgi/" = "http://localhost:8450/cgi/"
-"/gogo/" = "redirect:https://google.com"
-
-# Target unreachable: connection refused.
-"/bad/unreacheable" = "http://localhost:1/"
-
-# Bad target.
-"/bad/empty" = "http:"
-
-
-# Raw proxy to the same backend.
-[[raw]]
-addr = ":8445"
-to = "localhost:8450"
-to_tls = false
-
-[[raw]]
-addr = ":8446"
-certs = ".certs"
-to = "localhost:8450"
-to_tls = false
diff --git a/test/01-fe.yaml b/test/01-fe.yaml
new file mode 100644
index 0000000..7c4fd69
--- /dev/null
+++ b/test/01-fe.yaml
@@ -0,0 +1,37 @@
+
+control_addr: "127.0.0.1:8440"
+
+_proxy: &proxyroutes
+  "/dir/": "http://localhost:8450/dir/"
+  "/file": "http://localhost:8450/file"
+  "/cgi/": "http://localhost:8450/cgi/"
+  "/bad/unreacheable": "http://localhost:1/"
+  "/bad/empty": "http:"
+  "/dar/": "http://localhost:8450/dir/"
+
+_redirect: &redirect
+  "/gogo/": "https://google.com"
+
+
+http:
+  ":8441":
+    proxy: *proxyroutes
+    redirect: *redirect
+
+https:
+  ":8442":
+    certs: ".certs"
+    proxy: *proxyroutes
+    redirect: *redirect
+
+
+
+# Raw proxy to the same backend.
+raw:
+  ":8445":
+    to: "localhost:8450"
+
+  ":8446":
+    to: "localhost:8450"
+    certs: ".certs"
+
diff --git a/test/test.sh b/test/test.sh
index 61aced8..eb20ca3 100755
--- a/test/test.sh
+++ b/test/test.sh
@@ -85,13 +85,13 @@ function snoop() {
 echo "## Setup"
 
 # Launch the backend serving static files and CGI.
-gofer -logfile=.01-be.log -configfile=01-be.conf
+gofer -logfile=.01-be.log -configfile=01-be.yaml
 DIR_PID=$PID
 wait_until_ready 8450
 
 # Launch the test instance.
 generate_certs
-gofer -logfile=.01-fe.log -configfile=01-fe.conf
+gofer -logfile=.01-fe.log -configfile=01-fe.yaml
 wait_until_ready 8441  # http
 wait_until_ready 8442  # https
 wait_until_ready 8445  # raw