2020-02-18 19:07:44 +00:00
// Copyright (c) 2020 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.
// The derper binary is a simple DERP server.
package main // import "tailscale.com/cmd/derper"
import (
2020-03-02 16:55:44 +00:00
"context"
2020-02-18 19:07:44 +00:00
"encoding/json"
2020-03-02 16:55:44 +00:00
"errors"
2020-02-21 17:35:53 +00:00
"expvar"
2020-02-18 19:07:44 +00:00
"flag"
2020-02-21 17:35:53 +00:00
"fmt"
2020-02-18 19:07:44 +00:00
"io"
"io/ioutil"
"log"
2020-02-27 03:11:14 +00:00
"net"
2020-02-18 19:07:44 +00:00
"net/http"
"os"
"path/filepath"
2020-03-02 16:55:44 +00:00
"regexp"
2020-06-01 22:19:41 +00:00
"strings"
2020-02-27 03:11:14 +00:00
"time"
2020-02-18 19:07:44 +00:00
"github.com/tailscale/wireguard-go/wgcfg"
"golang.org/x/crypto/acme/autocert"
"tailscale.com/atomicfile"
"tailscale.com/derp"
"tailscale.com/derp/derphttp"
2020-02-21 15:52:40 +00:00
"tailscale.com/logpolicy"
2020-03-04 20:24:07 +00:00
"tailscale.com/metrics"
2020-05-25 16:15:50 +00:00
"tailscale.com/net/stun"
2020-02-25 16:07:41 +00:00
"tailscale.com/tsweb"
2020-02-18 19:07:44 +00:00
"tailscale.com/types/key"
)
var (
2020-02-21 17:35:53 +00:00
dev = flag . Bool ( "dev" , false , "run in localhost development mode" )
2020-02-21 15:52:40 +00:00
addr = flag . String ( "a" , ":443" , "server address" )
configPath = flag . String ( "c" , "" , "config file path" )
2020-02-25 16:07:41 +00:00
certDir = flag . String ( "certdir" , tsweb . DefaultCertDir ( "derper-certs" ) , "directory to store LetsEncrypt certs, if addr's port is :443" )
2020-02-21 15:52:40 +00:00
hostname = flag . String ( "hostname" , "derp.tailscale.com" , "LetsEncrypt host name, if addr's port is :443" )
logCollection = flag . String ( "logcollection" , "" , "If non-empty, logtail collection to log to" )
2020-02-27 03:11:14 +00:00
runSTUN = flag . Bool ( "stun" , false , "also run a STUN server" )
2020-06-01 22:19:41 +00:00
meshPSKFile = flag . String ( "mesh-psk-file" , defaultMeshPSKFile ( ) , "if non-empty, path to file containing the mesh pre-shared key file. It should contain some hex string; whitespace is trimmed." )
meshWith = flag . String ( "mesh-with" , "" , "optional comma-separated list of hostnames to mesh with; the server's own hostname can be in the list, in which case it's ignored if its DNS resolves to an IP on the machine" )
2020-02-18 19:07:44 +00:00
)
type config struct {
PrivateKey wgcfg . PrivateKey
}
func loadConfig ( ) config {
2020-02-21 17:35:53 +00:00
if * dev {
return config { PrivateKey : mustNewKey ( ) }
}
2020-02-18 19:07:44 +00:00
if * configPath == "" {
log . Fatalf ( "derper: -c <config path> not specified" )
}
b , err := ioutil . ReadFile ( * configPath )
switch {
case os . IsNotExist ( err ) :
return writeNewConfig ( )
case err != nil :
log . Fatal ( err )
panic ( "unreachable" )
default :
var cfg config
if err := json . Unmarshal ( b , & cfg ) ; err != nil {
log . Fatalf ( "derper: config: %v" , err )
}
return cfg
}
}
2020-02-21 17:35:53 +00:00
func mustNewKey ( ) wgcfg . PrivateKey {
2020-02-18 19:07:44 +00:00
key , err := wgcfg . NewPrivateKey ( )
if err != nil {
log . Fatal ( err )
}
2020-02-21 17:35:53 +00:00
return key
}
2020-02-18 19:07:44 +00:00
2020-02-21 17:35:53 +00:00
func writeNewConfig ( ) config {
key := mustNewKey ( )
2020-02-18 19:07:44 +00:00
if err := os . MkdirAll ( filepath . Dir ( * configPath ) , 0777 ) ; err != nil {
log . Fatal ( err )
}
cfg := config {
PrivateKey : key ,
}
b , err := json . MarshalIndent ( cfg , "" , "\t" )
if err != nil {
log . Fatal ( err )
}
if err := atomicfile . WriteFile ( * configPath , b , 0666 ) ; err != nil {
log . Fatal ( err )
}
return cfg
}
func main ( ) {
flag . Parse ( )
2020-02-21 17:35:53 +00:00
if * dev {
* logCollection = ""
* addr = ":3340" // above the keys DERP
log . Printf ( "Running in dev mode." )
2020-03-03 19:33:22 +00:00
tsweb . DevMode = true
2020-02-21 17:35:53 +00:00
}
2020-02-21 15:52:40 +00:00
var logPol * logpolicy . Policy
if * logCollection != "" {
logPol = logpolicy . New ( * logCollection )
log . SetOutput ( logPol . Logtail )
}
2020-02-18 19:07:44 +00:00
cfg := loadConfig ( )
2020-02-25 16:07:41 +00:00
letsEncrypt := tsweb . IsProd443 ( * addr )
2020-02-18 19:07:44 +00:00
s := derp . NewServer ( key . Private ( cfg . PrivateKey ) , log . Printf )
2020-06-01 22:19:41 +00:00
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" )
}
// TODO(bradfitz): parse & use the *meshWith
_ = * meshWith
2020-02-21 17:35:53 +00:00
expvar . Publish ( "derp" , s . ExpVar ( ) )
2020-02-18 19:07:44 +00:00
2020-02-21 17:35:53 +00:00
// Create our own mux so we don't expose /debug/ stuff to the world.
2020-02-25 16:07:41 +00:00
mux := tsweb . NewMux ( debugHandler ( s ) )
2020-02-18 19:07:44 +00:00
mux . Handle ( "/derp" , derphttp . Handler ( s ) )
mux . Handle ( "/" , http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
2020-02-21 17:35:53 +00:00
w . Header ( ) . Set ( "Content-Type" , "text/html; charset=utf-8" )
2020-02-18 19:07:44 +00:00
w . WriteHeader ( 200 )
2020-02-21 17:35:53 +00:00
io . WriteString ( w , ` < html > < body >
< h1 > DERP < / h1 >
< p >
This is a
< a href = "https://tailscale.com/" > Tailscale < / a >
< a href = "https://godoc.org/tailscale.com/derp" > DERP < / a >
server .
< / p >
` )
2020-02-25 16:07:41 +00:00
if tsweb . AllowDebugAccess ( r ) {
2020-02-21 17:35:53 +00:00
io . WriteString ( w , "<p>Debug info at <a href='/debug/'>/debug/</a>.</p>\n" )
}
2020-02-18 19:07:44 +00:00
} ) )
2020-02-27 03:11:14 +00:00
if * runSTUN {
go serveSTUN ( )
}
2020-02-18 19:07:44 +00:00
httpsrv := & http . Server {
Addr : * addr ,
Handler : mux ,
}
var err error
if letsEncrypt {
if * certDir == "" {
log . Fatalf ( "missing required --certdir flag" )
}
log . Printf ( "derper: serving on %s with TLS" , * addr )
certManager := & autocert . Manager {
Prompt : autocert . AcceptTOS ,
HostPolicy : autocert . HostWhitelist ( * hostname ) ,
Cache : autocert . DirCache ( * certDir ) ,
}
2020-03-02 16:55:44 +00:00
if * hostname == "derp.tailscale.com" {
certManager . HostPolicy = prodAutocertHostPolicy
2020-03-03 19:53:50 +00:00
certManager . Email = "security@tailscale.com"
2020-03-02 16:55:44 +00:00
}
2020-02-18 19:07:44 +00:00
httpsrv . TLSConfig = certManager . TLSConfig ( )
go func ( ) {
2020-02-25 16:07:41 +00:00
err := http . ListenAndServe ( ":80" , certManager . HTTPHandler ( tsweb . Port80Handler { mux } ) )
2020-02-18 19:07:44 +00:00
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 {
log . Fatalf ( "derper: %v" , err )
}
}
2020-02-21 17:35:53 +00:00
func debugHandler ( s * derp . Server ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
f := func ( format string , args ... interface { } ) { fmt . Fprintf ( w , format , args ... ) }
f ( ` < html > < body >
< h1 > DERP debug < / h1 >
< ul >
` )
f ( "<li><b>Hostname:</b> %v</li>\n" , * hostname )
2020-02-25 16:07:41 +00:00
f ( "<li><b>Uptime:</b> %v</li>\n" , tsweb . Uptime ( ) )
2020-06-01 22:19:41 +00:00
f ( "<li><b>Mesh Key:</b> %v</li>\n" , s . HasMeshKey ( ) )
2020-02-21 17:35:53 +00:00
2020-03-06 21:01:18 +00:00
f ( ` < li > < a href = "/debug/vars" > / debug / vars < / a > ( Go ) < / li >
< li > < a href = "/debug/varz" > / debug / varz < / a > ( Prometheus ) < / li >
2020-02-21 17:35:53 +00:00
< li > < a href = "/debug/pprof/" > / debug / pprof / < / a > < / li >
< li > < a href = "/debug/pprof/goroutine?debug=1" > / debug / pprof / goroutine < / a > ( collapsed ) < / li >
< li > < a href = "/debug/pprof/goroutine?debug=2" > / debug / pprof / goroutine < / a > ( full ) < / li >
< ul >
< / html >
` )
} )
}
2020-02-27 03:11:14 +00:00
func serveSTUN ( ) {
pc , err := net . ListenPacket ( "udp" , ":3478" )
if err != nil {
log . Fatalf ( "failed to open STUN listener: %v" , err )
}
log . Printf ( "running STUN server on %v" , pc . LocalAddr ( ) )
2020-03-04 20:24:07 +00:00
2020-02-27 03:11:14 +00:00
var (
2020-03-04 20:24:07 +00:00
stats = new ( metrics . Set )
stunDisposition = & metrics . LabelMap { Label : "disposition" }
stunAddrFamily = & metrics . LabelMap { Label : "family" }
stunReadError = stunDisposition . Get ( "read_error" )
stunNotSTUN = stunDisposition . Get ( "not_stun" )
stunWriteError = stunDisposition . Get ( "write_error" )
stunSuccess = stunDisposition . Get ( "success" )
stunIPv4 = stunAddrFamily . Get ( "ipv4" )
stunIPv6 = stunAddrFamily . Get ( "ipv6" )
2020-02-27 03:11:14 +00:00
)
2020-03-04 20:24:07 +00:00
stats . Set ( "counter_requests" , stunDisposition )
stats . Set ( "counter_addrfamily" , stunAddrFamily )
expvar . Publish ( "stun" , stats )
2020-02-27 03:11:14 +00:00
var buf [ 64 << 10 ] byte
for {
n , addr , err := pc . ReadFrom ( buf [ : ] )
if err != nil {
log . Printf ( "STUN ReadFrom: %v" , err )
time . Sleep ( time . Second )
2020-03-04 20:24:07 +00:00
stunReadError . Add ( 1 )
2020-02-27 03:11:14 +00:00
continue
}
ua , ok := addr . ( * net . UDPAddr )
if ! ok {
log . Printf ( "STUN unexpected address %T %v" , addr , addr )
2020-03-04 20:24:07 +00:00
stunReadError . Add ( 1 )
2020-02-27 03:11:14 +00:00
continue
}
pkt := buf [ : n ]
if ! stun . Is ( pkt ) {
2020-03-04 20:24:07 +00:00
stunNotSTUN . Add ( 1 )
2020-02-27 03:11:14 +00:00
continue
}
txid , err := stun . ParseBindingRequest ( pkt )
if err != nil {
2020-03-04 20:24:07 +00:00
stunNotSTUN . Add ( 1 )
2020-02-27 03:11:14 +00:00
continue
}
if ua . IP . To4 ( ) != nil {
2020-03-04 20:24:07 +00:00
stunIPv4 . Add ( 1 )
2020-02-27 03:11:14 +00:00
} else {
2020-03-04 20:24:07 +00:00
stunIPv6 . Add ( 1 )
2020-02-27 03:11:14 +00:00
}
res := stun . Response ( txid , ua . IP , uint16 ( ua . Port ) )
_ , err = pc . WriteTo ( res , addr )
if err != nil {
2020-03-04 20:24:07 +00:00
stunWriteError . Add ( 1 )
2020-02-27 03:11:14 +00:00
} else {
2020-03-04 20:24:07 +00:00
stunSuccess . Add ( 1 )
2020-02-27 03:11:14 +00:00
}
}
}
2020-03-02 16:55:44 +00:00
2020-06-01 22:19:41 +00:00
var validProdHostname = regexp . MustCompile ( ` ^derp([^.]*)\.tailscale\.com\.?$ ` )
2020-03-02 16:55:44 +00:00
func prodAutocertHostPolicy ( _ context . Context , host string ) error {
if validProdHostname . MatchString ( host ) {
return nil
}
return errors . New ( "invalid hostname" )
}
2020-06-01 22:19:41 +00:00
func defaultMeshPSKFile ( ) string {
const def = "/home/derp/keys/derp-mesh.key"
if _ , err := os . Stat ( def ) ; err == nil {
return def
}
return ""
}