Skip to content

Commit 87546a5

Browse files
committed
cmd/derper: allow absent SNI when using manual certs and IP literal for hostname
Updates tailscale#11776 Change-Id: I81756415feb630da093833accc3074903ebd84a7 Signed-off-by: Brad Fitzpatrick <[email protected]>
1 parent 614c612 commit 87546a5

File tree

4 files changed

+108
-7
lines changed

4 files changed

+108
-7
lines changed

cmd/derper/cert.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"crypto/x509"
99
"errors"
1010
"fmt"
11+
"net"
1112
"net/http"
1213
"path/filepath"
1314
"regexp"
@@ -53,8 +54,9 @@ func certProviderByCertMode(mode, dir, hostname string) (certProvider, error) {
5354
}
5455

5556
type manualCertManager struct {
56-
cert *tls.Certificate
57-
hostname string
57+
cert *tls.Certificate
58+
hostname string // hostname or IP address of server
59+
noHostname bool // whether hostname is an IP address
5860
}
5961

6062
// NewManualCertManager returns a cert provider which read certificate by given hostname on create.
@@ -74,7 +76,11 @@ func NewManualCertManager(certdir, hostname string) (certProvider, error) {
7476
if err := x509Cert.VerifyHostname(hostname); err != nil {
7577
return nil, fmt.Errorf("cert invalid for hostname %q: %w", hostname, err)
7678
}
77-
return &manualCertManager{cert: &cert, hostname: hostname}, nil
79+
return &manualCertManager{
80+
cert: &cert,
81+
hostname: hostname,
82+
noHostname: net.ParseIP(hostname) != nil,
83+
}, nil
7884
}
7985

8086
func (m *manualCertManager) TLSConfig() *tls.Config {
@@ -88,7 +94,7 @@ func (m *manualCertManager) TLSConfig() *tls.Config {
8894
}
8995

9096
func (m *manualCertManager) getCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
91-
if hi.ServerName != m.hostname {
97+
if hi.ServerName != m.hostname && !m.noHostname {
9298
return nil, fmt.Errorf("cert mismatch with hostname: %q", hi.ServerName)
9399
}
94100

cmd/derper/cert_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) Tailscale Inc & AUTHORS
2+
// SPDX-License-Identifier: BSD-3-Clause
3+
4+
package main
5+
6+
import (
7+
"crypto/ecdsa"
8+
"crypto/elliptic"
9+
"crypto/rand"
10+
"crypto/tls"
11+
"crypto/x509"
12+
"crypto/x509/pkix"
13+
"encoding/pem"
14+
"math/big"
15+
"net"
16+
"os"
17+
"path/filepath"
18+
"testing"
19+
"time"
20+
)
21+
22+
// Verify that in --certmode=manual mode, we can use a bare IP address
23+
// as the --hostname and that GetCertificate will return it.
24+
func TestCertIP(t *testing.T) {
25+
dir := t.TempDir()
26+
const hostname = "1.2.3.4"
27+
28+
priv, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
29+
if err != nil {
30+
t.Fatal(err)
31+
}
32+
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
33+
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
34+
if err != nil {
35+
t.Fatal(err)
36+
}
37+
ip := net.ParseIP(hostname)
38+
if ip == nil {
39+
t.Fatalf("invalid IP address %q", hostname)
40+
}
41+
template := &x509.Certificate{
42+
SerialNumber: serialNumber,
43+
Subject: pkix.Name{
44+
Organization: []string{"Tailscale Test Corp"},
45+
},
46+
NotBefore: time.Now(),
47+
NotAfter: time.Now().Add(30 * 24 * time.Hour),
48+
49+
KeyUsage: x509.KeyUsageDigitalSignature,
50+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
51+
BasicConstraintsValid: true,
52+
IPAddresses: []net.IP{ip},
53+
}
54+
derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv)
55+
if err != nil {
56+
t.Fatal(err)
57+
}
58+
certOut, err := os.Create(filepath.Join(dir, hostname+".crt"))
59+
if err != nil {
60+
t.Fatal(err)
61+
}
62+
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
63+
t.Fatalf("Failed to write data to cert.pem: %v", err)
64+
}
65+
if err := certOut.Close(); err != nil {
66+
t.Fatalf("Error closing cert.pem: %v", err)
67+
}
68+
69+
keyOut, err := os.OpenFile(filepath.Join(dir, hostname+".key"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
70+
if err != nil {
71+
t.Fatal(err)
72+
}
73+
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
74+
if err != nil {
75+
t.Fatalf("Unable to marshal private key: %v", err)
76+
}
77+
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
78+
t.Fatalf("Failed to write data to key.pem: %v", err)
79+
}
80+
if err := keyOut.Close(); err != nil {
81+
t.Fatalf("Error closing key.pem: %v", err)
82+
}
83+
84+
cp, err := certProviderByCertMode("manual", dir, hostname)
85+
if err != nil {
86+
t.Fatal(err)
87+
}
88+
back, err := cp.TLSConfig().GetCertificate(&tls.ClientHelloInfo{
89+
ServerName: "", // no SNI
90+
})
91+
if err != nil {
92+
t.Fatalf("GetCertificate: %v", err)
93+
}
94+
if back == nil {
95+
t.Fatalf("GetCertificate returned nil")
96+
}
97+
}

cmd/derper/derper.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ var (
5858
configPath = flag.String("c", "", "config file path")
5959
certMode = flag.String("certmode", "letsencrypt", "mode for getting a cert. possible options: manual, letsencrypt")
6060
certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store LetsEncrypt certs, if addr's port is :443")
61-
hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443")
61+
hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443. When --certmode=manual, this can be an IP address to avoid SNI checks")
6262
runSTUN = flag.Bool("stun", true, "whether to run a STUN server. It will bind to the same IP (if any) as the --addr flag value.")
6363
runDERP = flag.Bool("derp", true, "whether to run a DERP server. The only reason to set this false is if you're decommissioning a server but want to keep its bootstrap DNS functionality still running.")
6464

cmd/derper/derper_test.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package main
66
import (
77
"bytes"
88
"context"
9-
"fmt"
109
"net/http"
1110
"net/http/httptest"
1211
"strings"
@@ -138,5 +137,4 @@ func TestTemplate(t *testing.T) {
138137
if !strings.Contains(str, "Debug info") {
139138
t.Error("Output is missing debug info")
140139
}
141-
fmt.Println(buf.String())
142140
}

0 commit comments

Comments
 (0)