Browse Source

Add TLS support for client/server communication

Analogous to etcd, optionally use TLS to encrypt communication
between the client and the server. Also, as an option, client side
certificates can be used to authenticate connecting clients.

Fixes #178 and #229
Eugene Yakubovich 9 years ago
parent
commit
3655315cfe

+ 103 - 0
Documentation/client-server.md

@@ -0,0 +1,103 @@
+## Client/Server mode (EXPERIMENTAL)
+
+### Getting Started
+
+By default flannel runs without a central controller, utilizing etcd for coordination.
+However, it can also be configured to run in client/server mode, where a special instance of the flannel daemon (the server) is the only one that communicates with etcd.
+This setup offers the advantange of having only a single server directly connecting to etcd, with the rest of the flannel daemons (clients) accessing etcd via the server.
+The server is completely stateless and does not assume that it has exclusive access to the etcd keyspace.
+In the future this will be exploited to provide failover; currently, however, the clients accept only a single endpoint to which to connect.
+The stateless server also makes it possible to run some nodes in client mode side-by-side with those connecting to etcd directly.
+
+To run the flannel daemon in server mode, simply provide the `--listen` flag:
+```
+$ flanneld --listen=0.0.0.0:8888
+```
+
+To run the flannel daemon in client mode, use the `--remote` flag to point it to a flannel server instance:
+```
+$ flanneld --remote=10.0.0.3:8888
+```
+
+It is important to note that the server itself does not join the flannel network (i.e. it won't assign itself a subnet) -- it just satisfies requests from the clients.
+As such, if the host running the flannel server also needs to participate in the overlay, it should start two instances of flannel - one in client mode and one in server mode.
+
+
+### Systemd Socket Activation
+
+The server mode supports [systemd socket activation](http://www.freedesktop.org/software/systemd/man/systemd.socket.html).
+To request the use of socket activation, use the `--listen` flag:
+```
+$ flanneld --listen=fd://
+```
+
+This assumes that the listening socket is passed in via the default descriptor 3.
+To specify a different descriptor number, such as 5, use the following form:
+```
+$ flanneld --listen=fd://5
+```
+
+### Use of SSL/TLS to secure client/server communication
+
+By default, the communication between the client and server is unencrypted (uses HTTP).
+Just like the link between flannel and etcd can be secured via SSL/TLS, so too can the link between the client and the server be encrypted using SSL/TLS.
+You will need a CA certificate and also a private key, certificate pair for the server.
+The server certificate must be signed by the corresponding CA.
+The easiest way to get started is by using the [etcd-ca](https://github.com/coreos/etcd-ca) project:
+
+```
+# Create a new Certificate Authority (CA)
+$ etcd-ca init
+
+# Export the CA certifcate -- this will generate ca.crt
+$ etcd-ca export | tar xv
+
+# Create a new private key for the server
+$ etcd-ca new-cert myserver
+
+# Sign the server private key by the CA
+$ etcd-ca sign myserver
+
+# Export the server key and certifiate
+# This will generate myserver.key and myserver.crt
+$ etcd-ca export myserver | tar xv
+```
+
+You can now start the flannel server, specifying the private key and corresponding signed certificate:
+```
+$ flanneld --listen=0.0.0.0 --remote-certfile=./myserver.crt --remote-keyfile=./myserver.key
+```
+
+Finally, start the flannel client(s) pointing them at the CA certificate that was used to sign the server certificate:
+
+```
+$ flanneld --remote=10.0.0.3:8888 --remote-cafile=./ca.crt
+```
+
+### Authenticating clients by use of client certificates
+
+You can use client SSL certificates to restrict connecting clients to those that have their certificate signed by your CA.
+Using [etcd-ca](https://github.com/coreos/etcd-ca) as the CA, first make sure you have executed ran the steps in the previous section.
+Next, generate and sign a certificate for the client (repeat steps below for each client):
+
+```
+# Create a private key for the client1
+$ etcd-ca new-cert client1
+
+# Sign the client1 private key by the CA
+$ etcd-ca sign client1
+
+# Export the client1 key and certifiate
+# This will generate client1.key and client1.crt
+$ etcd-ca export client1 | tar xv
+```
+
+Start the server, specifying the CA certificate that was used to sign the client certificates:
+```
+$ flanneld --listen=0.0.0.0 --remote-certfile=./myserver.crt --remote-keyfile=./myserver.key --remote-cafile=./ca.crt
+```
+
+Launch the clients by also specifying their private key and corresponding certificate:
+```
+$ flanneld --remote=10.0.0.3:8888 --remote-cafile=./ca.crt --remote-keyfile=./client1.key --remote-certfile=./client1.crt
+```

+ 5 - 0
Godeps/Godeps.json

@@ -25,6 +25,11 @@
 			"Comment": "release-107",
 			"Rev": "6ddfebb10ece847f1ae09c701834f1b15abbd8b2"
 		},
+		{
+			"ImportPath": "github.com/coreos/etcd/pkg/transport",
+			"Comment": "v2.1.0-rc.0-24-g8ab388f",
+			"Rev": "8ab388fa56e6ad4d5566a42ef9f5ebeadc433a6e"
+		},
 		{
 			"ImportPath": "github.com/coreos/go-etcd/etcd",
 			"Comment": "v2.0.0-3-g0424b5f",

+ 93 - 0
Godeps/_workspace/src/github.com/coreos/etcd/pkg/transport/keepalive_listener.go

@@ -0,0 +1,93 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+	"crypto/tls"
+	"net"
+	"time"
+)
+
+// NewKeepAliveListener returns a listener that listens on the given address.
+// http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html
+func NewKeepAliveListener(addr string, scheme string, info TLSInfo) (net.Listener, error) {
+	l, err := net.Listen("tcp", addr)
+	if err != nil {
+		return nil, err
+	}
+
+	if !info.Empty() && scheme == "https" {
+		cfg, err := info.ServerConfig()
+		if err != nil {
+			return nil, err
+		}
+
+		return newTLSKeepaliveListener(l, cfg), nil
+	}
+
+	return &keepaliveListener{
+		Listener: l,
+	}, nil
+}
+
+type keepaliveListener struct{ net.Listener }
+
+func (kln *keepaliveListener) Accept() (net.Conn, error) {
+	c, err := kln.Listener.Accept()
+	if err != nil {
+		return nil, err
+	}
+	tcpc := c.(*net.TCPConn)
+	// detection time: tcp_keepalive_time + tcp_keepalive_probes + tcp_keepalive_intvl
+	// default on linux:  30 + 8 * 30
+	// default on osx:    30 + 8 * 75
+	tcpc.SetKeepAlive(true)
+	tcpc.SetKeepAlivePeriod(30 * time.Second)
+	return tcpc, nil
+}
+
+// A tlsKeepaliveListener implements a network listener (net.Listener) for TLS connections.
+type tlsKeepaliveListener struct {
+	net.Listener
+	config *tls.Config
+}
+
+// Accept waits for and returns the next incoming TLS connection.
+// The returned connection c is a *tls.Conn.
+func (l *tlsKeepaliveListener) Accept() (c net.Conn, err error) {
+	c, err = l.Listener.Accept()
+	if err != nil {
+		return
+	}
+	tcpc := c.(*net.TCPConn)
+	// detection time: tcp_keepalive_time + tcp_keepalive_probes + tcp_keepalive_intvl
+	// default on linux:  30 + 8 * 30
+	// default on osx:    30 + 8 * 75
+	tcpc.SetKeepAlive(true)
+	tcpc.SetKeepAlivePeriod(30 * time.Second)
+	c = tls.Server(c, l.config)
+	return
+}
+
+// NewListener creates a Listener which accepts connections from an inner
+// Listener and wraps each connection with Server.
+// The configuration config must be non-nil and must have
+// at least one certificate.
+func newTLSKeepaliveListener(inner net.Listener, config *tls.Config) net.Listener {
+	l := &tlsKeepaliveListener{}
+	l.Listener = inner
+	l.config = config
+	return l
+}

+ 64 - 0
Godeps/_workspace/src/github.com/coreos/etcd/pkg/transport/keepalive_listener_test.go

@@ -0,0 +1,64 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+	"crypto/tls"
+	"net/http"
+	"os"
+	"testing"
+)
+
+// TestNewKeepAliveListener tests NewKeepAliveListener returns a listener
+// that accepts connections.
+// TODO: verify the keepalive option is set correctly
+func TestNewKeepAliveListener(t *testing.T) {
+	ln, err := NewKeepAliveListener("127.0.0.1:0", "http", TLSInfo{})
+	if err != nil {
+		t.Fatalf("unexpected NewKeepAliveListener error: %v", err)
+	}
+
+	go http.Get("http://" + ln.Addr().String())
+	conn, err := ln.Accept()
+	if err != nil {
+		t.Fatalf("unexpected Accept error: %v", err)
+	}
+	conn.Close()
+	ln.Close()
+
+	// tls
+	tmp, err := createTempFile([]byte("XXX"))
+	if err != nil {
+		t.Fatalf("unable to create tmpfile: %v", err)
+	}
+	defer os.Remove(tmp)
+	tlsInfo := TLSInfo{CertFile: tmp, KeyFile: tmp}
+	tlsInfo.parseFunc = fakeCertificateParserFunc(tls.Certificate{}, nil)
+	tlsln, err := NewKeepAliveListener("127.0.0.1:0", "https", tlsInfo)
+	if err != nil {
+		t.Fatalf("unexpected NewKeepAliveListener error: %v", err)
+	}
+
+	go http.Get("https://" + tlsln.Addr().String())
+	conn, err = tlsln.Accept()
+	if err != nil {
+		t.Fatalf("unexpected Accept error: %v", err)
+	}
+	if _, ok := conn.(*tls.Conn); !ok {
+		t.Errorf("failed to accept *tls.Conn")
+	}
+	conn.Close()
+	tlsln.Close()
+}

+ 206 - 0
Godeps/_workspace/src/github.com/coreos/etcd/pkg/transport/listener.go

@@ -0,0 +1,206 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/pem"
+	"fmt"
+	"io/ioutil"
+	"net"
+	"net/http"
+	"time"
+)
+
+func NewListener(addr string, scheme string, info TLSInfo) (net.Listener, error) {
+	l, err := net.Listen("tcp", addr)
+	if err != nil {
+		return nil, err
+	}
+
+	if scheme == "https" {
+		if info.Empty() {
+			return nil, fmt.Errorf("cannot listen on TLS for %s: KeyFile and CertFile are not presented", scheme+"://"+addr)
+		}
+		cfg, err := info.ServerConfig()
+		if err != nil {
+			return nil, err
+		}
+
+		l = tls.NewListener(l, cfg)
+	}
+
+	return l, nil
+}
+
+func NewTransport(info TLSInfo) (*http.Transport, error) {
+	cfg, err := info.ClientConfig()
+	if err != nil {
+		return nil, err
+	}
+
+	t := &http.Transport{
+		// timeouts taken from http.DefaultTransport
+		Dial: (&net.Dialer{
+			Timeout:   30 * time.Second,
+			KeepAlive: 30 * time.Second,
+		}).Dial,
+		TLSHandshakeTimeout: 10 * time.Second,
+		TLSClientConfig:     cfg,
+	}
+
+	return t, nil
+}
+
+type TLSInfo struct {
+	CertFile       string
+	KeyFile        string
+	CAFile         string
+	TrustedCAFile  string
+	ClientCertAuth bool
+
+	// parseFunc exists to simplify testing. Typically, parseFunc
+	// should be left nil. In that case, tls.X509KeyPair will be used.
+	parseFunc func([]byte, []byte) (tls.Certificate, error)
+}
+
+func (info TLSInfo) String() string {
+	return fmt.Sprintf("cert = %s, key = %s, ca = %s", info.CertFile, info.KeyFile, info.CAFile)
+}
+
+func (info TLSInfo) Empty() bool {
+	return info.CertFile == "" && info.KeyFile == ""
+}
+
+func (info TLSInfo) baseConfig() (*tls.Config, error) {
+	if info.KeyFile == "" || info.CertFile == "" {
+		return nil, fmt.Errorf("KeyFile and CertFile must both be present[key: %v, cert: %v]", info.KeyFile, info.CertFile)
+	}
+
+	cert, err := ioutil.ReadFile(info.CertFile)
+	if err != nil {
+		return nil, err
+	}
+
+	key, err := ioutil.ReadFile(info.KeyFile)
+	if err != nil {
+		return nil, err
+	}
+
+	parseFunc := info.parseFunc
+	if parseFunc == nil {
+		parseFunc = tls.X509KeyPair
+	}
+
+	tlsCert, err := parseFunc(cert, key)
+	if err != nil {
+		return nil, err
+	}
+
+	cfg := &tls.Config{
+		Certificates: []tls.Certificate{tlsCert},
+		MinVersion:   tls.VersionTLS10,
+	}
+	return cfg, nil
+}
+
+// cafiles returns a list of CA file paths.
+func (info TLSInfo) cafiles() []string {
+	cs := make([]string, 0)
+	if info.CAFile != "" {
+		cs = append(cs, info.CAFile)
+	}
+	if info.TrustedCAFile != "" {
+		cs = append(cs, info.TrustedCAFile)
+	}
+	return cs
+}
+
+// ServerConfig generates a tls.Config object for use by an HTTP server.
+func (info TLSInfo) ServerConfig() (*tls.Config, error) {
+	cfg, err := info.baseConfig()
+	if err != nil {
+		return nil, err
+	}
+
+	cfg.ClientAuth = tls.NoClientCert
+	if info.CAFile != "" || info.ClientCertAuth {
+		cfg.ClientAuth = tls.RequireAndVerifyClientCert
+	}
+
+	CAFiles := info.cafiles()
+	if len(CAFiles) > 0 {
+		cp, err := newCertPool(CAFiles)
+		if err != nil {
+			return nil, err
+		}
+		cfg.ClientCAs = cp
+	}
+
+	return cfg, nil
+}
+
+// ClientConfig generates a tls.Config object for use by an HTTP client.
+func (info TLSInfo) ClientConfig() (*tls.Config, error) {
+	var cfg *tls.Config
+	var err error
+
+	if !info.Empty() {
+		cfg, err = info.baseConfig()
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		cfg = &tls.Config{}
+	}
+
+	CAFiles := info.cafiles()
+	if len(CAFiles) > 0 {
+		cfg.RootCAs, err = newCertPool(CAFiles)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return cfg, nil
+}
+
+// newCertPool creates x509 certPool with provided CA files.
+func newCertPool(CAFiles []string) (*x509.CertPool, error) {
+	certPool := x509.NewCertPool()
+
+	for _, CAFile := range CAFiles {
+		pemByte, err := ioutil.ReadFile(CAFile)
+		if err != nil {
+			return nil, err
+		}
+
+		for {
+			var block *pem.Block
+			block, pemByte = pem.Decode(pemByte)
+			if block == nil {
+				break
+			}
+			cert, err := x509.ParseCertificate(block.Bytes)
+			if err != nil {
+				return nil, err
+			}
+			certPool.AddCert(cert)
+		}
+	}
+
+	return certPool, nil
+}

+ 242 - 0
Godeps/_workspace/src/github.com/coreos/etcd/pkg/transport/listener_test.go

@@ -0,0 +1,242 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+	"crypto/tls"
+	"errors"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"testing"
+)
+
+func createTempFile(b []byte) (string, error) {
+	f, err := ioutil.TempFile("", "etcd-test-tls-")
+	if err != nil {
+		return "", err
+	}
+	defer f.Close()
+
+	if _, err = f.Write(b); err != nil {
+		return "", err
+	}
+
+	return f.Name(), nil
+}
+
+func fakeCertificateParserFunc(cert tls.Certificate, err error) func(certPEMBlock, keyPEMBlock []byte) (tls.Certificate, error) {
+	return func(certPEMBlock, keyPEMBlock []byte) (tls.Certificate, error) {
+		return cert, err
+	}
+}
+
+// TestNewListenerTLSInfo tests that NewListener with valid TLSInfo returns
+// a TLS listerner that accepts TLS connections.
+func TestNewListenerTLSInfo(t *testing.T) {
+	tmp, err := createTempFile([]byte("XXX"))
+	if err != nil {
+		t.Fatalf("unable to create tmpfile: %v", err)
+	}
+	defer os.Remove(tmp)
+	tlsInfo := TLSInfo{CertFile: tmp, KeyFile: tmp}
+	tlsInfo.parseFunc = fakeCertificateParserFunc(tls.Certificate{}, nil)
+	ln, err := NewListener("127.0.0.1:0", "https", tlsInfo)
+	if err != nil {
+		t.Fatalf("unexpected NewListener error: %v", err)
+	}
+	defer ln.Close()
+
+	go http.Get("https://" + ln.Addr().String())
+	conn, err := ln.Accept()
+	if err != nil {
+		t.Fatalf("unexpected Accept error: %v", err)
+	}
+	defer conn.Close()
+	if _, ok := conn.(*tls.Conn); !ok {
+		t.Errorf("failed to accept *tls.Conn")
+	}
+}
+
+func TestNewListenerTLSEmptyInfo(t *testing.T) {
+	_, err := NewListener("127.0.0.1:0", "https", TLSInfo{})
+	if err == nil {
+		t.Errorf("err = nil, want not presented error")
+	}
+}
+
+func TestNewListenerTLSInfoNonexist(t *testing.T) {
+	tlsInfo := TLSInfo{CertFile: "@badname", KeyFile: "@badname"}
+	_, err := NewListener("127.0.0.1:0", "https", tlsInfo)
+	werr := &os.PathError{
+		Op:   "open",
+		Path: "@badname",
+		Err:  errors.New("no such file or directory"),
+	}
+	if err.Error() != werr.Error() {
+		t.Errorf("err = %v, want %v", err, werr)
+	}
+}
+
+func TestNewTransportTLSInfo(t *testing.T) {
+	tmp, err := createTempFile([]byte("XXX"))
+	if err != nil {
+		t.Fatalf("Unable to prepare tmpfile: %v", err)
+	}
+	defer os.Remove(tmp)
+
+	tests := []TLSInfo{
+		TLSInfo{},
+		TLSInfo{
+			CertFile: tmp,
+			KeyFile:  tmp,
+		},
+		TLSInfo{
+			CertFile: tmp,
+			KeyFile:  tmp,
+			CAFile:   tmp,
+		},
+		TLSInfo{
+			CAFile: tmp,
+		},
+	}
+
+	for i, tt := range tests {
+		tt.parseFunc = fakeCertificateParserFunc(tls.Certificate{}, nil)
+		trans, err := NewTransport(tt)
+		if err != nil {
+			t.Fatalf("Received unexpected error from NewTransport: %v", err)
+		}
+
+		if trans.TLSClientConfig == nil {
+			t.Fatalf("#%d: want non-nil TLSClientConfig", i)
+		}
+	}
+}
+
+func TestTLSInfoEmpty(t *testing.T) {
+	tests := []struct {
+		info TLSInfo
+		want bool
+	}{
+		{TLSInfo{}, true},
+		{TLSInfo{CAFile: "baz"}, true},
+		{TLSInfo{CertFile: "foo"}, false},
+		{TLSInfo{KeyFile: "bar"}, false},
+		{TLSInfo{CertFile: "foo", KeyFile: "bar"}, false},
+		{TLSInfo{CertFile: "foo", CAFile: "baz"}, false},
+		{TLSInfo{KeyFile: "bar", CAFile: "baz"}, false},
+		{TLSInfo{CertFile: "foo", KeyFile: "bar", CAFile: "baz"}, false},
+	}
+
+	for i, tt := range tests {
+		got := tt.info.Empty()
+		if tt.want != got {
+			t.Errorf("#%d: result of Empty() incorrect: want=%t got=%t", i, tt.want, got)
+		}
+	}
+}
+
+func TestTLSInfoMissingFields(t *testing.T) {
+	tmp, err := createTempFile([]byte("XXX"))
+	if err != nil {
+		t.Fatalf("Unable to prepare tmpfile: %v", err)
+	}
+	defer os.Remove(tmp)
+
+	tests := []TLSInfo{
+		TLSInfo{CertFile: tmp},
+		TLSInfo{KeyFile: tmp},
+		TLSInfo{CertFile: tmp, CAFile: tmp},
+		TLSInfo{KeyFile: tmp, CAFile: tmp},
+	}
+
+	for i, info := range tests {
+		if _, err := info.ServerConfig(); err == nil {
+			t.Errorf("#%d: expected non-nil error from ServerConfig()", i)
+		}
+
+		if _, err = info.ClientConfig(); err == nil {
+			t.Errorf("#%d: expected non-nil error from ClientConfig()", i)
+		}
+	}
+}
+
+func TestTLSInfoParseFuncError(t *testing.T) {
+	tmp, err := createTempFile([]byte("XXX"))
+	if err != nil {
+		t.Fatalf("Unable to prepare tmpfile: %v", err)
+	}
+	defer os.Remove(tmp)
+
+	info := TLSInfo{CertFile: tmp, KeyFile: tmp, CAFile: tmp}
+	info.parseFunc = fakeCertificateParserFunc(tls.Certificate{}, errors.New("fake"))
+
+	if _, err := info.ServerConfig(); err == nil {
+		t.Errorf("expected non-nil error from ServerConfig()")
+	}
+
+	if _, err = info.ClientConfig(); err == nil {
+		t.Errorf("expected non-nil error from ClientConfig()")
+	}
+}
+
+func TestTLSInfoConfigFuncs(t *testing.T) {
+	tmp, err := createTempFile([]byte("XXX"))
+	if err != nil {
+		t.Fatalf("Unable to prepare tmpfile: %v", err)
+	}
+	defer os.Remove(tmp)
+
+	tests := []struct {
+		info       TLSInfo
+		clientAuth tls.ClientAuthType
+		wantCAs    bool
+	}{
+		{
+			info:       TLSInfo{CertFile: tmp, KeyFile: tmp},
+			clientAuth: tls.NoClientCert,
+			wantCAs:    false,
+		},
+
+		{
+			info:       TLSInfo{CertFile: tmp, KeyFile: tmp, CAFile: tmp},
+			clientAuth: tls.RequireAndVerifyClientCert,
+			wantCAs:    true,
+		},
+	}
+
+	for i, tt := range tests {
+		tt.info.parseFunc = fakeCertificateParserFunc(tls.Certificate{}, nil)
+
+		sCfg, err := tt.info.ServerConfig()
+		if err != nil {
+			t.Errorf("#%d: expected nil error from ServerConfig(), got non-nil: %v", i, err)
+		}
+
+		if tt.wantCAs != (sCfg.ClientCAs != nil) {
+			t.Errorf("#%d: wantCAs=%t but ClientCAs=%v", i, tt.wantCAs, sCfg.ClientCAs)
+		}
+
+		cCfg, err := tt.info.ClientConfig()
+		if err != nil {
+			t.Errorf("#%d: expected nil error from ClientConfig(), got non-nil: %v", i, err)
+		}
+
+		if tt.wantCAs != (cCfg.RootCAs != nil) {
+			t.Errorf("#%d: wantCAs=%t but RootCAs=%v", i, tt.wantCAs, sCfg.RootCAs)
+		}
+	}
+}

+ 44 - 0
Godeps/_workspace/src/github.com/coreos/etcd/pkg/transport/timeout_conn.go

@@ -0,0 +1,44 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+	"net"
+	"time"
+)
+
+type timeoutConn struct {
+	net.Conn
+	wtimeoutd  time.Duration
+	rdtimeoutd time.Duration
+}
+
+func (c timeoutConn) Write(b []byte) (n int, err error) {
+	if c.wtimeoutd > 0 {
+		if err := c.SetWriteDeadline(time.Now().Add(c.wtimeoutd)); err != nil {
+			return 0, err
+		}
+	}
+	return c.Conn.Write(b)
+}
+
+func (c timeoutConn) Read(b []byte) (n int, err error) {
+	if c.rdtimeoutd > 0 {
+		if err := c.SetReadDeadline(time.Now().Add(c.rdtimeoutd)); err != nil {
+			return 0, err
+		}
+	}
+	return c.Conn.Read(b)
+}

