diff --git a/.semaphoreci/setup.sh b/.semaphoreci/setup.sh index e8c6e1e2e..2989e9143 100755 --- a/.semaphoreci/setup.sh +++ b/.semaphoreci/setup.sh @@ -2,7 +2,7 @@ set -e sudo -E apt-get -yq update -sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install docker-engine=${DOCKER_VERSION}* +sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install docker-ce=${DOCKER_VERSION}* docker version pip install --user -r requirements.txt diff --git a/.semaphoreci/vars b/.semaphoreci/vars index bf12b9b0a..d64644d1a 100644 --- a/.semaphoreci/vars +++ b/.semaphoreci/vars @@ -3,7 +3,7 @@ set -e export REPO='containous/traefik' -export DOCKER_VERSION=1.12.6 +export DOCKER_VERSION=17.03.0 if VERSION=$(git describe --exact-match --abbrev=0 --tags); then diff --git a/acme/acme.go b/acme/acme.go index 1d358adcb..4a8317113 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -320,7 +320,6 @@ func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, checkOnDemandDomain func for range ticker.C { a.renewCertificates() } - }) return nil } @@ -328,14 +327,11 @@ func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, checkOnDemandDomain func func (a *ACME) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { domain := types.CanonicalDomain(clientHello.ServerName) account := a.store.Get().(*Account) - //use regex to test for wildcard certs that might have been added into TLSConfig - for k := range a.TLSConfig.NameToCertificate { - selector := "^" + strings.Replace(k, "*.", "[^\\.]*\\.?", -1) + "$" - match, _ := regexp.MatchString(selector, domain) - if match { - return a.TLSConfig.NameToCertificate[k], nil - } + + if providedCertificate := a.getProvidedCertificate([]string{domain}); providedCertificate != nil { + return providedCertificate, nil } + if challengeCert, ok := a.challengeProvider.getCertificate(domain); ok { log.Debugf("ACME got challenge %s", domain) return challengeCert, nil @@ -520,11 +516,23 @@ func (a *ACME) loadCertificateOnDemand(clientHello *tls.ClientHelloInfo) (*tls.C // LoadCertificateForDomains loads certificates from ACME for given domains func (a *ACME) LoadCertificateForDomains(domains []string) { a.jobs.In() <- func() { - log.Debugf("LoadCertificateForDomains %s...", domains) + log.Debugf("LoadCertificateForDomains %v...", domains) + + if len(domains) == 0 { + // no domain + return + } + domains = fun.Map(types.CanonicalDomain, domains).([]string) + + // Check provided certificates + if a.getProvidedCertificate(domains) != nil { + return + } + operation := func() error { if a.client == nil { - return fmt.Errorf("ACME client still not built") + return errors.New("ACME client still not built") } return nil } @@ -540,11 +548,7 @@ func (a *ACME) LoadCertificateForDomains(domains []string) { } account := a.store.Get().(*Account) var domain Domain - if len(domains) == 0 { - // no domain - return - - } else if len(domains) > 1 { + if len(domains) > 1 { domain = Domain{Main: domains[0], SANs: domains[1:]} } else { domain = Domain{Main: domains[0]} @@ -578,6 +582,29 @@ func (a *ACME) LoadCertificateForDomains(domains []string) { } } +// Get provided certificate which check a domains list (Main and SANs) +func (a *ACME) getProvidedCertificate(domains []string) *tls.Certificate { + // Use regex to test for provided certs that might have been added into TLSConfig + providedCertMatch := false + log.Debugf("Look for provided certificate to validate %s...", domains) + for k := range a.TLSConfig.NameToCertificate { + selector := "^" + strings.Replace(k, "*.", "[^\\.]*\\.?", -1) + "$" + for _, domainToCheck := range domains { + providedCertMatch, _ = regexp.MatchString(selector, domainToCheck) + if !providedCertMatch { + break + } + } + if providedCertMatch { + log.Debugf("Got provided certificate for domains %s", domains) + return a.TLSConfig.NameToCertificate[k] + + } + } + log.Debugf("No provided certificate found for domains %s, get ACME certificate.", domains) + return nil +} + func (a *ACME) getDomainsCertificates(domains []string) (*Certificate, error) { domains = fun.Map(types.CanonicalDomain, domains).([]string) log.Debugf("Loading ACME certificates %s...", domains) diff --git a/acme/acme_test.go b/acme/acme_test.go index fe435bf37..ff2ad970c 100644 --- a/acme/acme_test.go +++ b/acme/acme_test.go @@ -1,6 +1,7 @@ package acme import ( + "crypto/tls" "encoding/base64" "net/http" "net/http/httptest" @@ -9,6 +10,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/xenolf/lego/acme" ) @@ -277,3 +279,18 @@ cijFkALeQp/qyeXdFld2v9gUN3eCgljgcl0QweRoIc=---`) t.Error("No change to acme.PreCheckDNS when meant to be adding enforcing override function.") } } + +func TestAcme_getProvidedCertificate(t *testing.T) { + mm := make(map[string]*tls.Certificate) + mm["*.containo.us"] = &tls.Certificate{} + mm["traefik.acme.io"] = &tls.Certificate{} + + a := ACME{TLSConfig: &tls.Config{NameToCertificate: mm}} + + domains := []string{"traefik.containo.us", "trae.containo.us"} + certificate := a.getProvidedCertificate(domains) + assert.NotNil(t, certificate) + domains = []string{"traefik.acme.io", "trae.acme.io"} + certificate = a.getProvidedCertificate(domains) + assert.Nil(t, certificate) +} diff --git a/build.Dockerfile b/build.Dockerfile index 28187b90a..ab34901da 100644 --- a/build.Dockerfile +++ b/build.Dockerfile @@ -28,7 +28,7 @@ RUN mkdir -p /usr/local/bin \ # Download docker RUN mkdir -p /usr/local/bin \ - && curl -fL https://get.docker.com/builds/Linux/x86_64/docker-${DOCKER_VERSION}.tgz \ + && curl -fL https://get.docker.com/builds/Linux/x86_64/docker-${DOCKER_VERSION}-ce.tgz \ | tar -xzC /usr/local/bin --transform 's#^.+/##x' WORKDIR /go/src/github.com/containous/traefik diff --git a/glide.lock b/glide.lock index 6ae926ca3..4b366e0e7 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 088194c8357ca08e27476866b9007adfa7711500fe0c78650ecb397c4f70075a -updated: 2017-05-19T23:30:19.890844996+02:00 +hash: 068db16642eb0249ce973711f4cf44206cc201214b1990e13f89e1ac3c402635 +updated: 2017-06-27T00:25:19.890844996+02:00 imports: - name: cloud.google.com/go version: 2e6a95edb1071d750f6d7db777bf66cd2997af6c @@ -409,7 +409,7 @@ imports: - name: github.com/vdemeester/docker-events version: be74d4929ec1ad118df54349fda4b0cba60f849b - name: github.com/vulcand/oxy - version: f88530866c561d24a6b5aac49f76d6351b788b9f + version: ad5bdb606fa9c64db267f0e43d63834908bdb05e repo: https://github.com/containous/oxy.git vcs: git subpackages: diff --git a/glide.yaml b/glide.yaml index 64fe24a5c..f56b535e7 100644 --- a/glide.yaml +++ b/glide.yaml @@ -9,7 +9,7 @@ import: - package: github.com/cenk/backoff - package: github.com/containous/flaeg - package: github.com/vulcand/oxy - version: f88530866c561d24a6b5aac49f76d6351b788b9f + version: ad5bdb606fa9c64db267f0e43d63834908bdb05e repo: https://github.com/containous/oxy.git vcs: git subpackages: diff --git a/integration/acme_test.go b/integration/acme_test.go index 0d4c02dc8..2a492a305 100644 --- a/integration/acme_test.go +++ b/integration/acme_test.go @@ -2,11 +2,12 @@ package main import ( "crypto/tls" + "fmt" "net/http" - "os" "time" "github.com/containous/traefik/integration/try" + "github.com/containous/traefik/testhelpers" "github.com/go-check/check" checker "github.com/vdemeester/shakers" ) @@ -17,6 +18,21 @@ type AcmeSuite struct { boulderIP string } +// Acme tests configuration +type AcmeTestCase struct { + onDemand bool + traefikConfFilePath string + domainToCheck string +} + +const ( + // Domain to check + acmeDomain = "traefik.acme.wtf" + + // Wildcard domain to check + wildcardDomain = "*.acme.wtf" +) + func (s *AcmeSuite) SetUpSuite(c *check.C) { s.createComposeProject(c, "boulder") s.composeProject.Start(c) @@ -35,11 +51,58 @@ func (s *AcmeSuite) TearDownSuite(c *check.C) { } } -func (s *AcmeSuite) TestRetrieveAcmeCertificate(c *check.C) { - file := s.adaptFile(c, "fixtures/acme/acme.toml", struct{ BoulderHost string }{s.boulderIP}) - defer os.Remove(file) - cmd, output := s.cmdTraefikWithConfigFile(file) +// Test OnDemand option with none provided certificate +func (s *AcmeSuite) TestOnDemandRetrieveAcmeCertificate(c *check.C) { + testCase := AcmeTestCase{ + traefikConfFilePath: "fixtures/acme/acme.toml", + onDemand: true, + domainToCheck: acmeDomain} + s.retrieveAcmeCertificate(c, testCase) +} + +// Test OnHostRule option with none provided certificate +func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificate(c *check.C) { + testCase := AcmeTestCase{ + traefikConfFilePath: "fixtures/acme/acme.toml", + onDemand: false, + domainToCheck: acmeDomain} + + s.retrieveAcmeCertificate(c, testCase) +} + +// Test OnDemand option with a wildcard provided certificate +func (s *AcmeSuite) TestOnDemandRetrieveAcmeCertificateWithWildcard(c *check.C) { + testCase := AcmeTestCase{ + traefikConfFilePath: "fixtures/acme/acme_provided.toml", + onDemand: true, + domainToCheck: wildcardDomain} + + s.retrieveAcmeCertificate(c, testCase) +} + +// Test onHostRule option with a wildcard provided certificate +func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificateWithWildcard(c *check.C) { + testCase := AcmeTestCase{ + traefikConfFilePath: "fixtures/acme/acme_provided.toml", + onDemand: false, + domainToCheck: wildcardDomain} + + s.retrieveAcmeCertificate(c, testCase) +} + +// Doing an HTTPS request and test the response certificate +func (s *AcmeSuite) retrieveAcmeCertificate(c *check.C, testCase AcmeTestCase) { + file := s.adaptFile(c, testCase.traefikConfFilePath, struct { + BoulderHost string + OnDemand, OnHostRule bool + }{ + BoulderHost: s.boulderIP, + OnDemand: testCase.onDemand, + OnHostRule: !testCase.onDemand, + }) + + cmd, output := s.cmdTraefikWithConfigFile(file) err := cmd.Start() c.Assert(err, checker.IsNil) defer cmd.Process.Kill() @@ -64,16 +127,39 @@ func (s *AcmeSuite) TestRetrieveAcmeCertificate(c *check.C) { tr = &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, - ServerName: "traefik.acme.wtf", + ServerName: acmeDomain, }, } client = &http.Client{Transport: tr} - req, _ := http.NewRequest("GET", "https://127.0.0.1:5001/", nil) - req.Host = "traefik.acme.wtf" - req.Header.Set("Host", "traefik.acme.wtf") + + req := testhelpers.MustNewRequest(http.MethodGet, "https://127.0.0.1:5001/", nil) + req.Host = acmeDomain + req.Header.Set("Host", acmeDomain) req.Header.Set("Accept", "*/*") - resp, err := client.Do(req) + + var resp *http.Response + + // Retry to send a Request which uses the LE generated certificate + err = try.Do(60*time.Second, func() error { + resp, err = client.Do(req) + + // /!\ If connection is not closed, SSLHandshake will only be done during the first trial /!\ + req.Close = true + + if err != nil { + return err + } + + cn := resp.TLS.PeerCertificates[0].Subject.CommonName + if cn != testCase.domainToCheck { + return fmt.Errorf("domain %s found in place of %s", cn, testCase.domainToCheck) + } + + return nil + }) + c.Assert(err, checker.IsNil) - // Expected a 200 c.Assert(resp.StatusCode, checker.Equals, http.StatusOK) + // Check Domain into response certificate + c.Assert(resp.TLS.PeerCertificates[0].Subject.CommonName, checker.Equals, testCase.domainToCheck) } diff --git a/integration/fixtures/acme/README.md b/integration/fixtures/acme/README.md new file mode 100644 index 000000000..e9eedd4c0 --- /dev/null +++ b/integration/fixtures/acme/README.md @@ -0,0 +1,37 @@ +# How to generate the self-signed wildcard certificate + +```bash +#!/usr/bin/env bash + +# Specify where we will install +# the wildcard certificate +SSL_DIR="./ssl" + +# Set the wildcarded domain +# we want to use +DOMAIN="*.acme.wtf" + +# A blank passphrase +PASSPHRASE="" + +# Set our CSR variables +SUBJ=" +C=FR +ST=MP +O= +localityName=Toulouse +commonName=$DOMAIN +organizationalUnitName=Traefik +emailAddress= +" + +# Create our SSL directory +# in case it doesn't exist +sudo mkdir -p "$SSL_DIR" + +# Generate our Private Key, CSR and Certificate +sudo openssl genrsa -out "$SSL_DIR/wildcard.key" 2048 +sudo openssl req -new -subj "$(echo -n "$SUBJ" | tr "\n" "/")" -key "$SSL_DIR/wildcard.key" -out "$SSL_DIR/wildcard.csr" -passin pass:$PASSPHRASE +sudo openssl x509 -req -days 3650 -in "$SSL_DIR/wildcard.csr" -signkey "$SSL_DIR/wildcard.key" -out "$SSL_DIR/wildcard.crt" +sudo rm -f "$SSL_DIR/wildcard.csr" +``` \ No newline at end of file diff --git a/integration/fixtures/acme/acme.toml b/integration/fixtures/acme/acme.toml index e1e637bf0..2c15e8636 100644 --- a/integration/fixtures/acme/acme.toml +++ b/integration/fixtures/acme/acme.toml @@ -14,7 +14,8 @@ defaultEntryPoints = ["http", "https"] email = "test@traefik.io" storage = "/dev/null" entryPoint = "https" -onDemand = true +onDemand = {{.OnDemand}} +OnHostRule = {{.OnHostRule}} caServer = "http://{{.BoulderHost}}:4000/directory" [file] diff --git a/integration/fixtures/acme/acme_provided.toml b/integration/fixtures/acme/acme_provided.toml new file mode 100644 index 000000000..dcd067df4 --- /dev/null +++ b/integration/fixtures/acme/acme_provided.toml @@ -0,0 +1,35 @@ +logLevel = "DEBUG" + +defaultEntryPoints = ["http", "https"] + +[entryPoints] + [entryPoints.http] + address = ":8080" + [entryPoints.https] + address = ":5001" + [entryPoints.https.tls] + [[entryPoints.https.tls.certificates]] + CertFile = "fixtures/acme/ssl/wildcard.crt" + KeyFile = "fixtures/acme/ssl/wildcard.key" + +[acme] +email = "test@traefik.io" +storage = "/dev/null" +entryPoint = "https" +onDemand = {{.OnDemand}} +OnHostRule = {{.OnHostRule}} +caServer = "http://{{.BoulderHost}}:4000/directory" + +[file] + +[backends] + [backends.backend] + [backends.backend.servers.server1] + url = "http://127.0.0.1:9010" + + +[frontends] + [frontends.frontend] + backend = "backend" + [frontends.frontend.routes.test] + rule = "Host:traefik.acme.wtf" diff --git a/integration/fixtures/acme/ssl/wildcard.crt b/integration/fixtures/acme/ssl/wildcard.crt new file mode 100644 index 000000000..8257703a9 --- /dev/null +++ b/integration/fixtures/acme/ssl/wildcard.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDJDCCAgwCCQCS90TE7NuTqzANBgkqhkiG9w0BAQsFADBUMQswCQYDVQQGEwJG +UjELMAkGA1UECAwCTVAxETAPBgNVBAcMCFRvdWxvdXNlMRMwEQYDVQQDDAoqLmFj +bWUud3RmMRAwDgYDVQQLDAdUcmFlZmlrMB4XDTE3MDYyMzE0NTE0MVoXDTI3MDYy +MTE0NTE0MVowVDELMAkGA1UEBhMCRlIxCzAJBgNVBAgMAk1QMREwDwYDVQQHDAhU +b3Vsb3VzZTETMBEGA1UEAwwKKi5hY21lLnd0ZjEQMA4GA1UECwwHVHJhZWZpazCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAODqsVCLhauFZPhPXqZDIKST +wqoJST+jO5O/WmA7oC4S6JlecRoNsHAXyddd3cQW3yZqB0ryOHrMOpMX0PPXf3jS +OOXoXA6xsq+RXlR4hDrBkOrj/LR/g62Eiuj2JVO2uy6tKJIetSB/Wzl6OgRkY/um +EXIc7zQS81/QKg+pg7Z4AYJht5J88nOFHJ3RspUMaH1vJ6LhH3MOUkgFj+I1OiqX +Tnkd7EDWbkYxAJa0xI2qbmY5VYv8dsIUN+IlPFDtBt87Fc2qv5dQkOz11FDYxWnz ++kxX6+MESLBaTvJjXvG+bzTfh9xCExFQFiN+Us0JuLX8HKQ4MqWL2IiVLsko2osC +AwEAATANBgkqhkiG9w0BAQsFAAOCAQEAl2jTX2yzUpiufrJ6WtZjKIAH8GF817hS +dWvt2eyLrBPvllMUj8zqCE5uNVUDVuXQvOhOyx+3zZzfcgfYqbTD8G8amNWcSiRA +vonoOn1p1pW2OonSi32h3qv5i4gCyh/6cBneYi03lkQ7uLCsJK9+dXTAvoKL6s23 +IXhZGS0Qkvs4vkORA2MX9tyJdyfCCaCx3GpPCGkKrKJ8ePTEvq1ZE2xdhERnV5pz +L1PRY2QthXXVjMz7AXw0gkHvAbtrKVKR1Tv4ZK34bFBh/kyGAjkcn0zdeFKITqTF +tCoXWEArmiRqGuXwbqU3mEA9Cv6aMM+0YX89K2InhOnBU80OWs0uMQ== +-----END CERTIFICATE----- diff --git a/integration/fixtures/acme/ssl/wildcard.key b/integration/fixtures/acme/ssl/wildcard.key new file mode 100644 index 000000000..fe8580b8d --- /dev/null +++ b/integration/fixtures/acme/ssl/wildcard.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA4OqxUIuFq4Vk+E9epkMgpJPCqglJP6M7k79aYDugLhLomV5x +Gg2wcBfJ113dxBbfJmoHSvI4esw6kxfQ89d/eNI45ehcDrGyr5FeVHiEOsGQ6uP8 +tH+DrYSK6PYlU7a7Lq0okh61IH9bOXo6BGRj+6YRchzvNBLzX9AqD6mDtngBgmG3 +knzyc4UcndGylQxofW8nouEfcw5SSAWP4jU6KpdOeR3sQNZuRjEAlrTEjapuZjlV +i/x2whQ34iU8UO0G3zsVzaq/l1CQ7PXUUNjFafP6TFfr4wRIsFpO8mNe8b5vNN+H +3EITEVAWI35SzQm4tfwcpDgypYvYiJUuySjaiwIDAQABAoIBAQCs9Ex9v4x+pQlL +2NzTxXLom6dp0dI92WwK5W696Zv3UhsDNRiMDFLNH73amxfZnizjAU2yWCkOZNX2 +Hq5TlDc11ZJjWRbRRdw+He8HzdUAybCCr+a3dgbv+6hGFGIHydCOyCEWm/50ivq/ +bDoI/pnT/ZQUyCM5TAlSeGSfvp7GRHi9v3HOl85H1Pn2Dvyk9gj4y3BIFrKuv8fJ +o6aEzlfgWGROCzshU2m8fB9P0B4hWDlJsc1D01sW60zhjLo9+XoWznmw5mczz7sc +S5sdDh47rSJsNRuFd7YDjeLzJWPqLrKVB5nn6nRbvrnBqhfsknkO4VIXhmEMSs1u +RMYOJ9ShAoGBAPinA6ktIeez1t5IsfxGwbCeZzFI1suZqZeX6ezNKaMpeykyAPuh +CqN7H+a4NCKsinsgHJowU98ckHeAsQ22s7R8dFZhyxEXkcBawY2soK29eq2aJHnY +lqKOwjOA7wgElRHwLkNFniQ5lKFPMly8a9NVAqg+Th/J3uR+7wE2t+b1AoGBAOeQ +H/vVkdaNB2ovnCxMh+OfxpcjkfF6KnD2jpn/TKsbR5BtnrtyRLc5+qt52D0CEgSy +qU3zrsZebShej3OIBPrEwIcPN+LezaxnLMf9RXdOde+wWrQLWLkShJaSTwSoGqZB +fcO0/sc1lzhGxm++ByP5mWbHr/VM9IdTQQH5Bct/AoGBAMhmOrIXeNL4Az2FU0Vi +dWp2T+7NqKfRAXj264Z5V4xzuxpZfadPhHZ7nhth7Erhyn4vRD4UoxQXPmvB4XCP +Bkh5YX3ZNUNiPorL2mDnd1xvcLcHm0xEfisnaWb/DCbnIomhjHeVXT4O1jYn0Qwi +o7hgNFMKXAaMuUJo9xGAWzkdAoGASxC4nY2tOiz7k1udt+qTPqHj4cjhHbOpoHb8 +4UUWmH0+ZL50b3Vqey8raH0WMSjDqIw2QBPXu2yO3EBTJnOYkaZIdz/isQPjDplf +tfEPnM5tgubbcHQhLdWn75u8S9km0nB2kYPR98gSnmarGzwx2mKmbOAc1Vs+BcRi +VX5hd4cCgYAubBq0VsFT0KVU3Rva3dgPR1K5bp4r4hE5cGXm4HvLiOgv995CwPy1 +27eONF9GN7hvjI6C17jA1Gyx5sN0QrsMv/1BZqiGaragMOPXFD+tVecWuKH4lZQi +VbKTOWHlGkrDCpiYWpfetQAjouj+0c6d+wigcoC8e5dwxBPI2f3rGw== +-----END RSA PRIVATE KEY----- diff --git a/vendor/github.com/vulcand/oxy/forward/fwd.go b/vendor/github.com/vulcand/oxy/forward/fwd.go index 4b358e425..1e0cc74e6 100644 --- a/vendor/github.com/vulcand/oxy/forward/fwd.go +++ b/vendor/github.com/vulcand/oxy/forward/fwd.go @@ -4,6 +4,7 @@ package forward import ( + "bufio" "crypto/tls" "io" "net" @@ -290,14 +291,26 @@ func (f *websocketForwarder) serveHTTP(w http.ResponseWriter, req *http.Request, ctx.errHandler.ServeHTTP(w, req, err) return } - errc := make(chan error, 2) - replicate := func(dst io.Writer, src io.Reader) { - _, err := io.Copy(dst, src) - errc <- err + + br := bufio.NewReader(targetConn) + resp, err := http.ReadResponse(br, req) + resp.Write(underlyingConn) + defer resp.Body.Close() + + // We connect the conn only if the switching protocol has not failed + if resp.StatusCode == http.StatusSwitchingProtocols { + ctx.log.Infof("Switching protocol success") + errc := make(chan error, 2) + replicate := func(dst io.Writer, src io.Reader) { + _, err := io.Copy(dst, src) + errc <- err + } + go replicate(targetConn, underlyingConn) + go replicate(underlyingConn, targetConn) + <-errc + } else { + ctx.log.Infof("Switching protocol failed") } - go replicate(targetConn, underlyingConn) - go replicate(underlyingConn, targetConn) - <-errc } // copyRequest makes a copy of the specified request.