Browse Source

Merge pull request #244 from eyakubovich/remote-tls

Add TLS support for client/server communication
Eugene Yakubovich 9 years ago
parent
commit
8da2c7c853

+ 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