+ 36 - 0
Godeps/_workspace/src/github.com/coreos/etcd/pkg/transport/timeout_dialer.go

@@ -0,0 +1,36 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+	"net"
+	"time"
+)
+
+type rwTimeoutDialer struct {
+	wtimeoutd  time.Duration
+	rdtimeoutd time.Duration
+	net.Dialer
+}
+
+func (d *rwTimeoutDialer) Dial(network, address string) (net.Conn, error) {
+	conn, err := d.Dialer.Dial(network, address)
+	tconn := &timeoutConn{
+		rdtimeoutd: d.rdtimeoutd,
+		wtimeoutd:  d.wtimeoutd,
+		Conn:       conn,
+	}
+	return tconn, err
+}

+ 87 - 0
Godeps/_workspace/src/github.com/coreos/etcd/pkg/transport/timeout_dialer_test.go

@@ -0,0 +1,87 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+	"net"
+	"testing"
+	"time"
+)
+
+func TestReadWriteTimeoutDialer(t *testing.T) {
+	stop := make(chan struct{})
+
+	ln, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		t.Fatalf("unexpected listen error: %v", err)
+	}
+	ts := testBlockingServer{ln, 2, stop}
+	go ts.Start(t)
+
+	d := rwTimeoutDialer{
+		wtimeoutd:  10 * time.Millisecond,
+		rdtimeoutd: 10 * time.Millisecond,
+	}
+	conn, err := d.Dial("tcp", ln.Addr().String())
+	if err != nil {
+		t.Fatalf("unexpected dial error: %v", err)
+	}
+	defer conn.Close()
+
+	// fill the socket buffer
+	data := make([]byte, 5*1024*1024)
+	timer := time.AfterFunc(d.wtimeoutd*5, func() {
+		t.Fatal("wait timeout")
+	})
+	defer timer.Stop()
+
+	_, err = conn.Write(data)
+	if operr, ok := err.(*net.OpError); !ok || operr.Op != "write" || !operr.Timeout() {
+		t.Errorf("err = %v, want write i/o timeout error", err)
+	}
+
+	timer.Reset(d.rdtimeoutd * 5)
+
+	conn, err = d.Dial("tcp", ln.Addr().String())
+	if err != nil {
+		t.Fatalf("unexpected dial error: %v", err)
+	}
+	defer conn.Close()
+
+	buf := make([]byte, 10)
+	_, err = conn.Read(buf)
+	if operr, ok := err.(*net.OpError); !ok || operr.Op != "read" || !operr.Timeout() {
+		t.Errorf("err = %v, want write i/o timeout error", err)
+	}
+
+	stop <- struct{}{}
+}
+
+type testBlockingServer struct {
+	ln   net.Listener
+	n    int
+	stop chan struct{}
+}
+
+func (ts *testBlockingServer) Start(t *testing.T) {
+	for i := 0; i < ts.n; i++ {
+		conn, err := ts.ln.Accept()
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer conn.Close()
+	}
+	<-ts.stop
+}

