Compare commits
3 Commits
will/statu
...
danderson/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19b1c31e60 | ||
|
|
f13e5e38b2 | ||
|
|
754a6d0514 |
@@ -7,54 +7,20 @@ package main
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
var unsafeHostnameCharacters = regexp.MustCompile(`[^a-zA-Z0-9-\.]`)
|
||||
|
||||
type certProvider interface {
|
||||
// TLSConfig creates a new TLS config suitable for net/http.Server servers.
|
||||
TLSConfig() *tls.Config
|
||||
// HTTPHandler handle ACME related request, if any.
|
||||
HTTPHandler(fallback http.Handler) http.Handler
|
||||
}
|
||||
|
||||
func certProviderByCertMode(mode, dir, hostname string) (certProvider, error) {
|
||||
if dir == "" {
|
||||
return nil, errors.New("missing required --certdir flag")
|
||||
}
|
||||
switch mode {
|
||||
case "letsencrypt":
|
||||
certManager := &autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
HostPolicy: autocert.HostWhitelist(hostname),
|
||||
Cache: autocert.DirCache(dir),
|
||||
}
|
||||
if hostname == "derp.tailscale.com" {
|
||||
certManager.HostPolicy = prodAutocertHostPolicy
|
||||
certManager.Email = "security@tailscale.com"
|
||||
}
|
||||
return certManager, nil
|
||||
case "manual":
|
||||
return NewManualCertManager(dir, hostname)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupport cert mode: %q", mode)
|
||||
}
|
||||
}
|
||||
|
||||
type manualCertManager struct {
|
||||
cert *tls.Certificate
|
||||
hostname string
|
||||
}
|
||||
|
||||
// NewManualCertManager returns a cert provider which read certificate by given hostname on create.
|
||||
func NewManualCertManager(certdir, hostname string) (certProvider, error) {
|
||||
func NewManualCertManager(certdir, hostname string) (*manualCertManager, error) {
|
||||
keyname := unsafeHostnameCharacters.ReplaceAllString(hostname, "")
|
||||
crtPath := filepath.Join(certdir, keyname+".crt")
|
||||
keyPath := filepath.Join(certdir, keyname+".key")
|
||||
@@ -73,23 +39,9 @@ func NewManualCertManager(certdir, hostname string) (certProvider, error) {
|
||||
return &manualCertManager{cert: &cert, hostname: hostname}, nil
|
||||
}
|
||||
|
||||
func (m *manualCertManager) TLSConfig() *tls.Config {
|
||||
return &tls.Config{
|
||||
Certificates: nil,
|
||||
NextProtos: []string{
|
||||
"h2", "http/1.1", // enable HTTP/2
|
||||
},
|
||||
GetCertificate: m.getCertificate,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manualCertManager) getCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
func (m *manualCertManager) GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if hi.ServerName != m.hostname {
|
||||
return nil, fmt.Errorf("cert mismatch with hostname: %q", hi.ServerName)
|
||||
}
|
||||
return m.cert, nil
|
||||
}
|
||||
|
||||
func (m *manualCertManager) HTTPHandler(fallback http.Handler) http.Handler {
|
||||
return fallback
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
@@ -32,6 +33,7 @@ import (
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/tsweb"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -135,9 +137,13 @@ func main() {
|
||||
tsweb.DevMode = true
|
||||
}
|
||||
|
||||
listenHost, _, err := net.SplitHostPort(*addr)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid server address: %v", err)
|
||||
if *certDir == "" {
|
||||
log.Fatal("missing required --certdir flag")
|
||||
}
|
||||
switch *certMode {
|
||||
case "letsencrypt", "manual":
|
||||
default:
|
||||
log.Fatalf("unknown --certmode %q", *certMode)
|
||||
}
|
||||
|
||||
var logPol *logpolicy.Policy
|
||||
@@ -148,34 +154,15 @@ func main() {
|
||||
|
||||
cfg := loadConfig()
|
||||
|
||||
serveTLS := tsweb.IsProd443(*addr) || *certMode == "manual"
|
||||
|
||||
s := derp.NewServer(cfg.PrivateKey, log.Printf)
|
||||
s.SetVerifyClient(*verifyClients)
|
||||
|
||||
if *meshPSKFile != "" {
|
||||
b, err := ioutil.ReadFile(*meshPSKFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
key := strings.TrimSpace(string(b))
|
||||
if matched, _ := regexp.MatchString(`(?i)^[0-9a-f]{64,}$`, key); !matched {
|
||||
log.Fatalf("key in %s must contain 64+ hex digits", *meshPSKFile)
|
||||
}
|
||||
s.SetMeshKey(key)
|
||||
log.Printf("DERP mesh key configured")
|
||||
}
|
||||
if err := startMesh(s); err != nil {
|
||||
log.Fatalf("startMesh: %v", err)
|
||||
s, err := startDerper(log.Printf, cfg.PrivateKey, *verifyClients, *meshPSKFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
expvar.Publish("derp", s.ExpVar())
|
||||
|
||||
mux := http.NewServeMux()
|
||||
derpHandler := derphttp.Handler(s)
|
||||
derpHandler = addWebSocketSupport(s, derpHandler)
|
||||
mux.Handle("/derp", derpHandler)
|
||||
mux.Handle("/derp", addWebSocketSupport(s, derphttp.Handler(s)))
|
||||
mux.HandleFunc("/derp/probe", probeHandler)
|
||||
go refreshBootstrapDNSLoop()
|
||||
mux.HandleFunc("/bootstrap-dns", handleBootstrapDNS)
|
||||
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
@@ -193,10 +180,21 @@ func main() {
|
||||
io.WriteString(w, "<p>Debug info at <a href='/debug/'>/debug/</a>.</p>\n")
|
||||
}
|
||||
}))
|
||||
debug := tsweb.Debugger(mux)
|
||||
debug.KV("TLS hostname", *hostname)
|
||||
debug.KV("Mesh key", s.HasMeshKey())
|
||||
debug.Handle("check", "Consistency check", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
httpCfg := tsweb.ServerConfig{
|
||||
Name: "derper",
|
||||
Addr: *addr,
|
||||
Handler: mux,
|
||||
AllowedHostnames: autocertPolicy(*hostname, *certMode == "letsencrypt"),
|
||||
ForceTLS: *certMode == "manual",
|
||||
}
|
||||
server := tsweb.NewServer(httpCfg)
|
||||
if server.HTTPS == nil {
|
||||
log.Fatal("derper can only serve over TLS")
|
||||
}
|
||||
server.Debug.KV("TLS hostname", *hostname)
|
||||
server.Debug.KV("Mesh key", s.HasMeshKey())
|
||||
server.Debug.Handle("check", "Consistency check", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
err := s.ConsistencyCheck()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
@@ -204,108 +202,71 @@ func main() {
|
||||
io.WriteString(w, "derp.Server ConsistencyCheck okay")
|
||||
}
|
||||
}))
|
||||
debug.Handle("traffic", "Traffic check", http.HandlerFunc(s.ServeDebugTraffic))
|
||||
server.Debug.Handle("traffic", "Traffic check", http.HandlerFunc(s.ServeDebugTraffic))
|
||||
|
||||
if server.CertManager != nil {
|
||||
if *hostname != "derp.tailscale.com" {
|
||||
server.CertManager.Email = ""
|
||||
}
|
||||
// TODO: derper could just use ~/.cache/tailscale/derper, but
|
||||
// for legacy compat, force the use of certDir.
|
||||
server.CertManager.Cache = autocert.DirCache(*certDir)
|
||||
}
|
||||
if *certMode == "manual" {
|
||||
certManager, err := NewManualCertManager(*certDir, *hostname)
|
||||
if err != nil {
|
||||
log.Fatalf("creating manual cert manager: %v", err)
|
||||
}
|
||||
server.HTTPS.TLSConfig.GetCertificate = certManager.GetCertificate
|
||||
}
|
||||
|
||||
// Append the derper meta-certificate to the "regular" TLS
|
||||
// certificate chain, to enable RTT-reduced handshaking.
|
||||
getCert := server.HTTPS.TLSConfig.GetCertificate
|
||||
server.HTTPS.TLSConfig.GetCertificate = func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
cert, err := getCert(hi)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert.Certificate = append(cert.Certificate, s.MetaCert())
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
if *runSTUN {
|
||||
listenHost, _, err := net.SplitHostPort(*addr)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid server address: %v", err)
|
||||
}
|
||||
go serveSTUN(listenHost)
|
||||
}
|
||||
|
||||
httpsrv := &http.Server{
|
||||
Addr: *addr,
|
||||
Handler: mux,
|
||||
|
||||
// Set read/write timeout. For derper, this basically
|
||||
// only affects TLS setup, as read/write deadlines are
|
||||
// cleared on Hijack, which the DERP server does. But
|
||||
// without this, we slowly accumulate stuck TLS
|
||||
// handshake goroutines forever. This also affects
|
||||
// /debug/ traffic, but 30 seconds is plenty for
|
||||
// Prometheus/etc scraping.
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
if serveTLS {
|
||||
log.Printf("derper: serving on %s with TLS", *addr)
|
||||
var certManager certProvider
|
||||
certManager, err = certProviderByCertMode(*certMode, *certDir, *hostname)
|
||||
if err != nil {
|
||||
log.Fatalf("derper: can not start cert provider: %v", err)
|
||||
}
|
||||
httpsrv.TLSConfig = certManager.TLSConfig()
|
||||
getCert := httpsrv.TLSConfig.GetCertificate
|
||||
httpsrv.TLSConfig.GetCertificate = func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
cert, err := getCert(hi)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert.Certificate = append(cert.Certificate, s.MetaCert())
|
||||
return cert, nil
|
||||
}
|
||||
// Disable TLS 1.0 and 1.1, which are obsolete and have security issues.
|
||||
httpsrv.TLSConfig.MinVersion = tls.VersionTLS12
|
||||
httpsrv.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.TLS != nil {
|
||||
label := "unknown"
|
||||
switch r.TLS.Version {
|
||||
case tls.VersionTLS10:
|
||||
label = "1.0"
|
||||
case tls.VersionTLS11:
|
||||
label = "1.1"
|
||||
case tls.VersionTLS12:
|
||||
label = "1.2"
|
||||
case tls.VersionTLS13:
|
||||
label = "1.3"
|
||||
}
|
||||
tlsRequestVersion.Add(label, 1)
|
||||
tlsActiveVersion.Add(label, 1)
|
||||
defer tlsActiveVersion.Add(label, -1)
|
||||
}
|
||||
|
||||
// Set HTTP headers to appease automated security scanners.
|
||||
//
|
||||
// Security automation gets cranky when HTTPS sites don't
|
||||
// set HSTS, and when they don't specify a content
|
||||
// security policy for XSS mitigation.
|
||||
//
|
||||
// DERP's HTTP interface is only ever used for debug
|
||||
// access (for which trivial safe policies work just
|
||||
// fine), and by DERP clients which don't obey any of
|
||||
// these browser-centric headers anyway.
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; form-action 'none'; base-uri 'self'; block-all-mixed-content; plugin-types 'none'")
|
||||
mux.ServeHTTP(w, r)
|
||||
})
|
||||
if *httpPort > -1 {
|
||||
go func() {
|
||||
port80srv := &http.Server{
|
||||
Addr: net.JoinHostPort(listenHost, fmt.Sprintf("%d", *httpPort)),
|
||||
Handler: certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}),
|
||||
ReadTimeout: 30 * time.Second,
|
||||
// Crank up WriteTimeout a bit more than usually
|
||||
// necessary just so we can do long CPU profiles
|
||||
// and not hit net/http/pprof's "profile
|
||||
// duration exceeds server's WriteTimeout".
|
||||
WriteTimeout: 5 * time.Minute,
|
||||
}
|
||||
err := port80srv.ListenAndServe()
|
||||
if err != nil {
|
||||
if err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
err = httpsrv.ListenAndServeTLS("", "")
|
||||
} else {
|
||||
log.Printf("derper: serving on %s", *addr)
|
||||
err = httpsrv.ListenAndServe()
|
||||
}
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
log.Fatalf("derper: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func startDerper(logf logger.Logf, privateKey key.NodePrivate, verifyClients bool, meshPSKFile string) (*derp.Server, error) {
|
||||
s := derp.NewServer(privateKey, logf)
|
||||
s.SetVerifyClient(verifyClients)
|
||||
|
||||
if meshPSKFile != "" {
|
||||
b, err := ioutil.ReadFile(meshPSKFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading mesh PSK file: %v", err)
|
||||
}
|
||||
key := strings.TrimSpace(string(b))
|
||||
if matched, _ := regexp.MatchString(`(?i)^[0-9a-f]{64,}$`, key); !matched {
|
||||
return nil, fmt.Errorf("key in %s must contain 64+ hex digits", meshPSKFile)
|
||||
}
|
||||
s.SetMeshKey(key)
|
||||
}
|
||||
if err := startMesh(s); err != nil {
|
||||
return nil, fmt.Errorf("startMesh: %v", err)
|
||||
}
|
||||
go refreshBootstrapDNSLoop()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// probeHandler is the endpoint that js/wasm clients hit to measure
|
||||
// DERP latency, since they can't do UDP STUN queries.
|
||||
func probeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -371,6 +332,16 @@ func serverSTUNListener(ctx context.Context, pc *net.UDPConn) {
|
||||
|
||||
var validProdHostname = regexp.MustCompile(`^derp([^.]*)\.tailscale\.com\.?$`)
|
||||
|
||||
func autocertPolicy(hostname string, useAutocert bool) autocert.HostPolicy {
|
||||
if !useAutocert {
|
||||
return nil
|
||||
}
|
||||
if hostname == "derp.tailscale.com" {
|
||||
return prodAutocertHostPolicy
|
||||
}
|
||||
return autocert.HostWhitelist(hostname)
|
||||
}
|
||||
|
||||
func prodAutocertHostPolicy(_ context.Context, host string) error {
|
||||
if validProdHostname.MatchString(host) {
|
||||
return nil
|
||||
|
||||
@@ -18,10 +18,10 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/tsweb"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -62,44 +62,37 @@ func main() {
|
||||
http.HandleFunc("/", root)
|
||||
log.Printf("Starting hello server.")
|
||||
|
||||
errc := make(chan error, 1)
|
||||
if *httpAddr != "" {
|
||||
log.Printf("running HTTP server on %s", *httpAddr)
|
||||
go func() {
|
||||
errc <- http.ListenAndServe(*httpAddr, nil)
|
||||
}()
|
||||
mainAddr := *httpsAddr
|
||||
if mainAddr == "" {
|
||||
mainAddr = *httpAddr
|
||||
}
|
||||
if *httpsAddr != "" {
|
||||
log.Printf("running HTTPS server on %s", *httpsAddr)
|
||||
go func() {
|
||||
hs := &http.Server{
|
||||
Addr: *httpsAddr,
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
switch hi.ServerName {
|
||||
case "hello.ts.net":
|
||||
return tailscale.GetCertificate(hi)
|
||||
case "hello.ipn.dev":
|
||||
c, err := tls.LoadX509KeyPair(
|
||||
"/etc/hello/hello.ipn.dev.crt",
|
||||
"/etc/hello/hello.ipn.dev.key",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
return nil, errors.New("invalid SNI name")
|
||||
},
|
||||
},
|
||||
IdleTimeout: 30 * time.Second,
|
||||
ReadHeaderTimeout: 20 * time.Second,
|
||||
MaxHeaderBytes: 10 << 10,
|
||||
httpCfg := tsweb.ServerConfig{
|
||||
Name: "hello",
|
||||
Addr: mainAddr,
|
||||
Handler: http.DefaultServeMux,
|
||||
}
|
||||
server := tsweb.NewServer(httpCfg)
|
||||
if server.HTTPS != nil {
|
||||
server.HTTPS.TLSConfig.GetCertificate = func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
switch hi.ServerName {
|
||||
case "hello.ts.net":
|
||||
return tailscale.GetCertificate(hi)
|
||||
case "hello.ipn.dev":
|
||||
c, err := tls.LoadX509KeyPair(
|
||||
"/etc/hello/hello.ipn.dev.crt",
|
||||
"/etc/hello/hello.ipn.dev.key",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
errc <- hs.ListenAndServeTLS("", "")
|
||||
}()
|
||||
return nil, errors.New("invalid SNI name")
|
||||
}
|
||||
}
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Fatal(<-errc)
|
||||
}
|
||||
|
||||
func devMode() bool { return *httpsAddr == "" && *httpAddr != "" }
|
||||
|
||||
@@ -265,7 +265,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/wgengine/wglog from tailscale.com/wgengine
|
||||
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
|
||||
golang.org/x/crypto/acme from tailscale.com/ipn/localapi
|
||||
golang.org/x/crypto/acme from tailscale.com/ipn/localapi+
|
||||
golang.org/x/crypto/acme/autocert from tailscale.com/tsweb
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from golang.zx2c4.com/wireguard/device
|
||||
L golang.org/x/crypto/blowfish from golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
|
||||
|
||||
385
tsweb/server.go
Normal file
385
tsweb/server.go
Normal file
@@ -0,0 +1,385 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package tsweb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
const defaultTimeout = 10 * time.Second
|
||||
|
||||
// ServerConfig is the initial configuration of a Server, for
|
||||
// consumption by NewServer.
|
||||
type ServerConfig struct {
|
||||
// Name is a human-readable name for the HTTP server.
|
||||
//
|
||||
// It must be valid as both a directory name and the user portion
|
||||
// of an email address, so it's best to stick to a single
|
||||
// alphanumeric word, such as "derper" or "control".
|
||||
//
|
||||
// The name is for internal use only, for things like naming cache
|
||||
// directories, identifying the source of autocert emails,
|
||||
// human-readable debug pages, ...
|
||||
Name string
|
||||
// Addr specifies the TCP address for the server to listen on, in
|
||||
// the form "host:port". If blank, ":http" is used.
|
||||
//
|
||||
// If Addr specifies port 443, TLS serving is automatically
|
||||
// configured and a second listener is set up on port 80 to handle
|
||||
// HTTP to HTTPS redirection. All other ports result in HTTP-only
|
||||
// serving.
|
||||
Addr string
|
||||
// Handler is the HTTP.Handler to invoke to handle
|
||||
// requests. Cannot be nil.
|
||||
Handler http.Handler
|
||||
// AllowedHostnames is a function that reports whether autocert is
|
||||
// allowed to obtain TLS certificates for a given client request.
|
||||
//
|
||||
// If the list of valid hostnames is static, you probably want to
|
||||
// use autocert.HostWhitelist(...).
|
||||
//
|
||||
// If nil, autocert will not allow any TLS requests, and you
|
||||
// should override Server.HTTPS.TLSConfig.GetCertificate to handle
|
||||
// TLS certificates yourself.
|
||||
AllowedHostnames autocert.HostPolicy
|
||||
|
||||
// HSTSNoSubdomains is whether to tell browsers to only apply
|
||||
// strict HTTPS serving to the current domain being requested,
|
||||
// rather than the request domain and all its subdomains (the
|
||||
// default).
|
||||
HSTSNoSubdomains bool
|
||||
|
||||
// RequestLog, if non-nil, receives one AccessLogRecord for every
|
||||
// completed HTTP request (unless the request handler invoked
|
||||
// SuppressLogging).
|
||||
RequestLog func(*AccessLogRecord)
|
||||
|
||||
// ForceTLS is whether to configure Server for TLS serving only,
|
||||
// regardless of Addr's port number. This disables autocert (since
|
||||
// autocert can't function on non-443 ports) and HTTP serving, and
|
||||
// requires you to adjust Server.HTTPS.TLSConfig yourself.
|
||||
ForceTLS bool
|
||||
|
||||
// TODO: some kind of dev mode? Alter how request logging and
|
||||
// error handling is done to be more human-friendly?
|
||||
}
|
||||
|
||||
// Server is an HTTP+HTTPS server. Callers should get a Server by
|
||||
// calling NewServer, then applying any desired customizations to the
|
||||
// prefilled fields prior to calling ListenAndServe.
|
||||
type Server struct {
|
||||
// Handler is the root handler for the Server.
|
||||
Handler http.Handler
|
||||
|
||||
// HTTPS is the HTTPS serving component of the server.
|
||||
// When non-nil, it defaults to getting TLS certs from
|
||||
// CertManager, and enforces TLS 1.2 as the minimum protocol
|
||||
// version.
|
||||
HTTPS *http.Server
|
||||
// CertManager, if non-nil, manages TLS certificates for HTTPS.
|
||||
CertManager *autocert.Manager
|
||||
|
||||
// HTTP is the HTTP serving component of the server. If HTTPS
|
||||
// serving is disabled, this is the main HTTP server. Otherwise,
|
||||
// it redirects everything to HTTPS, except for debug handlers
|
||||
// which are served directly to authorized clients.
|
||||
HTTP *http.Server
|
||||
|
||||
// Debug is the handler for /debug/ on HTTP and HTTPS.
|
||||
Debug *DebugHandler
|
||||
|
||||
// AlwaysHeaders are HTTP headers that are set on all requests
|
||||
// prior to invoking Handler.
|
||||
AlwaysHeaders http.Header
|
||||
// TLSHeaders are HTTP headers that are set on all TLS requests
|
||||
// prior to invoking Handler.
|
||||
TLSHeaders http.Header
|
||||
|
||||
// RequestLog is where HTTP request logs are written. If nil,
|
||||
// request logging is disabled.
|
||||
RequestLog func(*AccessLogRecord)
|
||||
|
||||
now func() time.Time // normally time.Now, modified for tests
|
||||
|
||||
vars metrics.Set
|
||||
httpRequests expvar.Int // counter, completed requests
|
||||
httpActive expvar.Int // gauge, currently alive requests
|
||||
tlsRequests metrics.LabelMap // counter, completed requests
|
||||
tlsActive metrics.LabelMap // gauge, currently alive requests
|
||||
statusCode metrics.LabelMap // status code of completed requests
|
||||
statusFamily metrics.LabelMap // like statusCode, but bucketed by 1st digit of status code
|
||||
}
|
||||
|
||||
// NewServer returns a Server, initialized by cfg with good defaults
|
||||
// for serving.
|
||||
func NewServer(cfg ServerConfig) *Server {
|
||||
s := &Server{
|
||||
Handler: cfg.Handler,
|
||||
Debug: Debugger(http.NewServeMux()),
|
||||
AlwaysHeaders: http.Header{},
|
||||
TLSHeaders: http.Header{},
|
||||
|
||||
tlsRequests: metrics.LabelMap{Label: "version"},
|
||||
tlsActive: metrics.LabelMap{Label: "version"},
|
||||
statusCode: metrics.LabelMap{Label: "code"},
|
||||
statusFamily: metrics.LabelMap{Label: "code_family"},
|
||||
}
|
||||
|
||||
s.vars.Set("http_requests", &s.httpRequests)
|
||||
s.vars.Set("gauge_http_active", &s.httpActive)
|
||||
s.vars.Set("tls_request_version", &s.tlsRequests)
|
||||
s.vars.Set("gauge_tls_active_version", &s.tlsActive)
|
||||
s.vars.Set("http_status", &s.statusCode)
|
||||
s.vars.Set("http_status_family", &s.statusFamily)
|
||||
|
||||
switch {
|
||||
case cfg.ForceTLS:
|
||||
s.HTTPS = &http.Server{
|
||||
Addr: cfg.Addr,
|
||||
Handler: s,
|
||||
ReadTimeout: defaultTimeout,
|
||||
WriteTimeout: defaultTimeout,
|
||||
IdleTimeout: defaultTimeout,
|
||||
TLSConfig: &tls.Config{
|
||||
NextProtos: []string{
|
||||
"h2", "http/1.1", // enable HTTP/2
|
||||
},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
},
|
||||
}
|
||||
// Deliberately don't forcibly enable HSTS here, this is a
|
||||
// configuration where the caller has requested very manual
|
||||
// TLS management, we shouldn't impose HSTS.
|
||||
case IsProd443(cfg.Addr):
|
||||
s.CertManager = &autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
Cache: autocert.DirCache(DefaultCertDir(cfg.Name)),
|
||||
Email: fmt.Sprintf("infra+autocert-%s@tailscale.com", cfg.Name),
|
||||
HostPolicy: cfg.AllowedHostnames,
|
||||
}
|
||||
if s.CertManager.HostPolicy == nil {
|
||||
s.CertManager.HostPolicy = func(context.Context, string) error {
|
||||
return errors.New("no TLS serving allowed")
|
||||
}
|
||||
}
|
||||
s.HTTPS = &http.Server{
|
||||
Addr: cfg.Addr,
|
||||
Handler: s,
|
||||
ReadTimeout: defaultTimeout,
|
||||
WriteTimeout: defaultTimeout,
|
||||
IdleTimeout: defaultTimeout,
|
||||
TLSConfig: s.CertManager.TLSConfig(),
|
||||
}
|
||||
s.HTTPS.TLSConfig.MinVersion = tls.VersionTLS12
|
||||
s.HTTP = &http.Server{
|
||||
Addr: ":80",
|
||||
Handler: s.CertManager.HTTPHandler(Port80Handler{s}),
|
||||
ReadTimeout: defaultTimeout,
|
||||
WriteTimeout: defaultTimeout,
|
||||
IdleTimeout: defaultTimeout,
|
||||
}
|
||||
hstsVal := "max-age=63072000"
|
||||
if !cfg.HSTSNoSubdomains {
|
||||
hstsVal += "; includeSubDomains"
|
||||
}
|
||||
s.TLSHeaders.Set("Strict-Transport-Security", hstsVal)
|
||||
default:
|
||||
s.HTTP = &http.Server{
|
||||
Addr: cfg.Addr,
|
||||
Handler: s,
|
||||
ReadTimeout: defaultTimeout,
|
||||
WriteTimeout: defaultTimeout,
|
||||
IdleTimeout: defaultTimeout,
|
||||
}
|
||||
}
|
||||
s.AlwaysHeaders.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; form-action 'none'; base-uri 'self'; block-all-mixed-content; plugin-types 'none'")
|
||||
s.AlwaysHeaders.Set("X-Frame-Options", "DENY")
|
||||
s.AlwaysHeaders.Set("X-Content-Type-Options", "nosniff")
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// ListenAndServe listens on the TCP network addresses s.HTTP.Addr and
|
||||
// s.HTTPS.Addr (if any) and then calls Serve or ServeTLS to handle
|
||||
// incoming requests.
|
||||
//
|
||||
// If s.HTTP.Addr is blank, ":http" is used.
|
||||
// If s.HTTPS.Addr is blank, ":https" is used.
|
||||
func (s *Server) ListenAndServe() error {
|
||||
errCh := make(chan error, 2)
|
||||
|
||||
if s.HTTP != nil {
|
||||
go func() { errCh <- s.HTTP.ListenAndServe() }()
|
||||
}
|
||||
if s.HTTPS != nil {
|
||||
go func() { errCh <- s.HTTPS.ListenAndServeTLS("", "") }()
|
||||
}
|
||||
|
||||
err := <-errCh
|
||||
if err == http.ErrServerClosed {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// close immediately closes all listeners and connections, except
|
||||
// hijacked Conns. Returns any error returned from closing underlying
|
||||
// listeners.
|
||||
func (s *Server) Close() error {
|
||||
var err error
|
||||
if s.HTTP != nil {
|
||||
err = s.HTTP.Close()
|
||||
}
|
||||
if s.HTTPS != nil {
|
||||
err2 := s.HTTPS.Close()
|
||||
if err == nil {
|
||||
err = err2
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ServeHTTP wraps s.Handler and adds the following features:
|
||||
// - Initializes default values for response headers from
|
||||
// s.AlwaysHeaders and s.TLSHeaders (if the request is over TLS).
|
||||
// - Sends requests for /debug/* to s.Debug.
|
||||
// - Maintains HTTP and TLS expvar metrics.
|
||||
// - If s.RequestLog is non-nil, writes out JSON AccessLogRecord
|
||||
// structs when requests complete.
|
||||
// - Injects tailscale helpers into the request context, so that
|
||||
// helpers like Err and SuppressLogging work.
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
defer s.httpRequests.Add(1)
|
||||
s.httpActive.Add(1)
|
||||
defer s.httpActive.Add(-1)
|
||||
if r.TLS != nil {
|
||||
label := "unknown"
|
||||
switch r.TLS.Version {
|
||||
case tls.VersionTLS10:
|
||||
label = "1.0"
|
||||
case tls.VersionTLS11:
|
||||
label = "1.1"
|
||||
case tls.VersionTLS12:
|
||||
label = "1.2"
|
||||
case tls.VersionTLS13:
|
||||
label = "1.3"
|
||||
}
|
||||
defer s.tlsRequests.Add(label, 1)
|
||||
s.tlsActive.Add(label, 1)
|
||||
defer s.tlsActive.Add(label, -1)
|
||||
}
|
||||
|
||||
for k, v := range s.AlwaysHeaders {
|
||||
w.Header()[k] = v
|
||||
}
|
||||
if r.TLS != nil {
|
||||
for k, v := range s.TLSHeaders {
|
||||
w.Header()[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// always throw in a loggingResponseWriter, even when not writing
|
||||
// access logs, so that we can record the response code and push
|
||||
// it to metrics.
|
||||
lw := &loggingResponseWriter{ResponseWriter: w, logf: logger.Discard}
|
||||
|
||||
path := r.RequestURI
|
||||
switch {
|
||||
case path == "/debug" || strings.HasPrefix(path, "/debug/"):
|
||||
s.Debug.ServeHTTP(lw, r)
|
||||
case s.RequestLog != nil:
|
||||
s.serveAndLog(lw, r)
|
||||
default:
|
||||
s.Handler.ServeHTTP(lw, r)
|
||||
}
|
||||
|
||||
s.statusCode.Add(strconv.Itoa(lw.httpCode()), 1)
|
||||
key := fmt.Sprintf("%dxx", lw.httpCode()/100)
|
||||
s.statusFamily.Add(key, 1)
|
||||
}
|
||||
|
||||
// serveAndLog invokes s.Handler and writes an access log entry to
|
||||
// s.RequestLog, which must be non-nil.
|
||||
//
|
||||
// s.Handler can provide a detailed error value, which is only logged
|
||||
// and not sent to the client, using the Err helper.
|
||||
//
|
||||
// s.Handler can suppress the writing of an access log entry by using
|
||||
// the SuppressLogging helper, for example to not log successful
|
||||
// requests on very high-volume API endpoints.
|
||||
func (s *Server) serveAndLog(lw *loggingResponseWriter, r *http.Request) {
|
||||
msg := &AccessLogRecord{
|
||||
When: s.now(),
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
Proto: r.Proto,
|
||||
TLS: r.TLS != nil,
|
||||
Host: r.Host,
|
||||
Method: r.Method,
|
||||
RequestURI: r.URL.RequestURI(),
|
||||
UserAgent: r.UserAgent(),
|
||||
Referer: r.Referer(),
|
||||
}
|
||||
|
||||
var (
|
||||
detailedErr error
|
||||
suppressLogging bool
|
||||
)
|
||||
ctx := context.WithValue(r.Context(), ctxRecordError, &detailedErr)
|
||||
ctx = context.WithValue(ctx, ctxSuppressLogging, &suppressLogging)
|
||||
r = r.WithContext(ctx)
|
||||
s.Handler.ServeHTTP(lw, r)
|
||||
|
||||
if suppressLogging {
|
||||
return
|
||||
}
|
||||
|
||||
msg.Seconds = s.now().Sub(msg.When).Seconds()
|
||||
msg.Bytes = lw.bytes
|
||||
msg.Code = lw.httpCode()
|
||||
if detailedErr != nil {
|
||||
msg.Err = detailedErr.Error()
|
||||
}
|
||||
|
||||
s.RequestLog(msg)
|
||||
}
|
||||
|
||||
var (
|
||||
// ctxRecordError is a context.WithValue key that stores a pointer
|
||||
// to an error, which http.Handlers can use to record a detailed
|
||||
// error that will be attached to the request's log entry.
|
||||
ctxRecordError = struct{}{}
|
||||
// ctxSuppressLogging is a context.WithValue key that stores a
|
||||
// pointer to a bool, which http.Handlers can set to true if they
|
||||
// want to suppress the writing of the current request's log
|
||||
// entry.
|
||||
ctxSuppressLogging = struct{}{}
|
||||
)
|
||||
|
||||
// Err records err as a detailed internal error for r, for possible
|
||||
// logging.
|
||||
func Err(r *http.Request, err error) {
|
||||
if perr, ok := r.Context().Value(ctxRecordError).(*error); ok {
|
||||
*perr = err
|
||||
}
|
||||
}
|
||||
|
||||
// SuppressLogging requests that no access log entry be written for r.
|
||||
func SuppressLogging(r *http.Request) {
|
||||
if pb, ok := r.Context().Value(ctxSuppressLogging).(*bool); ok {
|
||||
*pb = true
|
||||
}
|
||||
}
|
||||
@@ -280,6 +280,7 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// response code that gets sent, if any.
|
||||
type loggingResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
ctx context.Context
|
||||
code int
|
||||
bytes int
|
||||
hijacked bool
|
||||
@@ -330,6 +331,20 @@ func (l loggingResponseWriter) Flush() {
|
||||
f.Flush()
|
||||
}
|
||||
|
||||
func (l *loggingResponseWriter) httpCode() int {
|
||||
switch {
|
||||
case l.code != 0:
|
||||
return l.code
|
||||
case l.hijacked:
|
||||
return http.StatusSwitchingProtocols
|
||||
case l.ctx.Err() == context.Canceled:
|
||||
return 499 // nginx convention: Client Closed Request
|
||||
default:
|
||||
// Handler didn't write a body or send a header, that means 200.
|
||||
return 200
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPError is an error with embedded HTTP response information.
|
||||
//
|
||||
// It is the error type to be (optionally) used by Handler.ServeHTTPReturn.
|
||||
|
||||
Reference in New Issue
Block a user