+ 53 - 0
Godeps/_workspace/src/github.com/coreos/etcd/pkg/transport/timeout_listener.go

@@ -0,0 +1,53 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+	"net"
+	"time"
+)
+
+// NewTimeoutListener returns a listener that listens on the given address.
+// If read/write on the accepted connection blocks longer than its time limit,
+// it will return timeout error.
+func NewTimeoutListener(addr string, scheme string, info TLSInfo, rdtimeoutd, wtimeoutd time.Duration) (net.Listener, error) {
+	ln, err := NewListener(addr, scheme, info)
+	if err != nil {
+		return nil, err
+	}
+	return &rwTimeoutListener{
+		Listener:   ln,
+		rdtimeoutd: rdtimeoutd,
+		wtimeoutd:  wtimeoutd,
+	}, nil
+}
+
+type rwTimeoutListener struct {
+	net.Listener
+	wtimeoutd  time.Duration
+	rdtimeoutd time.Duration
+}
+
+func (rwln *rwTimeoutListener) Accept() (net.Conn, error) {
+	c, err := rwln.Listener.Accept()
+	if err != nil {
+		return nil, err
+	}
+	return timeoutConn{
+		Conn:       c,
+		wtimeoutd:  rwln.wtimeoutd,
+		rdtimeoutd: rwln.rdtimeoutd,
+	}, nil
+}

+ 95 - 0
Godeps/_workspace/src/github.com/coreos/etcd/pkg/transport/timeout_listener_test.go

@@ -0,0 +1,95 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+	"net"
+	"testing"
+	"time"
+)
+
+// TestNewTimeoutListener tests that NewTimeoutListener returns a
+// rwTimeoutListener struct with timeouts set.
+func TestNewTimeoutListener(t *testing.T) {
+	l, err := NewTimeoutListener("127.0.0.1:0", "http", TLSInfo{}, time.Hour, time.Hour)
+	if err != nil {
+		t.Fatalf("unexpected NewTimeoutListener error: %v", err)
+	}
+	defer l.Close()
+	tln := l.(*rwTimeoutListener)
+	if tln.rdtimeoutd != time.Hour {
+		t.Errorf("read timeout = %s, want %s", tln.rdtimeoutd, time.Hour)
+	}
+	if tln.wtimeoutd != time.Hour {
+		t.Errorf("write timeout = %s, want %s", tln.wtimeoutd, time.Hour)
+	}
+}
+
+func TestWriteReadTimeoutListener(t *testing.T) {
+	ln, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		t.Fatalf("unexpected listen error: %v", err)
+	}
+	wln := rwTimeoutListener{
+		Listener:   ln,
+		wtimeoutd:  10 * time.Millisecond,
+		rdtimeoutd: 10 * time.Millisecond,
+	}
+	stop := make(chan struct{})
+
+	blocker := func() {
+		conn, err := net.Dial("tcp", ln.Addr().String())
+		if err != nil {
+			t.Fatalf("unexpected dail error: %v", err)
+		}
+		defer conn.Close()
+		// block the receiver until the writer timeout
+		<-stop
+	}
+	go blocker()
+
+	conn, err := wln.Accept()
+	if err != nil {
+		t.Fatalf("unexpected accept error: %v", err)
+	}
+	defer conn.Close()
+
+	// fill the socket buffer
+	data := make([]byte, 5*1024*1024)
+	timer := time.AfterFunc(wln.wtimeoutd*5, func() {
+		t.Fatal("wait timeout")
+	})
+	defer timer.Stop()
+
+	_, err = conn.Write(data)
+	if operr, ok := err.(*net.OpError); !ok || operr.Op != "write" || !operr.Timeout() {
+		t.Errorf("err = %v, want write i/o timeout error", err)
+	}
+	stop <- struct{}{}
+
+	timer.Reset(wln.rdtimeoutd * 5)
+	go blocker()
+
+	conn, err = wln.Accept()
+	if err != nil {
+		t.Fatalf("unexpected accept error: %v", err)
+	}
+	buf := make([]byte, 10)
+	_, err = conn.Read(buf)
+	if operr, ok := err.(*net.OpError); !ok || operr.Op != "read" || !operr.Timeout() {
+		t.Errorf("err = %v, want write i/o timeout error", err)
+	}
+	stop <- struct{}{}
+}

+ 43 - 0
Godeps/_workspace/src/github.com/coreos/etcd/pkg/transport/timeout_transport.go

@@ -0,0 +1,43 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+	"net"
+	"net/http"
+	"time"
+)
+
+// NewTimeoutTransport returns a transport created using the given TLS info.
+// If read/write on the created connection blocks longer than its time limit,
+// it will return timeout error.
+func NewTimeoutTransport(info TLSInfo, dialtimeoutd, rdtimeoutd, wtimeoutd time.Duration) (*http.Transport, error) {
+	tr, err := NewTransport(info)
+	if err != nil {
+		return nil, err
+	}
+	// the timeouted connection will tiemout soon after it is idle.
+	// it should not be put back to http transport as an idle connection for future usage.
+	tr.MaxIdleConnsPerHost = -1
+	tr.Dial = (&rwTimeoutDialer{
+		Dialer: net.Dialer{
+			Timeout:   dialtimeoutd,
+			KeepAlive: 30 * time.Second,
+		},
+		rdtimeoutd: rdtimeoutd,
+		wtimeoutd:  wtimeoutd,
+	}).Dial
+	return tr, nil
+}

+ 85 - 0
Godeps/_workspace/src/github.com/coreos/etcd/pkg/transport/timeout_transport_test.go

@@ -0,0 +1,85 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+	"bytes"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+	"time"
+)
+
+// TestNewTimeoutTransport tests that NewTimeoutTransport returns a transport
+// that can dial out timeout connections.
+func TestNewTimeoutTransport(t *testing.T) {
+	tr, err := NewTimeoutTransport(TLSInfo{}, time.Hour, time.Hour, time.Hour)
+	if err != nil {
+		t.Fatalf("unexpected NewTimeoutTransport error: %v", err)
+	}
+
+	remoteAddr := func(w http.ResponseWriter, r *http.Request) {
+		w.Write([]byte(r.RemoteAddr))
+	}
+	srv := httptest.NewServer(http.HandlerFunc(remoteAddr))
+
+	defer srv.Close()
+	conn, err := tr.Dial("tcp", srv.Listener.Addr().String())
+	if err != nil {
+		t.Fatalf("unexpected dial error: %v", err)
+	}
+	defer conn.Close()
+
+	tconn, ok := conn.(*timeoutConn)
+	if !ok {
+		t.Fatalf("failed to dial out *timeoutConn")
+	}
+	if tconn.rdtimeoutd != time.Hour {
+		t.Errorf("read timeout = %s, want %s", tconn.rdtimeoutd, time.Hour)
+	}
+	if tconn.wtimeoutd != time.Hour {
+		t.Errorf("write timeout = %s, want %s", tconn.wtimeoutd, time.Hour)
+	}
+
+	// ensure not reuse timeout connection
+	req, err := http.NewRequest("GET", srv.URL, nil)
+	if err != nil {
+		t.Fatalf("unexpected err %v", err)
+	}
+	resp, err := tr.RoundTrip(req)
+	if err != nil {
+		t.Fatalf("unexpected err %v", err)
+	}
+	addr0, err := ioutil.ReadAll(resp.Body)
+	resp.Body.Close()
+	if err != nil {
+		t.Fatalf("unexpected err %v", err)
+	}
+
+	resp, err = tr.RoundTrip(req)
+	if err != nil {
+		t.Fatalf("unexpected err %v", err)
+	}
+	addr1, err := ioutil.ReadAll(resp.Body)
+	resp.Body.Close()
+	if err != nil {
+		t.Fatalf("unexpected err %v", err)
+	}
+
+	if bytes.Equal(addr0, addr1) {
+		t.Errorf("addr0 = %s addr1= %s, want not equal", string(addr0), string(addr1))
+	}
+}

+ 4 - 19
README.md

@@ -133,25 +133,7 @@ After flannel has acquired the subnet and configured backend, it will write out
 
 ## Client/Server mode (EXPERIMENTAL)
 
-By default flannel runs without a central controller, utilizing etcd for coordination.
-However, it can also be configured to run in client/server mode, where a special instance of the flannel daemon (the server) is the only one that communicates with etcd.
-This setup offers the advantange of having only a single server directly connecting to etcd, with the rest of the flannel daemons (clients) accessing etcd via the server.
-The server is completely stateless and does not assume that it has exclusive access to the etcd keyspace.
-In the future this will be exploited to provide failover; currently, however, the clients accept only a single endpoint to which to connect.
-The stateless server also makes it possible to run some nodes in client mode side-by-side with those connecting to etcd directly.
-
-To run the flannel daemon in server mode, simply provide the `--listen` flag:
-```
-$ flanneld --listen=0.0.0.0:8888
-```
-
-To run the flannel daemon in client mode, use the `--remote` flag to point it to a flannel server instance:
-```
-$ flanneld --remote=10.0.0.3:8888
-```
-
-It is important to note that the server itself does not join the flannel network (i.e. it won't assign itself a subnet) -- it just satisfies requests from the clients.
-As such, if the host running the flannel server also needs to participate in the overlay, it should start two instances of flannel - one in client mode and one in server mode.
+Please see [Documentation/client-server.md](https://github.com/coreos/flannel/tree/master/Documentation/client-server.md).
 
 ## Multi-network mode (EXPERIMENTAL)
 
@@ -203,6 +185,9 @@ $ flanneld --remote=10.0.0.3:8888 --networks=blue,green
 --ip-masq=false: setup IP masquerade for traffic destined for outside the flannel network.
 --listen="": if specified, will run in server mode. Value is IP and port (e.g. `0.0.0.0:8888`) to listen on or `fd://` for [socket activation](http://www.freedesktop.org/software/systemd/man/systemd.socket.html).
 --remote="": if specified, will run in client mode. Value is IP and port of the server.
+--remote-keyfile="": SSL key file used to secure client/server communication.
+--remote-certfile="": SSL certification file used to secure client/server communication.
+--remote-cafile="": SSL Certificate Authority file used to secure client/server communication.
 --networks="": if specified, will run in multi-network mode. Value is comma separate list of networks to join.
 -v=0: log level for V logs. Set to 1 to see messages related to data path.
 --version: print version and exit

+ 22 - 16
main.go

@@ -36,20 +36,23 @@ import (
 )
 
 type CmdLineOpts struct {
-	etcdEndpoints string
-	etcdPrefix    string
-	etcdKeyfile   string
-	etcdCertfile  string
-	etcdCAFile    string
-	help          bool
-	version       bool
-	ipMasq        bool
-	subnetFile    string
-	subnetDir     string
-	iface         string
-	listen        string
-	remote        string
-	networks      string
+	etcdEndpoints  string
+	etcdPrefix     string
+	etcdKeyfile    string
+	etcdCertfile   string
+	etcdCAFile     string
+	help           bool
+	version        bool
+	ipMasq         bool
+	subnetFile     string
+	subnetDir      string
+	iface          string
+	listen         string
+	remote         string
+	remoteKeyfile  string
+	remoteCertfile string
+	remoteCAFile   string
+	networks       string
 }
 
 var opts CmdLineOpts
@@ -65,6 +68,9 @@ func init() {
 	flag.StringVar(&opts.iface, "iface", "", "interface to use (IP or name) for inter-host communication")
 	flag.StringVar(&opts.listen, "listen", "", "run as server and listen on specified address (e.g. ':8080')")
 	flag.StringVar(&opts.remote, "remote", "", "run as client and connect to server on specified address (e.g. '10.1.2.3:8080')")
+	flag.StringVar(&opts.remoteKeyfile, "remote-keyfile", "", "SSL key file used to secure client/server communication")
+	flag.StringVar(&opts.remoteCertfile, "remote-certfile", "", "SSL certification file used to secure client/server communication")
+	flag.StringVar(&opts.remoteCAFile, "remote-cafile", "", "SSL Certificate Authority file used to secure client/server communication")
 	flag.StringVar(&opts.networks, "networks", "", "run in multi-network mode and service the specified networks")
 	flag.BoolVar(&opts.ipMasq, "ip-masq", false, "setup IP masquerade rule for traffic destined outside of overlay network")
 	flag.BoolVar(&opts.help, "help", false, "print this message")
@@ -160,7 +166,7 @@ func isMultiNetwork() bool {
 
 func newSubnetManager() (subnet.Manager, error) {
 	if opts.remote != "" {
-		return remote.NewRemoteManager(opts.remote), nil
+		return remote.NewRemoteManager(opts.remote, opts.remoteCAFile, opts.remoteCertfile, opts.remoteKeyfile)
 	}
 
 	cfg := &subnet.EtcdConfig{
@@ -259,7 +265,7 @@ func main() {
 		}
 		log.Info("running as server")
 		runFunc = func(ctx context.Context) {
-			remote.RunServer(ctx, sm, opts.listen)
+			remote.RunServer(ctx, sm, opts.listen, opts.remoteCAFile, opts.remoteCertfile, opts.remoteKeyfile)
 		}
 	} else {
 		networks := strings.Split(opts.networks, ",")

+ 37 - 15
remote/client.go

@@ -22,6 +22,7 @@ import (
 	"net/http"
 	"path"
 
+	"github.com/coreos/flannel/Godeps/_workspace/src/github.com/coreos/etcd/pkg/transport"
 	"github.com/coreos/flannel/Godeps/_workspace/src/golang.org/x/net/context"
 
 	"github.com/coreos/flannel/subnet"
@@ -29,11 +30,33 @@ import (
 
 // implements subnet.Manager by sending requests to the server
 type RemoteManager struct {
-	base string // includes scheme, host, and port, and version
+	base      string // includes scheme, host, and port, and version
+	transport *http.Transport
 }
 
-func NewRemoteManager(listenAddr string) subnet.Manager {
-	return &RemoteManager{base: "http://" + listenAddr + "/v1"}
+func NewRemoteManager(listenAddr, cafile, certfile, keyfile string) (subnet.Manager, error) {
+	tls := transport.TLSInfo{
+		CAFile:   cafile,
+		CertFile: certfile,
+		KeyFile:  keyfile,
+	}
+
+	t, err := transport.NewTransport(tls)
+	if err != nil {
+		return nil, err
+	}
+
+	var scheme string
+	if tls.Empty() && tls.CAFile == "" {
+		scheme = "http://"
+	} else {
+		scheme = "https://"
+	}
+
+	return &RemoteManager{
+		base:      scheme + listenAddr + "/v1",
+		transport: t,
+	}, nil
 }
 
 func (m *RemoteManager) mkurl(network string, parts ...string) string {
@@ -49,7 +72,7 @@ func (m *RemoteManager) mkurl(network string, parts ...string) string {
 func (m *RemoteManager) GetNetworkConfig(ctx context.Context, network string) (*subnet.Config, error) {
 	url := m.mkurl(network, "config")
 
-	resp, err := httpGet(ctx, url)
+	resp, err := m.httpGet(ctx, url)
 	if err != nil {
 		return nil, err
 	}
@@ -75,7 +98,7 @@ func (m *RemoteManager) AcquireLease(ctx context.Context, network string, attrs
 		return nil, err
 	}
 
-	resp, err := httpPutPost(ctx, "POST", url, "application/json", body)
+	resp, err := m.httpPutPost(ctx, "POST", url, "application/json", body)
 	if err != nil {
 		return nil, err
 	}
@@ -101,7 +124,7 @@ func (m *RemoteManager) RenewLease(ctx context.Context, network string, lease *s
 		return err
 	}
 
-	resp, err := httpPutPost(ctx, "PUT", url, "application/json", body)
+	resp, err := m.httpPutPost(ctx, "PUT", url, "application/json", body)
 	if err != nil {
 		return err
 	}
@@ -132,7 +155,7 @@ func (m *RemoteManager) WatchLeases(ctx context.Context, network string, cursor
 		url = fmt.Sprintf("%v?next=%v", url, c)
 	}
 
-	resp, err := httpGet(ctx, url)
+	resp, err := m.httpGet(ctx, url)
 	if err != nil {
 		return subnet.WatchResult{}, err
 	}
@@ -165,11 +188,10 @@ type httpRespErr struct {
 	err  error
 }
 
-func httpDo(ctx context.Context, req *http.Request) (*http.Response, error) {
+func (m *RemoteManager) httpDo(ctx context.Context, req *http.Request) (*http.Response, error) {
 	// Run the HTTP request in a goroutine (so it can be canceled) and pass
 	// the result via the channel c
-	tr := &http.Transport{}
-	client := &http.Client{Transport: tr}
+	client := &http.Client{Transport: m.transport}
 	c := make(chan httpRespErr, 1)
 	go func() {
 		resp, err := client.Do(req)
@@ -178,7 +200,7 @@ func httpDo(ctx context.Context, req *http.Request) (*http.Response, error) {
 
 	select {
 	case <-ctx.Done():
-		tr.CancelRequest(req)
+		m.transport.CancelRequest(req)
 		<-c // Wait for f to return.
 		return nil, ctx.Err()
 	case r := <-c:
@@ -186,20 +208,20 @@ func httpDo(ctx context.Context, req *http.Request) (*http.Response, error) {
 	}
 }
 
-func httpGet(ctx context.Context, url string) (*http.Response, error) {
+func (m *RemoteManager) httpGet(ctx context.Context, url string) (*http.Response, error) {
 	req, err := http.NewRequest("GET", url, nil)
 	if err != nil {
 		return nil, err
 	}
 
-	return httpDo(ctx, req)
+	return m.httpDo(ctx, req)
 }
 
-func httpPutPost(ctx context.Context, method, url, contentType string, body []byte) (*http.Response, error) {
+func (m *RemoteManager) httpPutPost(ctx context.Context, method, url, contentType string, body []byte) (*http.Response, error) {
 	req, err := http.NewRequest(method, url, bytes.NewBuffer(body))
 	if err != nil {
 		return nil, err
 	}
 	req.Header.Set("Content-Type", contentType)
-	return httpDo(ctx, req)
+	return m.httpDo(ctx, req)
 }

+ 5 - 2
remote/remote_test.go

@@ -41,7 +41,7 @@ func TestRemote(t *testing.T) {
 	wg := sync.WaitGroup{}
 	wg.Add(1)
 	go func() {
-		RunServer(ctx, sm, addr)
+		RunServer(ctx, sm, addr, "", "", "")
 		wg.Done()
 	}()
 
@@ -77,7 +77,10 @@ func isConnRefused(err error) bool {
 }
 
 func doTestRemote(ctx context.Context, t *testing.T, remoteAddr string) {
-	sm := NewRemoteManager(remoteAddr)
+	sm, err := NewRemoteManager(remoteAddr, "", "", "")
+	if err != nil {
+		t.Fatalf("Failed to create remote mananager: %v", err)
+	}
 
 	for i := 0; ; i++ {
 		cfg, err := sm.GetNetworkConfig(ctx, "_")

+ 31 - 5
remote/server.go

@@ -15,6 +15,7 @@
 package remote
 
 import (
+	"crypto/tls"
 	"encoding/json"
 	"fmt"
 	"net"
@@ -23,6 +24,7 @@ import (
 	"regexp"
 	"strconv"
 
+	"github.com/coreos/flannel/Godeps/_workspace/src/github.com/coreos/etcd/pkg/transport"
 	"github.com/coreos/flannel/Godeps/_workspace/src/github.com/coreos/go-systemd/activation"
 	log "github.com/coreos/flannel/Godeps/_workspace/src/github.com/golang/glog"
 	"github.com/coreos/flannel/Godeps/_workspace/src/github.com/gorilla/mux"
@@ -182,26 +184,50 @@ func fdListener(addr string) (net.Listener, error) {
 	return listeners[fdOffset], nil
 }
 
-func listener(addr string) (net.Listener, error) {
+func listener(addr, cafile, certfile, keyfile string) (net.Listener, error) {
 	rex := regexp.MustCompile("(?:([a-z]+)://)?(.*)")
 	groups := rex.FindStringSubmatch(addr)
 
+	var l net.Listener
+	var err error
+
 	switch {
 	case groups == nil:
 		return nil, fmt.Errorf("bad listener address")
 
 	case groups[1] == "", groups[1] == "tcp":
-		return net.Listen("tcp", groups[2])
+		if l, err = net.Listen("tcp", groups[2]); err != nil {
+			return nil, err
+		}
 
 	case groups[1] == "fd":
-		return fdListener(groups[2])
+		if l, err = fdListener(groups[2]); err != nil {
+			return nil, err
+		}
 
 	default:
 		return nil, fmt.Errorf("bad listener scheme")
 	}
+
+	tlsinfo := transport.TLSInfo{
+		CAFile:   cafile,
+		CertFile: certfile,
+		KeyFile:  keyfile,
+	}
+
+	if !tlsinfo.Empty() {
+		cfg, err := tlsinfo.ServerConfig()
+		if err != nil {
+			return nil, err
+		}
+
+		l = tls.NewListener(l, cfg)
+	}
+
+	return l, nil
 }
 
-func RunServer(ctx context.Context, sm subnet.Manager, listenAddr string) {
+func RunServer(ctx context.Context, sm subnet.Manager, listenAddr, cafile, certfile, keyfile string) {
 	// {network} is always required a the API level but to
 	// keep backward compat, special "_" network is allowed
 	// that means "no network"
@@ -212,7 +238,7 @@ func RunServer(ctx context.Context, sm subnet.Manager, listenAddr string) {
 	r.HandleFunc("/v1/{network}/leases/{subnet}", bindHandler(handleRenewLease, ctx, sm)).Methods("PUT")
 	r.HandleFunc("/v1/{network}/leases", bindHandler(handleWatchLeases, ctx, sm)).Methods("GET")
 
-	l, err := listener(listenAddr)
+	l, err := listener(listenAddr, cafile, certfile, keyfile)
 	if err != nil {
 		log.Errorf("Error listening on %v: %v", listenAddr, err)
 		return