From 8f0b44b8a436c332d8ca8199e5de05da189c17c7 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 2 Feb 2018 19:15:28 -0700 Subject: [PATCH 01/28] Create diagnostics package; persist UUID --- caddy/caddymain/run.go | 57 ++++++++++++++++++++--- diagnostics/diagnostics.go | 25 ++++++++++ vendor/github.com/google/uuid/hash.go | 2 +- vendor/github.com/google/uuid/node.go | 31 ++++-------- vendor/github.com/google/uuid/node_js.go | 12 +++++ vendor/github.com/google/uuid/node_net.go | 33 +++++++++++++ vendor/github.com/google/uuid/time.go | 6 +-- vendor/github.com/google/uuid/uuid.go | 19 +++++--- vendor/github.com/google/uuid/version4.go | 2 +- vendor/manifest | 2 +- 10 files changed, 148 insertions(+), 41 deletions(-) create mode 100644 diagnostics/diagnostics.go create mode 100644 vendor/github.com/google/uuid/node_js.go create mode 100644 vendor/github.com/google/uuid/node_net.go diff --git a/caddy/caddymain/run.go b/caddy/caddymain/run.go index e6faa0513..f1415b820 100644 --- a/caddy/caddymain/run.go +++ b/caddy/caddymain/run.go @@ -21,19 +21,19 @@ import ( "io/ioutil" "log" "os" + "path/filepath" "runtime" "strconv" "strings" + "github.com/google/uuid" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddytls" + "github.com/mholt/caddy/diagnostics" + "github.com/xenolf/lego/acme" "gopkg.in/natefinch/lumberjack.v2" - "github.com/xenolf/lego/acme" - - "github.com/mholt/caddy" - // plug in the HTTP server type - _ "github.com/mholt/caddy/caddyhttp" - - "github.com/mholt/caddy/caddytls" + _ "github.com/mholt/caddy/caddyhttp" // plug in the HTTP server type // This is where other plugins get plugged in (imported) ) @@ -87,6 +87,9 @@ func Run() { }) } + // initialize diagnostics client + initDiagnostics() + // Check for one-time actions if revoke != "" { err := caddytls.Revoke(revoke) @@ -266,6 +269,46 @@ func setCPU(cpu string) error { return nil } +// initDiagnostics initializes the diagnostics engine. +func initDiagnostics() { + uuidFilename := filepath.Join(caddy.AssetsPath(), "uuid") + + newUUID := func() uuid.UUID { + id := uuid.New() + err := ioutil.WriteFile(uuidFilename, id[:], 0644) + if err != nil { + log.Printf("[ERROR] Persisting instance UUID: %v", err) + } + return id + } + + var id uuid.UUID + + // load UUID from storage, or create one if we don't have one + if uuidFile, err := os.Open(uuidFilename); os.IsNotExist(err) { + // no UUID exists yet; create a new one and persist it + id = newUUID() + } else if err != nil { + log.Printf("[ERROR] Loading persistent UUID: %v", err) + id = newUUID() + } else { + defer uuidFile.Close() + uuidBytes, err := ioutil.ReadAll(uuidFile) + if err != nil { + log.Printf("[ERROR] Reading persistent UUID: %v", err) + id = newUUID() + } else { + id, err = uuid.FromBytes(uuidBytes) + if err != nil { + log.Printf("[ERROR] Parsing UUID: %v", err) + id = newUUID() + } + } + } + + diagnostics.Init(id) +} + const appName = "Caddy" // Flags that control program flow or startup diff --git a/diagnostics/diagnostics.go b/diagnostics/diagnostics.go new file mode 100644 index 000000000..a1d050d4b --- /dev/null +++ b/diagnostics/diagnostics.go @@ -0,0 +1,25 @@ +// Copyright 2015 Light Code Labs, LLC +// +// 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 diagnostics + +import ( + "github.com/google/uuid" +) + +func Init(uuid uuid.UUID) { + instanceUUID = uuid +} + +var instanceUUID uuid.UUID diff --git a/vendor/github.com/google/uuid/hash.go b/vendor/github.com/google/uuid/hash.go index 4fc5a77df..b17461631 100644 --- a/vendor/github.com/google/uuid/hash.go +++ b/vendor/github.com/google/uuid/hash.go @@ -27,7 +27,7 @@ var ( func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID { h.Reset() h.Write(space[:]) - h.Write([]byte(data)) + h.Write(data) s := h.Sum(nil) var uuid UUID copy(uuid[:], s) diff --git a/vendor/github.com/google/uuid/node.go b/vendor/github.com/google/uuid/node.go index 5f0156a2e..384f07d02 100644 --- a/vendor/github.com/google/uuid/node.go +++ b/vendor/github.com/google/uuid/node.go @@ -5,16 +5,14 @@ package uuid import ( - "net" "sync" ) var ( - nodeMu sync.Mutex - interfaces []net.Interface // cached list of interfaces - ifname string // name of interface being used - nodeID [6]byte // hardware for version 1 UUIDs - zeroID [6]byte // nodeID with only 0's + nodeMu sync.Mutex + ifname string // name of interface being used + nodeID [6]byte // hardware for version 1 UUIDs + zeroID [6]byte // nodeID with only 0's ) // NodeInterface returns the name of the interface from which the NodeID was @@ -39,20 +37,12 @@ func SetNodeInterface(name string) bool { } func setNodeInterface(name string) bool { - if interfaces == nil { - var err error - interfaces, err = net.Interfaces() - if err != nil && name != "" { - return false - } - } - for _, ifs := range interfaces { - if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) { - copy(nodeID[:], ifs.HardwareAddr) - ifname = ifs.Name - return true - } + iname, addr := getHardwareInterface(name) // null implementation for js + if iname != "" && addr != nil { + ifname = iname + copy(nodeID[:], addr) + return true } // We found no interfaces with a valid hardware address. If name @@ -94,9 +84,6 @@ func SetNodeID(id []byte) bool { // NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is // not valid. The NodeID is only well defined for version 1 and 2 UUIDs. func (uuid UUID) NodeID() []byte { - if len(uuid) != 16 { - return nil - } var node [6]byte copy(node[:], uuid[10:]) return node[:] diff --git a/vendor/github.com/google/uuid/node_js.go b/vendor/github.com/google/uuid/node_js.go new file mode 100644 index 000000000..24b78edc9 --- /dev/null +++ b/vendor/github.com/google/uuid/node_js.go @@ -0,0 +1,12 @@ +// Copyright 2017 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build js + +package uuid + +// getHardwareInterface returns nil values for the JS version of the code. +// This remvoves the "net" dependency, because it is not used in the browser. +// Using the "net" library inflates the size of the transpiled JS code by 673k bytes. +func getHardwareInterface(name string) (string, []byte) { return "", nil } diff --git a/vendor/github.com/google/uuid/node_net.go b/vendor/github.com/google/uuid/node_net.go new file mode 100644 index 000000000..0cbbcddbd --- /dev/null +++ b/vendor/github.com/google/uuid/node_net.go @@ -0,0 +1,33 @@ +// Copyright 2017 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !js + +package uuid + +import "net" + +var interfaces []net.Interface // cached list of interfaces + +// getHardwareInterface returns the name and hardware address of interface name. +// If name is "" then the name and hardware address of one of the system's +// interfaces is returned. If no interfaces are found (name does not exist or +// there are no interfaces) then "", nil is returned. +// +// Only addresses of at least 6 bytes are returned. +func getHardwareInterface(name string) (string, []byte) { + if interfaces == nil { + var err error + interfaces, err = net.Interfaces() + if err != nil { + return "", nil + } + } + for _, ifs := range interfaces { + if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) { + return ifs.Name, ifs.HardwareAddr + } + } + return "", nil +} diff --git a/vendor/github.com/google/uuid/time.go b/vendor/github.com/google/uuid/time.go index fd7fe0ac4..e6ef06cdc 100644 --- a/vendor/github.com/google/uuid/time.go +++ b/vendor/github.com/google/uuid/time.go @@ -86,7 +86,7 @@ func clockSequence() int { return int(clockSeq & 0x3fff) } -// SetClockSeq sets the clock sequence to the lower 14 bits of seq. Setting to +// SetClockSequence sets the clock sequence to the lower 14 bits of seq. Setting to // -1 causes a new sequence to be generated. func SetClockSequence(seq int) { defer timeMu.Unlock() @@ -100,9 +100,9 @@ func setClockSequence(seq int) { randomBits(b[:]) // clock sequence seq = int(b[0])<<8 | int(b[1]) } - old_seq := clockSeq + oldSeq := clockSeq clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant - if old_seq != clockSeq { + if oldSeq != clockSeq { lasttime = 0 } } diff --git a/vendor/github.com/google/uuid/uuid.go b/vendor/github.com/google/uuid/uuid.go index 23161a86c..7f3643fe9 100644 --- a/vendor/github.com/google/uuid/uuid.go +++ b/vendor/github.com/google/uuid/uuid.go @@ -58,11 +58,11 @@ func Parse(s string) (UUID, error) { 14, 16, 19, 21, 24, 26, 28, 30, 32, 34} { - if v, ok := xtob(s[x], s[x+1]); !ok { + v, ok := xtob(s[x], s[x+1]) + if !ok { return uuid, errors.New("invalid UUID format") - } else { - uuid[i] = v } + uuid[i] = v } return uuid, nil } @@ -88,15 +88,22 @@ func ParseBytes(b []byte) (UUID, error) { 14, 16, 19, 21, 24, 26, 28, 30, 32, 34} { - if v, ok := xtob(b[x], b[x+1]); !ok { + v, ok := xtob(b[x], b[x+1]) + if !ok { return uuid, errors.New("invalid UUID format") - } else { - uuid[i] = v } + uuid[i] = v } return uuid, nil } +// FromBytes creates a new UUID from a byte slice. Returns an error if the slice +// does not have a length of 16. The bytes are copied from the slice. +func FromBytes(b []byte) (uuid UUID, err error) { + err = uuid.UnmarshalBinary(b) + return uuid, err +} + // Must returns uuid if err is nil and panics otherwise. func Must(uuid UUID, err error) UUID { if err != nil { diff --git a/vendor/github.com/google/uuid/version4.go b/vendor/github.com/google/uuid/version4.go index 74c4e6c9f..84af91c9f 100644 --- a/vendor/github.com/google/uuid/version4.go +++ b/vendor/github.com/google/uuid/version4.go @@ -14,7 +14,7 @@ func New() UUID { return Must(NewRandom()) } -// NewRandom returns a Random (Version 4) UUID or panics. +// NewRandom returns a Random (Version 4) UUID. // // The strength of the UUIDs is based on the strength of the crypto/rand // package. diff --git a/vendor/manifest b/vendor/manifest index 5f0d211c0..a44a451e6 100644 --- a/vendor/manifest +++ b/vendor/manifest @@ -80,7 +80,7 @@ "importpath": "github.com/google/uuid", "repository": "https://github.com/google/uuid", "vcs": "git", - "revision": "7e072fc3a7be179aee6d3359e46015aa8c995314", + "revision": "dec09d789f3dba190787f8b4454c7d3c936fed9e", "branch": "master", "notests": true }, From 388ff6bc0ad0f3e646682e83a8074fffcf74fc28 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 8 Feb 2018 19:55:10 -0700 Subject: [PATCH 02/28] diagnostics: Implemented collection functions and create first metrics - Also implemented robust error handling and failovers - Vendored klauspost/cpuid --- caddy/caddymain/run.go | 24 +- caddyhttp/httpserver/plugin.go | 3 + caddyhttp/httpserver/server.go | 3 + caddytls/client.go | 6 +- diagnostics/collection.go | 250 ++++ diagnostics/diagnostics.go | 242 +++- vendor/github.com/klauspost/cpuid/LICENSE | 22 + vendor/github.com/klauspost/cpuid/cpuid.go | 1030 +++++++++++++++++ vendor/github.com/klauspost/cpuid/cpuid_386.s | 42 + .../github.com/klauspost/cpuid/cpuid_amd64.s | 42 + .../klauspost/cpuid/detect_intel.go | 17 + .../github.com/klauspost/cpuid/detect_ref.go | 23 + vendor/github.com/klauspost/cpuid/generate.go | 4 + .../github.com/klauspost/cpuid/private-gen.go | 476 ++++++++ .../klauspost/cpuid/private/cpuid.go | 1024 ++++++++++++++++ .../klauspost/cpuid/private/cpuid_386.s | 42 + .../klauspost/cpuid/private/cpuid_amd64.s | 42 + .../cpuid/private/cpuid_detect_intel.go | 17 + .../cpuid/private/cpuid_detect_ref.go | 23 + vendor/manifest | 8 + 20 files changed, 3336 insertions(+), 4 deletions(-) create mode 100644 diagnostics/collection.go create mode 100644 vendor/github.com/klauspost/cpuid/LICENSE create mode 100644 vendor/github.com/klauspost/cpuid/cpuid.go create mode 100644 vendor/github.com/klauspost/cpuid/cpuid_386.s create mode 100644 vendor/github.com/klauspost/cpuid/cpuid_amd64.s create mode 100644 vendor/github.com/klauspost/cpuid/detect_intel.go create mode 100644 vendor/github.com/klauspost/cpuid/detect_ref.go create mode 100644 vendor/github.com/klauspost/cpuid/generate.go create mode 100644 vendor/github.com/klauspost/cpuid/private-gen.go create mode 100644 vendor/github.com/klauspost/cpuid/private/cpuid.go create mode 100644 vendor/github.com/klauspost/cpuid/private/cpuid_386.s create mode 100644 vendor/github.com/klauspost/cpuid/private/cpuid_amd64.s create mode 100644 vendor/github.com/klauspost/cpuid/private/cpuid_detect_intel.go create mode 100644 vendor/github.com/klauspost/cpuid/private/cpuid_detect_ref.go diff --git a/caddy/caddymain/run.go b/caddy/caddymain/run.go index f1415b820..6ffcd5c6c 100644 --- a/caddy/caddymain/run.go +++ b/caddy/caddymain/run.go @@ -27,6 +27,7 @@ import ( "strings" "github.com/google/uuid" + "github.com/klauspost/cpuid" "github.com/mholt/caddy" "github.com/mholt/caddy/caddytls" "github.com/mholt/caddy/diagnostics" @@ -51,6 +52,7 @@ func init() { flag.StringVar(&caddytls.DefaultEmail, "email", "", "Default ACME CA account email address") flag.DurationVar(&acme.HTTPClient.Timeout, "catimeout", acme.HTTPClient.Timeout, "Default ACME CA HTTP timeout") flag.StringVar(&logfile, "log", "", "Process log file") + flag.BoolVar(&noDiag, "no-diagnostics", false, "Disable diagnostic reporting") flag.StringVar(&caddy.PidFile, "pidfile", "", "Path to write pid file") flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)") flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate") @@ -88,7 +90,9 @@ func Run() { } // initialize diagnostics client - initDiagnostics() + if !noDiag { + initDiagnostics() + } // Check for one-time actions if revoke != "" { @@ -146,6 +150,23 @@ func Run() { // Execute instantiation events caddy.EmitEvent(caddy.InstanceStartupEvent, instance) + // Begin diagnostics (these are no-ops if diagnostics disabled) + diagnostics.Set("caddy_version", appVersion) + // TODO: plugins + diagnostics.Set("num_listeners", len(instance.Servers())) + diagnostics.Set("os", runtime.GOOS) + diagnostics.Set("arch", runtime.GOARCH) + diagnostics.Set("cpu", struct { + NumLogical int `json:"num_logical"` + AESNI bool `json:"aes_ni"` + BrandName string `json:"brand_name"` + }{ + NumLogical: runtime.NumCPU(), + AESNI: cpuid.CPU.AesNi(), + BrandName: cpuid.CPU.BrandName, + }) + diagnostics.StartEmitting() + // Twiddle your thumbs instance.Wait() } @@ -321,6 +342,7 @@ var ( version bool plugins bool validate bool + noDiag bool ) // Build information obtained with the help of -ldflags diff --git a/caddyhttp/httpserver/plugin.go b/caddyhttp/httpserver/plugin.go index 643eea7f7..58a636196 100644 --- a/caddyhttp/httpserver/plugin.go +++ b/caddyhttp/httpserver/plugin.go @@ -29,6 +29,7 @@ import ( "github.com/mholt/caddy/caddyfile" "github.com/mholt/caddy/caddyhttp/staticfiles" "github.com/mholt/caddy/caddytls" + "github.com/mholt/caddy/diagnostics" ) const serverType = "http" @@ -205,6 +206,8 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) { } } + diagnostics.Set("num_sites", len(h.siteConfigs)) + // we must map (group) each config to a bind address groups, err := groupSiteConfigsByListenAddr(h.siteConfigs) if err != nil { diff --git a/caddyhttp/httpserver/server.go b/caddyhttp/httpserver/server.go index 92f2b6fd7..5033bb21e 100644 --- a/caddyhttp/httpserver/server.go +++ b/caddyhttp/httpserver/server.go @@ -36,6 +36,7 @@ import ( "github.com/mholt/caddy" "github.com/mholt/caddy/caddyhttp/staticfiles" "github.com/mholt/caddy/caddytls" + "github.com/mholt/caddy/diagnostics" ) // Server is the HTTP server implementation. @@ -345,6 +346,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } }() + go diagnostics.AppendUniqueString("user_agent", r.Header.Get("User-Agent")) + // copy the original, unchanged URL into the context // so it can be referenced by middlewares urlCopy := *r.URL diff --git a/caddytls/client.go b/caddytls/client.go index 26ef6a3c5..44f394807 100644 --- a/caddytls/client.go +++ b/caddytls/client.go @@ -26,6 +26,7 @@ import ( "time" "github.com/mholt/caddy" + "github.com/mholt/caddy/diagnostics" "github.com/xenolf/lego/acme" ) @@ -276,6 +277,8 @@ Attempts: break } + go diagnostics.Increment("acme_certificates_obtained") + return nil } @@ -350,8 +353,9 @@ func (c *ACMEClient) Renew(name string) error { return errors.New("too many renewal attempts; last error: " + err.Error()) } - // Executes Cert renew events caddy.EmitEvent(caddy.CertRenewEvent, name) + go diagnostics.Increment("acme_certificates_obtained") + go diagnostics.Increment("acme_certificates_renewed") return saveCertResource(storage, newCertMeta) } diff --git a/diagnostics/collection.go b/diagnostics/collection.go new file mode 100644 index 000000000..40e42e5b7 --- /dev/null +++ b/diagnostics/collection.go @@ -0,0 +1,250 @@ +// Copyright 2015 Light Code Labs, LLC +// +// 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 diagnostics + +import ( + "log" + + "github.com/google/uuid" +) + +// Init initializes this package so that it may +// be used. Do not call this function more than +// once. Init panics if it is called more than +// once or if the UUID value is empty. Once this +// function is called, the rest of the package +// may safely be used. If this function is not +// called, the collector functions may still be +// invoked, but they will be no-ops. +func Init(instanceID uuid.UUID) { + if enabled { + panic("already initialized") + } + if instanceID.String() == "" { + panic("empty UUID") + } + instanceUUID = instanceID + enabled = true +} + +// StartEmitting sends the current payload and begins the +// transmission cycle for updates. This is the first +// update sent, and future ones will be sent until +// StopEmitting is called. +// +// This function is non-blocking (it spawns a new goroutine). +// +// This function panics if it was called more than once. +// It is a no-op if this package was not initialized. +func StartEmitting() { + if !enabled { + return + } + updateTimerMu.Lock() + if updateTimer != nil { + updateTimerMu.Unlock() + panic("updates already started") + } + updateTimerMu.Unlock() + updateMu.Lock() + if updating { + updateMu.Unlock() + panic("update already in progress") + } + updateMu.Unlock() + go logEmit(false) +} + +// StopEmitting sends the current payload and terminates +// the update cycle. No more updates will be sent. +// +// It is a no-op if the package was never initialized +// or if emitting was never started. +func StopEmitting() { + if !enabled { + return + } + updateTimerMu.Lock() + if updateTimer == nil { + updateTimerMu.Unlock() + return + } + updateTimerMu.Unlock() + logEmit(true) +} + +// Set puts a value in the buffer to be included +// in the next emission. It overwrites any +// previous value. +// +// This function is safe for multiple goroutines, +// and it is recommended to call this using the +// go keyword after the call to SendHello so it +// doesn't block crucial code. +func Set(key string, val interface{}) { + if !enabled { + return + } + bufferMu.Lock() + if bufferItemCount >= maxBufferItems { + bufferMu.Unlock() + return + } + if _, ok := buffer[key]; !ok { + bufferItemCount++ + } + buffer[key] = val + bufferMu.Unlock() +} + +// Append appends value to a list named key. +// If key is new, a new list will be created. +// If key maps to a type that is not a list, +// an error is logged, and this is a no-op. +// +// TODO: is this function needed/useful? +func Append(key string, value interface{}) { + if !enabled { + return + } + bufferMu.Lock() + if bufferItemCount >= maxBufferItems { + bufferMu.Unlock() + return + } + // TODO: Test this... + bufVal, inBuffer := buffer[key] + sliceVal, sliceOk := bufVal.([]interface{}) + if inBuffer && !sliceOk { + bufferMu.Unlock() + log.Printf("[PANIC] Diagnostics: key %s already used for non-slice value", key) + return + } + if sliceVal == nil { + buffer[key] = []interface{}{value} + } else if sliceOk { + buffer[key] = append(sliceVal, value) + } + bufferItemCount++ + bufferMu.Unlock() +} + +// AppendUniqueString adds value to a set named key. +// Set items are unordered. Values in the set +// are unique, but repeat values are counted. +// +// If key is new, a new set will be created. +// If key maps to a type that is not a string +// set, an error is logged, and this is a no-op. +func AppendUniqueString(key, value string) { + if !enabled { + return + } + bufferMu.Lock() + if bufferItemCount >= maxBufferItems { + bufferMu.Unlock() + return + } + bufVal, inBuffer := buffer[key] + mapVal, mapOk := bufVal.(map[string]int) + if inBuffer && !mapOk { + bufferMu.Unlock() + log.Printf("[PANIC] Diagnostics: key %s already used for non-map value", key) + return + } + if mapVal == nil { + buffer[key] = map[string]int{value: 1} + bufferItemCount++ + } else if mapOk { + mapVal[value]++ + } + bufferMu.Unlock() +} + +// AppendUniqueInt adds value to a set named key. +// Set items are unordered. Values in the set +// are unique, but repeat values are counted. +// +// If key is new, a new set will be created. +// If key maps to a type that is not an integer +// set, an error is logged, and this is a no-op. +func AppendUniqueInt(key string, value int) { + if !enabled { + return + } + bufferMu.Lock() + if bufferItemCount >= maxBufferItems { + bufferMu.Unlock() + return + } + bufVal, inBuffer := buffer[key] + mapVal, mapOk := bufVal.(map[int]int) + if inBuffer && !mapOk { + bufferMu.Unlock() + log.Printf("[PANIC] Diagnostics: key %s already used for non-map value", key) + return + } + if mapVal == nil { + buffer[key] = map[int]int{value: 1} + bufferItemCount++ + } else if mapOk { + mapVal[value]++ + } + bufferMu.Unlock() +} + +// Increment adds 1 to a value named key. +// If it does not exist, it is created with +// a value of 1. If key maps to a type that +// is not an integer, an error is logged, +// and this is a no-op. +func Increment(key string) { + incrementOrDecrement(key, true) +} + +// Decrement is the same as increment except +// it subtracts 1. +func Decrement(key string) { + incrementOrDecrement(key, false) +} + +// inc == true: increment +// inc == false: decrement +func incrementOrDecrement(key string, inc bool) { + if !enabled { + return + } + bufferMu.Lock() + bufVal, inBuffer := buffer[key] + intVal, intOk := bufVal.(int) + if inBuffer && !intOk { + bufferMu.Unlock() + log.Printf("[PANIC] Diagnostics: key %s already used for non-integer value", key) + return + } + if !inBuffer { + if bufferItemCount >= maxBufferItems { + bufferMu.Unlock() + return + } + bufferItemCount++ + } + if inc { + buffer[key] = intVal + 1 + } else { + buffer[key] = intVal - 1 + } + bufferMu.Unlock() +} diff --git a/diagnostics/diagnostics.go b/diagnostics/diagnostics.go index a1d050d4b..dd4cac87d 100644 --- a/diagnostics/diagnostics.go +++ b/diagnostics/diagnostics.go @@ -12,14 +12,252 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package diagnostics implements the client for server-side diagnostics +// of the network. Functions in this package are synchronous and blocking +// unless otherwise specified. For convenience, most functions here do +// not return errors, but errors are logged to the standard logger. +// +// To use this package, first call Init(). You can then call any of the +// collection/aggregation functions. Call StartEmitting() when you are +// ready to begin sending diagnostic updates. +// +// When collecting metrics (functions like Set, Append*, or Increment), +// it may be desirable and even recommended to run invoke them in a new +// goroutine (use the go keyword) in case there is lock contention; +// they are thread-safe (unless noted), and you may not want them to +// block the main thread of execution. However, sometimes blocking +// may be necessary too; for example, adding startup metrics to the +// buffer before the call to StartEmitting(). package diagnostics import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" + "strings" + "sync" + "time" + "github.com/google/uuid" ) -func Init(uuid uuid.UUID) { - instanceUUID = uuid +// logEmit calls emit and then logs the error, if any. +func logEmit(final bool) { + err := emit(final) + if err != nil { + log.Printf("[ERROR] Sending diganostics: %v", err) + } } +// emit sends an update to the diagnostics server. +// If final is true, no future updates will be scheduled. +// Otherwise, the next update will be scheduled. +func emit(final bool) error { + if !enabled { + return fmt.Errorf("diagnostics not enabled") + } + + // ensure only one update happens at a time; + // skip update if previous one still in progress + updateMu.Lock() + if updating { + updateMu.Unlock() + log.Println("[NOTICE] Skipping this diagnostics update because previous one is still working") + return nil + } + updating = true + updateMu.Unlock() + defer func() { + updateMu.Lock() + updating = false + updateMu.Unlock() + }() + + // terminate any pending update if this is the last one + if final { + updateTimerMu.Lock() + updateTimer.Stop() + updateTimer = nil + updateTimerMu.Unlock() + } + + payloadBytes, err := makePayloadAndResetBuffer() + if err != nil { + return err + } + + // this will hold the server's reply + var reply Response + + // transmit the payload - use a loop to retry in case of failure + for i := 0; i < 4; i++ { + if i > 0 && err != nil { + // don't hammer the server; first failure might have been + // a fluke, but back off more after that + log.Printf("[WARNING] Sending diagnostics (attempt %d): %v - waiting and retrying", i, err) + time.Sleep(time.Duration(i*i*i) * time.Second) + } + + // send it + var resp *http.Response + resp, err = httpClient.Post(endpoint+instanceUUID.String(), "application/json", bytes.NewReader(payloadBytes)) + if err != nil { + continue + } + + // ensure we can read the response + if ct := resp.Header.Get("Content-Type"); (resp.StatusCode < 300 || resp.StatusCode >= 400) && + !strings.Contains(ct, "json") { + err = fmt.Errorf("diagnostics server replied with unknown content-type: %s", ct) + resp.Body.Close() + continue + } + + // read the response body + err = json.NewDecoder(resp.Body).Decode(&reply) + resp.Body.Close() // close response body as soon as we're done with it + if err != nil { + continue + } + + // ensure we won't slam the diagnostics server + if reply.NextUpdate < 1*time.Second { + reply.NextUpdate = defaultUpdateInterval + } + + // make sure we didn't send the update too soon; if so, + // just wait and try again -- this is a special case of + // error that we handle differently, as you can see + if resp.StatusCode == http.StatusTooManyRequests { + log.Printf("[NOTICE] Sending diagnostics: we were too early; waiting %s before trying again", reply.NextUpdate) + time.Sleep(reply.NextUpdate) + continue + } else if resp.StatusCode >= 400 { + err = fmt.Errorf("diagnostics server returned status code %d", resp.StatusCode) + continue + } + + break + } + if err == nil { + // (remember, if there was an error, we return it + // below, so it will get logged if it's supposed to) + log.Println("[INFO] Sending diagnostics: success") + } + + // even if there was an error after retrying, we should + // schedule the next update using our default update + // interval because the server might be healthy later + + // schedule the next update (if this wasn't the last one and + // if the remote server didn't tell us to stop sending) + if !final && !reply.Stop { + updateTimerMu.Lock() + updateTimer = time.AfterFunc(reply.NextUpdate, func() { + logEmit(false) + }) + updateTimerMu.Unlock() + } + + return err +} + +// makePayloadAndResetBuffer prepares a payload +// by emptying the collection buffer. It returns +// the bytes of the payload to send to the server. +// Since the buffer is reset by this, if the +// resulting byte slice is lost, the payload is +// gone with it. +func makePayloadAndResetBuffer() ([]byte, error) { + // make a local pointer to the buffer, then reset + // the buffer to an empty map to clear it out + bufferMu.Lock() + bufCopy := buffer + buffer = make(map[string]interface{}) + bufferItemCount = 0 + bufferMu.Unlock() + + // encode payload in preparation for transmission + payload := Payload{ + InstanceID: instanceUUID.String(), + Timestamp: time.Now().UTC(), + Data: bufCopy, + } + return json.Marshal(payload) +} + +// Response contains the body of a response from the +// diagnostics server. +type Response struct { + // NextUpdate is how long to wait before the next update. + NextUpdate time.Duration `json:"next_update"` + + // Stop instructs the diagnostics server to stop sending + // diagnostics. This would only be done under extenuating + // circumstances, but we are prepared for it nonetheless. + Stop bool `json:"stop,omitempty"` + + // Error will be populated with an error message, if any. + // This field should be empty if the status code is < 400. + Error string `json:"error,omitempty"` +} + +// Payload is the data that gets sent to the diagnostics server. +type Payload struct { + // The universally unique ID of the instance + InstanceID string `json:"instance_id"` + + // The UTC timestamp of the transmission + Timestamp time.Time `json:"timestamp"` + + // The metrics + Data map[string]interface{} `json:"data,omitempty"` +} + +// httpClient should be used for HTTP requests. It +// is configured with a timeout for reliability. +var httpClient = http.Client{Timeout: 1 * time.Minute} + +// buffer holds the data that we are building up to send. +var buffer = make(map[string]interface{}) +var bufferItemCount = 0 +var bufferMu sync.RWMutex // protects both the buffer and its count + +// updating is used to ensure only one +// update happens at a time. +var updating bool +var updateMu sync.Mutex + +// updateTimer fires off the next update. +// If no update is scheduled, this is nil. +var updateTimer *time.Timer +var updateTimerMu sync.Mutex + +// instanceUUID is the ID of the current instance. +// This MUST be set to emit diagnostics. var instanceUUID uuid.UUID + +// enabled indicates whether the package has +// been initialized and can be actively used. +var enabled bool + +const ( + // endpoint is the base URL to remote diagnostics server; + // the instance ID will be appended to it. + endpoint = "http://localhost:8081/update/" + + // defaultUpdateInterval is how long to wait before emitting + // more diagnostic data. This value is only used if the + // client receives a nonsensical value, or doesn't send one + // at all, indicating a likely problem with the server. Thus, + // this value should be a long duration to help alleviate + // extra load on the server. + defaultUpdateInterval = 1 * time.Hour + + // maxBufferItems is the maximum number of items we'll allow + // in the buffer before we start dropping new ones, in a + // rough (simple) attempt to keep memory use under control. + maxBufferItems = 100000 +) diff --git a/vendor/github.com/klauspost/cpuid/LICENSE b/vendor/github.com/klauspost/cpuid/LICENSE new file mode 100644 index 000000000..5cec7ee94 --- /dev/null +++ b/vendor/github.com/klauspost/cpuid/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Klaus Post + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/github.com/klauspost/cpuid/cpuid.go b/vendor/github.com/klauspost/cpuid/cpuid.go new file mode 100644 index 000000000..44e50946f --- /dev/null +++ b/vendor/github.com/klauspost/cpuid/cpuid.go @@ -0,0 +1,1030 @@ +// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file. + +// Package cpuid provides information about the CPU running the current program. +// +// CPU features are detected on startup, and kept for fast access through the life of the application. +// Currently x86 / x64 (AMD64) is supported. +// +// You can access the CPU information by accessing the shared CPU variable of the cpuid library. +// +// Package home: https://github.com/klauspost/cpuid +package cpuid + +import "strings" + +// Vendor is a representation of a CPU vendor. +type Vendor int + +const ( + Other Vendor = iota + Intel + AMD + VIA + Transmeta + NSC + KVM // Kernel-based Virtual Machine + MSVM // Microsoft Hyper-V or Windows Virtual PC + VMware + XenHVM +) + +const ( + CMOV = 1 << iota // i686 CMOV + NX // NX (No-Execute) bit + AMD3DNOW // AMD 3DNOW + AMD3DNOWEXT // AMD 3DNowExt + MMX // standard MMX + MMXEXT // SSE integer functions or AMD MMX ext + SSE // SSE functions + SSE2 // P4 SSE functions + SSE3 // Prescott SSE3 functions + SSSE3 // Conroe SSSE3 functions + SSE4 // Penryn SSE4.1 functions + SSE4A // AMD Barcelona microarchitecture SSE4a instructions + SSE42 // Nehalem SSE4.2 functions + AVX // AVX functions + AVX2 // AVX2 functions + FMA3 // Intel FMA 3 + FMA4 // Bulldozer FMA4 functions + XOP // Bulldozer XOP functions + F16C // Half-precision floating-point conversion + BMI1 // Bit Manipulation Instruction Set 1 + BMI2 // Bit Manipulation Instruction Set 2 + TBM // AMD Trailing Bit Manipulation + LZCNT // LZCNT instruction + POPCNT // POPCNT instruction + AESNI // Advanced Encryption Standard New Instructions + CLMUL // Carry-less Multiplication + HTT // Hyperthreading (enabled) + HLE // Hardware Lock Elision + RTM // Restricted Transactional Memory + RDRAND // RDRAND instruction is available + RDSEED // RDSEED instruction is available + ADX // Intel ADX (Multi-Precision Add-Carry Instruction Extensions) + SHA // Intel SHA Extensions + AVX512F // AVX-512 Foundation + AVX512DQ // AVX-512 Doubleword and Quadword Instructions + AVX512IFMA // AVX-512 Integer Fused Multiply-Add Instructions + AVX512PF // AVX-512 Prefetch Instructions + AVX512ER // AVX-512 Exponential and Reciprocal Instructions + AVX512CD // AVX-512 Conflict Detection Instructions + AVX512BW // AVX-512 Byte and Word Instructions + AVX512VL // AVX-512 Vector Length Extensions + AVX512VBMI // AVX-512 Vector Bit Manipulation Instructions + MPX // Intel MPX (Memory Protection Extensions) + ERMS // Enhanced REP MOVSB/STOSB + RDTSCP // RDTSCP Instruction + CX16 // CMPXCHG16B Instruction + SGX // Software Guard Extensions + + // Performance indicators + SSE2SLOW // SSE2 is supported, but usually not faster + SSE3SLOW // SSE3 is supported, but usually not faster + ATOM // Atom processor, some SSSE3 instructions are slower +) + +var flagNames = map[Flags]string{ + CMOV: "CMOV", // i686 CMOV + NX: "NX", // NX (No-Execute) bit + AMD3DNOW: "AMD3DNOW", // AMD 3DNOW + AMD3DNOWEXT: "AMD3DNOWEXT", // AMD 3DNowExt + MMX: "MMX", // Standard MMX + MMXEXT: "MMXEXT", // SSE integer functions or AMD MMX ext + SSE: "SSE", // SSE functions + SSE2: "SSE2", // P4 SSE2 functions + SSE3: "SSE3", // Prescott SSE3 functions + SSSE3: "SSSE3", // Conroe SSSE3 functions + SSE4: "SSE4.1", // Penryn SSE4.1 functions + SSE4A: "SSE4A", // AMD Barcelona microarchitecture SSE4a instructions + SSE42: "SSE4.2", // Nehalem SSE4.2 functions + AVX: "AVX", // AVX functions + AVX2: "AVX2", // AVX functions + FMA3: "FMA3", // Intel FMA 3 + FMA4: "FMA4", // Bulldozer FMA4 functions + XOP: "XOP", // Bulldozer XOP functions + F16C: "F16C", // Half-precision floating-point conversion + BMI1: "BMI1", // Bit Manipulation Instruction Set 1 + BMI2: "BMI2", // Bit Manipulation Instruction Set 2 + TBM: "TBM", // AMD Trailing Bit Manipulation + LZCNT: "LZCNT", // LZCNT instruction + POPCNT: "POPCNT", // POPCNT instruction + AESNI: "AESNI", // Advanced Encryption Standard New Instructions + CLMUL: "CLMUL", // Carry-less Multiplication + HTT: "HTT", // Hyperthreading (enabled) + HLE: "HLE", // Hardware Lock Elision + RTM: "RTM", // Restricted Transactional Memory + RDRAND: "RDRAND", // RDRAND instruction is available + RDSEED: "RDSEED", // RDSEED instruction is available + ADX: "ADX", // Intel ADX (Multi-Precision Add-Carry Instruction Extensions) + SHA: "SHA", // Intel SHA Extensions + AVX512F: "AVX512F", // AVX-512 Foundation + AVX512DQ: "AVX512DQ", // AVX-512 Doubleword and Quadword Instructions + AVX512IFMA: "AVX512IFMA", // AVX-512 Integer Fused Multiply-Add Instructions + AVX512PF: "AVX512PF", // AVX-512 Prefetch Instructions + AVX512ER: "AVX512ER", // AVX-512 Exponential and Reciprocal Instructions + AVX512CD: "AVX512CD", // AVX-512 Conflict Detection Instructions + AVX512BW: "AVX512BW", // AVX-512 Byte and Word Instructions + AVX512VL: "AVX512VL", // AVX-512 Vector Length Extensions + AVX512VBMI: "AVX512VBMI", // AVX-512 Vector Bit Manipulation Instructions + MPX: "MPX", // Intel MPX (Memory Protection Extensions) + ERMS: "ERMS", // Enhanced REP MOVSB/STOSB + RDTSCP: "RDTSCP", // RDTSCP Instruction + CX16: "CX16", // CMPXCHG16B Instruction + SGX: "SGX", // Software Guard Extensions + + // Performance indicators + SSE2SLOW: "SSE2SLOW", // SSE2 supported, but usually not faster + SSE3SLOW: "SSE3SLOW", // SSE3 supported, but usually not faster + ATOM: "ATOM", // Atom processor, some SSSE3 instructions are slower + +} + +// CPUInfo contains information about the detected system CPU. +type CPUInfo struct { + BrandName string // Brand name reported by the CPU + VendorID Vendor // Comparable CPU vendor ID + Features Flags // Features of the CPU + PhysicalCores int // Number of physical processor cores in your CPU. Will be 0 if undetectable. + ThreadsPerCore int // Number of threads per physical core. Will be 1 if undetectable. + LogicalCores int // Number of physical cores times threads that can run on each core through the use of hyperthreading. Will be 0 if undetectable. + Family int // CPU family number + Model int // CPU model number + CacheLine int // Cache line size in bytes. Will be 0 if undetectable. + Cache struct { + L1I int // L1 Instruction Cache (per core or shared). Will be -1 if undetected + L1D int // L1 Data Cache (per core or shared). Will be -1 if undetected + L2 int // L2 Cache (per core or shared). Will be -1 if undetected + L3 int // L3 Instruction Cache (per core or shared). Will be -1 if undetected + } + SGX SGXSupport + maxFunc uint32 + maxExFunc uint32 +} + +var cpuid func(op uint32) (eax, ebx, ecx, edx uint32) +var cpuidex func(op, op2 uint32) (eax, ebx, ecx, edx uint32) +var xgetbv func(index uint32) (eax, edx uint32) +var rdtscpAsm func() (eax, ebx, ecx, edx uint32) + +// CPU contains information about the CPU as detected on startup, +// or when Detect last was called. +// +// Use this as the primary entry point to you data, +// this way queries are +var CPU CPUInfo + +func init() { + initCPU() + Detect() +} + +// Detect will re-detect current CPU info. +// This will replace the content of the exported CPU variable. +// +// Unless you expect the CPU to change while you are running your program +// you should not need to call this function. +// If you call this, you must ensure that no other goroutine is accessing the +// exported CPU variable. +func Detect() { + CPU.maxFunc = maxFunctionID() + CPU.maxExFunc = maxExtendedFunction() + CPU.BrandName = brandName() + CPU.CacheLine = cacheLine() + CPU.Family, CPU.Model = familyModel() + CPU.Features = support() + CPU.SGX = hasSGX(CPU.Features&SGX != 0) + CPU.ThreadsPerCore = threadsPerCore() + CPU.LogicalCores = logicalCores() + CPU.PhysicalCores = physicalCores() + CPU.VendorID = vendorID() + CPU.cacheSize() +} + +// Generated here: http://play.golang.org/p/BxFH2Gdc0G + +// Cmov indicates support of CMOV instructions +func (c CPUInfo) Cmov() bool { + return c.Features&CMOV != 0 +} + +// Amd3dnow indicates support of AMD 3DNOW! instructions +func (c CPUInfo) Amd3dnow() bool { + return c.Features&AMD3DNOW != 0 +} + +// Amd3dnowExt indicates support of AMD 3DNOW! Extended instructions +func (c CPUInfo) Amd3dnowExt() bool { + return c.Features&AMD3DNOWEXT != 0 +} + +// MMX indicates support of MMX instructions +func (c CPUInfo) MMX() bool { + return c.Features&MMX != 0 +} + +// MMXExt indicates support of MMXEXT instructions +// (SSE integer functions or AMD MMX ext) +func (c CPUInfo) MMXExt() bool { + return c.Features&MMXEXT != 0 +} + +// SSE indicates support of SSE instructions +func (c CPUInfo) SSE() bool { + return c.Features&SSE != 0 +} + +// SSE2 indicates support of SSE 2 instructions +func (c CPUInfo) SSE2() bool { + return c.Features&SSE2 != 0 +} + +// SSE3 indicates support of SSE 3 instructions +func (c CPUInfo) SSE3() bool { + return c.Features&SSE3 != 0 +} + +// SSSE3 indicates support of SSSE 3 instructions +func (c CPUInfo) SSSE3() bool { + return c.Features&SSSE3 != 0 +} + +// SSE4 indicates support of SSE 4 (also called SSE 4.1) instructions +func (c CPUInfo) SSE4() bool { + return c.Features&SSE4 != 0 +} + +// SSE42 indicates support of SSE4.2 instructions +func (c CPUInfo) SSE42() bool { + return c.Features&SSE42 != 0 +} + +// AVX indicates support of AVX instructions +// and operating system support of AVX instructions +func (c CPUInfo) AVX() bool { + return c.Features&AVX != 0 +} + +// AVX2 indicates support of AVX2 instructions +func (c CPUInfo) AVX2() bool { + return c.Features&AVX2 != 0 +} + +// FMA3 indicates support of FMA3 instructions +func (c CPUInfo) FMA3() bool { + return c.Features&FMA3 != 0 +} + +// FMA4 indicates support of FMA4 instructions +func (c CPUInfo) FMA4() bool { + return c.Features&FMA4 != 0 +} + +// XOP indicates support of XOP instructions +func (c CPUInfo) XOP() bool { + return c.Features&XOP != 0 +} + +// F16C indicates support of F16C instructions +func (c CPUInfo) F16C() bool { + return c.Features&F16C != 0 +} + +// BMI1 indicates support of BMI1 instructions +func (c CPUInfo) BMI1() bool { + return c.Features&BMI1 != 0 +} + +// BMI2 indicates support of BMI2 instructions +func (c CPUInfo) BMI2() bool { + return c.Features&BMI2 != 0 +} + +// TBM indicates support of TBM instructions +// (AMD Trailing Bit Manipulation) +func (c CPUInfo) TBM() bool { + return c.Features&TBM != 0 +} + +// Lzcnt indicates support of LZCNT instruction +func (c CPUInfo) Lzcnt() bool { + return c.Features&LZCNT != 0 +} + +// Popcnt indicates support of POPCNT instruction +func (c CPUInfo) Popcnt() bool { + return c.Features&POPCNT != 0 +} + +// HTT indicates the processor has Hyperthreading enabled +func (c CPUInfo) HTT() bool { + return c.Features&HTT != 0 +} + +// SSE2Slow indicates that SSE2 may be slow on this processor +func (c CPUInfo) SSE2Slow() bool { + return c.Features&SSE2SLOW != 0 +} + +// SSE3Slow indicates that SSE3 may be slow on this processor +func (c CPUInfo) SSE3Slow() bool { + return c.Features&SSE3SLOW != 0 +} + +// AesNi indicates support of AES-NI instructions +// (Advanced Encryption Standard New Instructions) +func (c CPUInfo) AesNi() bool { + return c.Features&AESNI != 0 +} + +// Clmul indicates support of CLMUL instructions +// (Carry-less Multiplication) +func (c CPUInfo) Clmul() bool { + return c.Features&CLMUL != 0 +} + +// NX indicates support of NX (No-Execute) bit +func (c CPUInfo) NX() bool { + return c.Features&NX != 0 +} + +// SSE4A indicates support of AMD Barcelona microarchitecture SSE4a instructions +func (c CPUInfo) SSE4A() bool { + return c.Features&SSE4A != 0 +} + +// HLE indicates support of Hardware Lock Elision +func (c CPUInfo) HLE() bool { + return c.Features&HLE != 0 +} + +// RTM indicates support of Restricted Transactional Memory +func (c CPUInfo) RTM() bool { + return c.Features&RTM != 0 +} + +// Rdrand indicates support of RDRAND instruction is available +func (c CPUInfo) Rdrand() bool { + return c.Features&RDRAND != 0 +} + +// Rdseed indicates support of RDSEED instruction is available +func (c CPUInfo) Rdseed() bool { + return c.Features&RDSEED != 0 +} + +// ADX indicates support of Intel ADX (Multi-Precision Add-Carry Instruction Extensions) +func (c CPUInfo) ADX() bool { + return c.Features&ADX != 0 +} + +// SHA indicates support of Intel SHA Extensions +func (c CPUInfo) SHA() bool { + return c.Features&SHA != 0 +} + +// AVX512F indicates support of AVX-512 Foundation +func (c CPUInfo) AVX512F() bool { + return c.Features&AVX512F != 0 +} + +// AVX512DQ indicates support of AVX-512 Doubleword and Quadword Instructions +func (c CPUInfo) AVX512DQ() bool { + return c.Features&AVX512DQ != 0 +} + +// AVX512IFMA indicates support of AVX-512 Integer Fused Multiply-Add Instructions +func (c CPUInfo) AVX512IFMA() bool { + return c.Features&AVX512IFMA != 0 +} + +// AVX512PF indicates support of AVX-512 Prefetch Instructions +func (c CPUInfo) AVX512PF() bool { + return c.Features&AVX512PF != 0 +} + +// AVX512ER indicates support of AVX-512 Exponential and Reciprocal Instructions +func (c CPUInfo) AVX512ER() bool { + return c.Features&AVX512ER != 0 +} + +// AVX512CD indicates support of AVX-512 Conflict Detection Instructions +func (c CPUInfo) AVX512CD() bool { + return c.Features&AVX512CD != 0 +} + +// AVX512BW indicates support of AVX-512 Byte and Word Instructions +func (c CPUInfo) AVX512BW() bool { + return c.Features&AVX512BW != 0 +} + +// AVX512VL indicates support of AVX-512 Vector Length Extensions +func (c CPUInfo) AVX512VL() bool { + return c.Features&AVX512VL != 0 +} + +// AVX512VBMI indicates support of AVX-512 Vector Bit Manipulation Instructions +func (c CPUInfo) AVX512VBMI() bool { + return c.Features&AVX512VBMI != 0 +} + +// MPX indicates support of Intel MPX (Memory Protection Extensions) +func (c CPUInfo) MPX() bool { + return c.Features&MPX != 0 +} + +// ERMS indicates support of Enhanced REP MOVSB/STOSB +func (c CPUInfo) ERMS() bool { + return c.Features&ERMS != 0 +} + +// RDTSCP Instruction is available. +func (c CPUInfo) RDTSCP() bool { + return c.Features&RDTSCP != 0 +} + +// CX16 indicates if CMPXCHG16B instruction is available. +func (c CPUInfo) CX16() bool { + return c.Features&CX16 != 0 +} + +// TSX is split into HLE (Hardware Lock Elision) and RTM (Restricted Transactional Memory) detection. +// So TSX simply checks that. +func (c CPUInfo) TSX() bool { + return c.Features&(MPX|RTM) == MPX|RTM +} + +// Atom indicates an Atom processor +func (c CPUInfo) Atom() bool { + return c.Features&ATOM != 0 +} + +// Intel returns true if vendor is recognized as Intel +func (c CPUInfo) Intel() bool { + return c.VendorID == Intel +} + +// AMD returns true if vendor is recognized as AMD +func (c CPUInfo) AMD() bool { + return c.VendorID == AMD +} + +// Transmeta returns true if vendor is recognized as Transmeta +func (c CPUInfo) Transmeta() bool { + return c.VendorID == Transmeta +} + +// NSC returns true if vendor is recognized as National Semiconductor +func (c CPUInfo) NSC() bool { + return c.VendorID == NSC +} + +// VIA returns true if vendor is recognized as VIA +func (c CPUInfo) VIA() bool { + return c.VendorID == VIA +} + +// RTCounter returns the 64-bit time-stamp counter +// Uses the RDTSCP instruction. The value 0 is returned +// if the CPU does not support the instruction. +func (c CPUInfo) RTCounter() uint64 { + if !c.RDTSCP() { + return 0 + } + a, _, _, d := rdtscpAsm() + return uint64(a) | (uint64(d) << 32) +} + +// Ia32TscAux returns the IA32_TSC_AUX part of the RDTSCP. +// This variable is OS dependent, but on Linux contains information +// about the current cpu/core the code is running on. +// If the RDTSCP instruction isn't supported on the CPU, the value 0 is returned. +func (c CPUInfo) Ia32TscAux() uint32 { + if !c.RDTSCP() { + return 0 + } + _, _, ecx, _ := rdtscpAsm() + return ecx +} + +// LogicalCPU will return the Logical CPU the code is currently executing on. +// This is likely to change when the OS re-schedules the running thread +// to another CPU. +// If the current core cannot be detected, -1 will be returned. +func (c CPUInfo) LogicalCPU() int { + if c.maxFunc < 1 { + return -1 + } + _, ebx, _, _ := cpuid(1) + return int(ebx >> 24) +} + +// VM Will return true if the cpu id indicates we are in +// a virtual machine. This is only a hint, and will very likely +// have many false negatives. +func (c CPUInfo) VM() bool { + switch c.VendorID { + case MSVM, KVM, VMware, XenHVM: + return true + } + return false +} + +// Flags contains detected cpu features and caracteristics +type Flags uint64 + +// String returns a string representation of the detected +// CPU features. +func (f Flags) String() string { + return strings.Join(f.Strings(), ",") +} + +// Strings returns and array of the detected features. +func (f Flags) Strings() []string { + s := support() + r := make([]string, 0, 20) + for i := uint(0); i < 64; i++ { + key := Flags(1 << i) + val := flagNames[key] + if s&key != 0 { + r = append(r, val) + } + } + return r +} + +func maxExtendedFunction() uint32 { + eax, _, _, _ := cpuid(0x80000000) + return eax +} + +func maxFunctionID() uint32 { + a, _, _, _ := cpuid(0) + return a +} + +func brandName() string { + if maxExtendedFunction() >= 0x80000004 { + v := make([]uint32, 0, 48) + for i := uint32(0); i < 3; i++ { + a, b, c, d := cpuid(0x80000002 + i) + v = append(v, a, b, c, d) + } + return strings.Trim(string(valAsString(v...)), " ") + } + return "unknown" +} + +func threadsPerCore() int { + mfi := maxFunctionID() + if mfi < 0x4 || vendorID() != Intel { + return 1 + } + + if mfi < 0xb { + _, b, _, d := cpuid(1) + if (d & (1 << 28)) != 0 { + // v will contain logical core count + v := (b >> 16) & 255 + if v > 1 { + a4, _, _, _ := cpuid(4) + // physical cores + v2 := (a4 >> 26) + 1 + if v2 > 0 { + return int(v) / int(v2) + } + } + } + return 1 + } + _, b, _, _ := cpuidex(0xb, 0) + if b&0xffff == 0 { + return 1 + } + return int(b & 0xffff) +} + +func logicalCores() int { + mfi := maxFunctionID() + switch vendorID() { + case Intel: + // Use this on old Intel processors + if mfi < 0xb { + if mfi < 1 { + return 0 + } + // CPUID.1:EBX[23:16] represents the maximum number of addressable IDs (initial APIC ID) + // that can be assigned to logical processors in a physical package. + // The value may not be the same as the number of logical processors that are present in the hardware of a physical package. + _, ebx, _, _ := cpuid(1) + logical := (ebx >> 16) & 0xff + return int(logical) + } + _, b, _, _ := cpuidex(0xb, 1) + return int(b & 0xffff) + case AMD: + _, b, _, _ := cpuid(1) + return int((b >> 16) & 0xff) + default: + return 0 + } +} + +func familyModel() (int, int) { + if maxFunctionID() < 0x1 { + return 0, 0 + } + eax, _, _, _ := cpuid(1) + family := ((eax >> 8) & 0xf) + ((eax >> 20) & 0xff) + model := ((eax >> 4) & 0xf) + ((eax >> 12) & 0xf0) + return int(family), int(model) +} + +func physicalCores() int { + switch vendorID() { + case Intel: + return logicalCores() / threadsPerCore() + case AMD: + if maxExtendedFunction() >= 0x80000008 { + _, _, c, _ := cpuid(0x80000008) + return int(c&0xff) + 1 + } + } + return 0 +} + +// Except from http://en.wikipedia.org/wiki/CPUID#EAX.3D0:_Get_vendor_ID +var vendorMapping = map[string]Vendor{ + "AMDisbetter!": AMD, + "AuthenticAMD": AMD, + "CentaurHauls": VIA, + "GenuineIntel": Intel, + "TransmetaCPU": Transmeta, + "GenuineTMx86": Transmeta, + "Geode by NSC": NSC, + "VIA VIA VIA ": VIA, + "KVMKVMKVMKVM": KVM, + "Microsoft Hv": MSVM, + "VMwareVMware": VMware, + "XenVMMXenVMM": XenHVM, +} + +func vendorID() Vendor { + _, b, c, d := cpuid(0) + v := valAsString(b, d, c) + vend, ok := vendorMapping[string(v)] + if !ok { + return Other + } + return vend +} + +func cacheLine() int { + if maxFunctionID() < 0x1 { + return 0 + } + + _, ebx, _, _ := cpuid(1) + cache := (ebx & 0xff00) >> 5 // cflush size + if cache == 0 && maxExtendedFunction() >= 0x80000006 { + _, _, ecx, _ := cpuid(0x80000006) + cache = ecx & 0xff // cacheline size + } + // TODO: Read from Cache and TLB Information + return int(cache) +} + +func (c *CPUInfo) cacheSize() { + c.Cache.L1D = -1 + c.Cache.L1I = -1 + c.Cache.L2 = -1 + c.Cache.L3 = -1 + vendor := vendorID() + switch vendor { + case Intel: + if maxFunctionID() < 4 { + return + } + for i := uint32(0); ; i++ { + eax, ebx, ecx, _ := cpuidex(4, i) + cacheType := eax & 15 + if cacheType == 0 { + break + } + cacheLevel := (eax >> 5) & 7 + coherency := int(ebx&0xfff) + 1 + partitions := int((ebx>>12)&0x3ff) + 1 + associativity := int((ebx>>22)&0x3ff) + 1 + sets := int(ecx) + 1 + size := associativity * partitions * coherency * sets + switch cacheLevel { + case 1: + if cacheType == 1 { + // 1 = Data Cache + c.Cache.L1D = size + } else if cacheType == 2 { + // 2 = Instruction Cache + c.Cache.L1I = size + } else { + if c.Cache.L1D < 0 { + c.Cache.L1I = size + } + if c.Cache.L1I < 0 { + c.Cache.L1I = size + } + } + case 2: + c.Cache.L2 = size + case 3: + c.Cache.L3 = size + } + } + case AMD: + // Untested. + if maxExtendedFunction() < 0x80000005 { + return + } + _, _, ecx, edx := cpuid(0x80000005) + c.Cache.L1D = int(((ecx >> 24) & 0xFF) * 1024) + c.Cache.L1I = int(((edx >> 24) & 0xFF) * 1024) + + if maxExtendedFunction() < 0x80000006 { + return + } + _, _, ecx, _ = cpuid(0x80000006) + c.Cache.L2 = int(((ecx >> 16) & 0xFFFF) * 1024) + } + + return +} + +type SGXSupport struct { + Available bool + SGX1Supported bool + SGX2Supported bool + MaxEnclaveSizeNot64 int64 + MaxEnclaveSize64 int64 +} + +func hasSGX(available bool) (rval SGXSupport) { + rval.Available = available + + if !available { + return + } + + a, _, _, d := cpuidex(0x12, 0) + rval.SGX1Supported = a&0x01 != 0 + rval.SGX2Supported = a&0x02 != 0 + rval.MaxEnclaveSizeNot64 = 1 << (d & 0xFF) // pow 2 + rval.MaxEnclaveSize64 = 1 << ((d >> 8) & 0xFF) // pow 2 + + return +} + +func support() Flags { + mfi := maxFunctionID() + vend := vendorID() + if mfi < 0x1 { + return 0 + } + rval := uint64(0) + _, _, c, d := cpuid(1) + if (d & (1 << 15)) != 0 { + rval |= CMOV + } + if (d & (1 << 23)) != 0 { + rval |= MMX + } + if (d & (1 << 25)) != 0 { + rval |= MMXEXT + } + if (d & (1 << 25)) != 0 { + rval |= SSE + } + if (d & (1 << 26)) != 0 { + rval |= SSE2 + } + if (c & 1) != 0 { + rval |= SSE3 + } + if (c & 0x00000200) != 0 { + rval |= SSSE3 + } + if (c & 0x00080000) != 0 { + rval |= SSE4 + } + if (c & 0x00100000) != 0 { + rval |= SSE42 + } + if (c & (1 << 25)) != 0 { + rval |= AESNI + } + if (c & (1 << 1)) != 0 { + rval |= CLMUL + } + if c&(1<<23) != 0 { + rval |= POPCNT + } + if c&(1<<30) != 0 { + rval |= RDRAND + } + if c&(1<<29) != 0 { + rval |= F16C + } + if c&(1<<13) != 0 { + rval |= CX16 + } + if vend == Intel && (d&(1<<28)) != 0 && mfi >= 4 { + if threadsPerCore() > 1 { + rval |= HTT + } + } + + // Check XGETBV, OXSAVE and AVX bits + if c&(1<<26) != 0 && c&(1<<27) != 0 && c&(1<<28) != 0 { + // Check for OS support + eax, _ := xgetbv(0) + if (eax & 0x6) == 0x6 { + rval |= AVX + if (c & 0x00001000) != 0 { + rval |= FMA3 + } + } + } + + // Check AVX2, AVX2 requires OS support, but BMI1/2 don't. + if mfi >= 7 { + _, ebx, ecx, _ := cpuidex(7, 0) + if (rval&AVX) != 0 && (ebx&0x00000020) != 0 { + rval |= AVX2 + } + if (ebx & 0x00000008) != 0 { + rval |= BMI1 + if (ebx & 0x00000100) != 0 { + rval |= BMI2 + } + } + if ebx&(1<<2) != 0 { + rval |= SGX + } + if ebx&(1<<4) != 0 { + rval |= HLE + } + if ebx&(1<<9) != 0 { + rval |= ERMS + } + if ebx&(1<<11) != 0 { + rval |= RTM + } + if ebx&(1<<14) != 0 { + rval |= MPX + } + if ebx&(1<<18) != 0 { + rval |= RDSEED + } + if ebx&(1<<19) != 0 { + rval |= ADX + } + if ebx&(1<<29) != 0 { + rval |= SHA + } + + // Only detect AVX-512 features if XGETBV is supported + if c&((1<<26)|(1<<27)) == (1<<26)|(1<<27) { + // Check for OS support + eax, _ := xgetbv(0) + + // Verify that XCR0[7:5] = ‘111b’ (OPMASK state, upper 256-bit of ZMM0-ZMM15 and + // ZMM16-ZMM31 state are enabled by OS) + /// and that XCR0[2:1] = ‘11b’ (XMM state and YMM state are enabled by OS). + if (eax>>5)&7 == 7 && (eax>>1)&3 == 3 { + if ebx&(1<<16) != 0 { + rval |= AVX512F + } + if ebx&(1<<17) != 0 { + rval |= AVX512DQ + } + if ebx&(1<<21) != 0 { + rval |= AVX512IFMA + } + if ebx&(1<<26) != 0 { + rval |= AVX512PF + } + if ebx&(1<<27) != 0 { + rval |= AVX512ER + } + if ebx&(1<<28) != 0 { + rval |= AVX512CD + } + if ebx&(1<<30) != 0 { + rval |= AVX512BW + } + if ebx&(1<<31) != 0 { + rval |= AVX512VL + } + // ecx + if ecx&(1<<1) != 0 { + rval |= AVX512VBMI + } + } + } + } + + if maxExtendedFunction() >= 0x80000001 { + _, _, c, d := cpuid(0x80000001) + if (c & (1 << 5)) != 0 { + rval |= LZCNT + rval |= POPCNT + } + if (d & (1 << 31)) != 0 { + rval |= AMD3DNOW + } + if (d & (1 << 30)) != 0 { + rval |= AMD3DNOWEXT + } + if (d & (1 << 23)) != 0 { + rval |= MMX + } + if (d & (1 << 22)) != 0 { + rval |= MMXEXT + } + if (c & (1 << 6)) != 0 { + rval |= SSE4A + } + if d&(1<<20) != 0 { + rval |= NX + } + if d&(1<<27) != 0 { + rval |= RDTSCP + } + + /* Allow for selectively disabling SSE2 functions on AMD processors + with SSE2 support but not SSE4a. This includes Athlon64, some + Opteron, and some Sempron processors. MMX, SSE, or 3DNow! are faster + than SSE2 often enough to utilize this special-case flag. + AV_CPU_FLAG_SSE2 and AV_CPU_FLAG_SSE2SLOW are both set in this case + so that SSE2 is used unless explicitly disabled by checking + AV_CPU_FLAG_SSE2SLOW. */ + if vendorID() != Intel && + rval&SSE2 != 0 && (c&0x00000040) == 0 { + rval |= SSE2SLOW + } + + /* XOP and FMA4 use the AVX instruction coding scheme, so they can't be + * used unless the OS has AVX support. */ + if (rval & AVX) != 0 { + if (c & 0x00000800) != 0 { + rval |= XOP + } + if (c & 0x00010000) != 0 { + rval |= FMA4 + } + } + + if vendorID() == Intel { + family, model := familyModel() + if family == 6 && (model == 9 || model == 13 || model == 14) { + /* 6/9 (pentium-m "banias"), 6/13 (pentium-m "dothan"), and + * 6/14 (core1 "yonah") theoretically support sse2, but it's + * usually slower than mmx. */ + if (rval & SSE2) != 0 { + rval |= SSE2SLOW + } + if (rval & SSE3) != 0 { + rval |= SSE3SLOW + } + } + /* The Atom processor has SSSE3 support, which is useful in many cases, + * but sometimes the SSSE3 version is slower than the SSE2 equivalent + * on the Atom, but is generally faster on other processors supporting + * SSSE3. This flag allows for selectively disabling certain SSSE3 + * functions on the Atom. */ + if family == 6 && model == 28 { + rval |= ATOM + } + } + } + return Flags(rval) +} + +func valAsString(values ...uint32) []byte { + r := make([]byte, 4*len(values)) + for i, v := range values { + dst := r[i*4:] + dst[0] = byte(v & 0xff) + dst[1] = byte((v >> 8) & 0xff) + dst[2] = byte((v >> 16) & 0xff) + dst[3] = byte((v >> 24) & 0xff) + switch { + case dst[0] == 0: + return r[:i*4] + case dst[1] == 0: + return r[:i*4+1] + case dst[2] == 0: + return r[:i*4+2] + case dst[3] == 0: + return r[:i*4+3] + } + } + return r +} diff --git a/vendor/github.com/klauspost/cpuid/cpuid_386.s b/vendor/github.com/klauspost/cpuid/cpuid_386.s new file mode 100644 index 000000000..4d731711e --- /dev/null +++ b/vendor/github.com/klauspost/cpuid/cpuid_386.s @@ -0,0 +1,42 @@ +// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file. + +// +build 386,!gccgo + +// func asmCpuid(op uint32) (eax, ebx, ecx, edx uint32) +TEXT ·asmCpuid(SB), 7, $0 + XORL CX, CX + MOVL op+0(FP), AX + CPUID + MOVL AX, eax+4(FP) + MOVL BX, ebx+8(FP) + MOVL CX, ecx+12(FP) + MOVL DX, edx+16(FP) + RET + +// func asmCpuidex(op, op2 uint32) (eax, ebx, ecx, edx uint32) +TEXT ·asmCpuidex(SB), 7, $0 + MOVL op+0(FP), AX + MOVL op2+4(FP), CX + CPUID + MOVL AX, eax+8(FP) + MOVL BX, ebx+12(FP) + MOVL CX, ecx+16(FP) + MOVL DX, edx+20(FP) + RET + +// func xgetbv(index uint32) (eax, edx uint32) +TEXT ·asmXgetbv(SB), 7, $0 + MOVL index+0(FP), CX + BYTE $0x0f; BYTE $0x01; BYTE $0xd0 // XGETBV + MOVL AX, eax+4(FP) + MOVL DX, edx+8(FP) + RET + +// func asmRdtscpAsm() (eax, ebx, ecx, edx uint32) +TEXT ·asmRdtscpAsm(SB), 7, $0 + BYTE $0x0F; BYTE $0x01; BYTE $0xF9 // RDTSCP + MOVL AX, eax+0(FP) + MOVL BX, ebx+4(FP) + MOVL CX, ecx+8(FP) + MOVL DX, edx+12(FP) + RET diff --git a/vendor/github.com/klauspost/cpuid/cpuid_amd64.s b/vendor/github.com/klauspost/cpuid/cpuid_amd64.s new file mode 100644 index 000000000..3c1d60e42 --- /dev/null +++ b/vendor/github.com/klauspost/cpuid/cpuid_amd64.s @@ -0,0 +1,42 @@ +// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file. + +//+build amd64,!gccgo + +// func asmCpuid(op uint32) (eax, ebx, ecx, edx uint32) +TEXT ·asmCpuid(SB), 7, $0 + XORQ CX, CX + MOVL op+0(FP), AX + CPUID + MOVL AX, eax+8(FP) + MOVL BX, ebx+12(FP) + MOVL CX, ecx+16(FP) + MOVL DX, edx+20(FP) + RET + +// func asmCpuidex(op, op2 uint32) (eax, ebx, ecx, edx uint32) +TEXT ·asmCpuidex(SB), 7, $0 + MOVL op+0(FP), AX + MOVL op2+4(FP), CX + CPUID + MOVL AX, eax+8(FP) + MOVL BX, ebx+12(FP) + MOVL CX, ecx+16(FP) + MOVL DX, edx+20(FP) + RET + +// func asmXgetbv(index uint32) (eax, edx uint32) +TEXT ·asmXgetbv(SB), 7, $0 + MOVL index+0(FP), CX + BYTE $0x0f; BYTE $0x01; BYTE $0xd0 // XGETBV + MOVL AX, eax+8(FP) + MOVL DX, edx+12(FP) + RET + +// func asmRdtscpAsm() (eax, ebx, ecx, edx uint32) +TEXT ·asmRdtscpAsm(SB), 7, $0 + BYTE $0x0F; BYTE $0x01; BYTE $0xF9 // RDTSCP + MOVL AX, eax+0(FP) + MOVL BX, ebx+4(FP) + MOVL CX, ecx+8(FP) + MOVL DX, edx+12(FP) + RET diff --git a/vendor/github.com/klauspost/cpuid/detect_intel.go b/vendor/github.com/klauspost/cpuid/detect_intel.go new file mode 100644 index 000000000..a5f04dd6d --- /dev/null +++ b/vendor/github.com/klauspost/cpuid/detect_intel.go @@ -0,0 +1,17 @@ +// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file. + +// +build 386,!gccgo amd64,!gccgo + +package cpuid + +func asmCpuid(op uint32) (eax, ebx, ecx, edx uint32) +func asmCpuidex(op, op2 uint32) (eax, ebx, ecx, edx uint32) +func asmXgetbv(index uint32) (eax, edx uint32) +func asmRdtscpAsm() (eax, ebx, ecx, edx uint32) + +func initCPU() { + cpuid = asmCpuid + cpuidex = asmCpuidex + xgetbv = asmXgetbv + rdtscpAsm = asmRdtscpAsm +} diff --git a/vendor/github.com/klauspost/cpuid/detect_ref.go b/vendor/github.com/klauspost/cpuid/detect_ref.go new file mode 100644 index 000000000..909c5d9a7 --- /dev/null +++ b/vendor/github.com/klauspost/cpuid/detect_ref.go @@ -0,0 +1,23 @@ +// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file. + +// +build !amd64,!386 gccgo + +package cpuid + +func initCPU() { + cpuid = func(op uint32) (eax, ebx, ecx, edx uint32) { + return 0, 0, 0, 0 + } + + cpuidex = func(op, op2 uint32) (eax, ebx, ecx, edx uint32) { + return 0, 0, 0, 0 + } + + xgetbv = func(index uint32) (eax, edx uint32) { + return 0, 0 + } + + rdtscpAsm = func() (eax, ebx, ecx, edx uint32) { + return 0, 0, 0, 0 + } +} diff --git a/vendor/github.com/klauspost/cpuid/generate.go b/vendor/github.com/klauspost/cpuid/generate.go new file mode 100644 index 000000000..90e7a98d2 --- /dev/null +++ b/vendor/github.com/klauspost/cpuid/generate.go @@ -0,0 +1,4 @@ +package cpuid + +//go:generate go run private-gen.go +//go:generate gofmt -w ./private diff --git a/vendor/github.com/klauspost/cpuid/private-gen.go b/vendor/github.com/klauspost/cpuid/private-gen.go new file mode 100644 index 000000000..437333d29 --- /dev/null +++ b/vendor/github.com/klauspost/cpuid/private-gen.go @@ -0,0 +1,476 @@ +// +build ignore + +package main + +import ( + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "io" + "io/ioutil" + "log" + "os" + "reflect" + "strings" + "unicode" + "unicode/utf8" +) + +var inFiles = []string{"cpuid.go", "cpuid_test.go"} +var copyFiles = []string{"cpuid_amd64.s", "cpuid_386.s", "detect_ref.go", "detect_intel.go"} +var fileSet = token.NewFileSet() +var reWrites = []rewrite{ + initRewrite("CPUInfo -> cpuInfo"), + initRewrite("Vendor -> vendor"), + initRewrite("Flags -> flags"), + initRewrite("Detect -> detect"), + initRewrite("CPU -> cpu"), +} +var excludeNames = map[string]bool{"string": true, "join": true, "trim": true, + // cpuid_test.go + "t": true, "println": true, "logf": true, "log": true, "fatalf": true, "fatal": true, +} + +var excludePrefixes = []string{"test", "benchmark"} + +func main() { + Package := "private" + parserMode := parser.ParseComments + exported := make(map[string]rewrite) + for _, file := range inFiles { + in, err := os.Open(file) + if err != nil { + log.Fatalf("opening input", err) + } + + src, err := ioutil.ReadAll(in) + if err != nil { + log.Fatalf("reading input", err) + } + + astfile, err := parser.ParseFile(fileSet, file, src, parserMode) + if err != nil { + log.Fatalf("parsing input", err) + } + + for _, rw := range reWrites { + astfile = rw(astfile) + } + + // Inspect the AST and print all identifiers and literals. + var startDecl token.Pos + var endDecl token.Pos + ast.Inspect(astfile, func(n ast.Node) bool { + var s string + switch x := n.(type) { + case *ast.Ident: + if x.IsExported() { + t := strings.ToLower(x.Name) + for _, pre := range excludePrefixes { + if strings.HasPrefix(t, pre) { + return true + } + } + if excludeNames[t] != true { + //if x.Pos() > startDecl && x.Pos() < endDecl { + exported[x.Name] = initRewrite(x.Name + " -> " + t) + } + } + + case *ast.GenDecl: + if x.Tok == token.CONST && x.Lparen > 0 { + startDecl = x.Lparen + endDecl = x.Rparen + // fmt.Printf("Decl:%s -> %s\n", fileSet.Position(startDecl), fileSet.Position(endDecl)) + } + } + if s != "" { + fmt.Printf("%s:\t%s\n", fileSet.Position(n.Pos()), s) + } + return true + }) + + for _, rw := range exported { + astfile = rw(astfile) + } + + var buf bytes.Buffer + + printer.Fprint(&buf, fileSet, astfile) + + // Remove package documentation and insert information + s := buf.String() + ind := strings.Index(buf.String(), "\npackage cpuid") + s = s[ind:] + s = "// Generated, DO NOT EDIT,\n" + + "// but copy it to your own project and rename the package.\n" + + "// See more at http://github.com/klauspost/cpuid\n" + + s + + outputName := Package + string(os.PathSeparator) + file + + err = ioutil.WriteFile(outputName, []byte(s), 0644) + if err != nil { + log.Fatalf("writing output: %s", err) + } + log.Println("Generated", outputName) + } + + for _, file := range copyFiles { + dst := "" + if strings.HasPrefix(file, "cpuid") { + dst = Package + string(os.PathSeparator) + file + } else { + dst = Package + string(os.PathSeparator) + "cpuid_" + file + } + err := copyFile(file, dst) + if err != nil { + log.Fatalf("copying file: %s", err) + } + log.Println("Copied", dst) + } +} + +// CopyFile copies a file from src to dst. If src and dst files exist, and are +// the same, then return success. Copy the file contents from src to dst. +func copyFile(src, dst string) (err error) { + sfi, err := os.Stat(src) + if err != nil { + return + } + if !sfi.Mode().IsRegular() { + // cannot copy non-regular files (e.g., directories, + // symlinks, devices, etc.) + return fmt.Errorf("CopyFile: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String()) + } + dfi, err := os.Stat(dst) + if err != nil { + if !os.IsNotExist(err) { + return + } + } else { + if !(dfi.Mode().IsRegular()) { + return fmt.Errorf("CopyFile: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String()) + } + if os.SameFile(sfi, dfi) { + return + } + } + err = copyFileContents(src, dst) + return +} + +// copyFileContents copies the contents of the file named src to the file named +// by dst. The file will be created if it does not already exist. If the +// destination file exists, all it's contents will be replaced by the contents +// of the source file. +func copyFileContents(src, dst string) (err error) { + in, err := os.Open(src) + if err != nil { + return + } + defer in.Close() + out, err := os.Create(dst) + if err != nil { + return + } + defer func() { + cerr := out.Close() + if err == nil { + err = cerr + } + }() + if _, err = io.Copy(out, in); err != nil { + return + } + err = out.Sync() + return +} + +type rewrite func(*ast.File) *ast.File + +// Mostly copied from gofmt +func initRewrite(rewriteRule string) rewrite { + f := strings.Split(rewriteRule, "->") + if len(f) != 2 { + fmt.Fprintf(os.Stderr, "rewrite rule must be of the form 'pattern -> replacement'\n") + os.Exit(2) + } + pattern := parseExpr(f[0], "pattern") + replace := parseExpr(f[1], "replacement") + return func(p *ast.File) *ast.File { return rewriteFile(pattern, replace, p) } +} + +// parseExpr parses s as an expression. +// It might make sense to expand this to allow statement patterns, +// but there are problems with preserving formatting and also +// with what a wildcard for a statement looks like. +func parseExpr(s, what string) ast.Expr { + x, err := parser.ParseExpr(s) + if err != nil { + fmt.Fprintf(os.Stderr, "parsing %s %s at %s\n", what, s, err) + os.Exit(2) + } + return x +} + +// Keep this function for debugging. +/* +func dump(msg string, val reflect.Value) { + fmt.Printf("%s:\n", msg) + ast.Print(fileSet, val.Interface()) + fmt.Println() +} +*/ + +// rewriteFile applies the rewrite rule 'pattern -> replace' to an entire file. +func rewriteFile(pattern, replace ast.Expr, p *ast.File) *ast.File { + cmap := ast.NewCommentMap(fileSet, p, p.Comments) + m := make(map[string]reflect.Value) + pat := reflect.ValueOf(pattern) + repl := reflect.ValueOf(replace) + + var rewriteVal func(val reflect.Value) reflect.Value + rewriteVal = func(val reflect.Value) reflect.Value { + // don't bother if val is invalid to start with + if !val.IsValid() { + return reflect.Value{} + } + for k := range m { + delete(m, k) + } + val = apply(rewriteVal, val) + if match(m, pat, val) { + val = subst(m, repl, reflect.ValueOf(val.Interface().(ast.Node).Pos())) + } + return val + } + + r := apply(rewriteVal, reflect.ValueOf(p)).Interface().(*ast.File) + r.Comments = cmap.Filter(r).Comments() // recreate comments list + return r +} + +// set is a wrapper for x.Set(y); it protects the caller from panics if x cannot be changed to y. +func set(x, y reflect.Value) { + // don't bother if x cannot be set or y is invalid + if !x.CanSet() || !y.IsValid() { + return + } + defer func() { + if x := recover(); x != nil { + if s, ok := x.(string); ok && + (strings.Contains(s, "type mismatch") || strings.Contains(s, "not assignable")) { + // x cannot be set to y - ignore this rewrite + return + } + panic(x) + } + }() + x.Set(y) +} + +// Values/types for special cases. +var ( + objectPtrNil = reflect.ValueOf((*ast.Object)(nil)) + scopePtrNil = reflect.ValueOf((*ast.Scope)(nil)) + + identType = reflect.TypeOf((*ast.Ident)(nil)) + objectPtrType = reflect.TypeOf((*ast.Object)(nil)) + positionType = reflect.TypeOf(token.NoPos) + callExprType = reflect.TypeOf((*ast.CallExpr)(nil)) + scopePtrType = reflect.TypeOf((*ast.Scope)(nil)) +) + +// apply replaces each AST field x in val with f(x), returning val. +// To avoid extra conversions, f operates on the reflect.Value form. +func apply(f func(reflect.Value) reflect.Value, val reflect.Value) reflect.Value { + if !val.IsValid() { + return reflect.Value{} + } + + // *ast.Objects introduce cycles and are likely incorrect after + // rewrite; don't follow them but replace with nil instead + if val.Type() == objectPtrType { + return objectPtrNil + } + + // similarly for scopes: they are likely incorrect after a rewrite; + // replace them with nil + if val.Type() == scopePtrType { + return scopePtrNil + } + + switch v := reflect.Indirect(val); v.Kind() { + case reflect.Slice: + for i := 0; i < v.Len(); i++ { + e := v.Index(i) + set(e, f(e)) + } + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + e := v.Field(i) + set(e, f(e)) + } + case reflect.Interface: + e := v.Elem() + set(v, f(e)) + } + return val +} + +func isWildcard(s string) bool { + rune, size := utf8.DecodeRuneInString(s) + return size == len(s) && unicode.IsLower(rune) +} + +// match returns true if pattern matches val, +// recording wildcard submatches in m. +// If m == nil, match checks whether pattern == val. +func match(m map[string]reflect.Value, pattern, val reflect.Value) bool { + // Wildcard matches any expression. If it appears multiple + // times in the pattern, it must match the same expression + // each time. + if m != nil && pattern.IsValid() && pattern.Type() == identType { + name := pattern.Interface().(*ast.Ident).Name + if isWildcard(name) && val.IsValid() { + // wildcards only match valid (non-nil) expressions. + if _, ok := val.Interface().(ast.Expr); ok && !val.IsNil() { + if old, ok := m[name]; ok { + return match(nil, old, val) + } + m[name] = val + return true + } + } + } + + // Otherwise, pattern and val must match recursively. + if !pattern.IsValid() || !val.IsValid() { + return !pattern.IsValid() && !val.IsValid() + } + if pattern.Type() != val.Type() { + return false + } + + // Special cases. + switch pattern.Type() { + case identType: + // For identifiers, only the names need to match + // (and none of the other *ast.Object information). + // This is a common case, handle it all here instead + // of recursing down any further via reflection. + p := pattern.Interface().(*ast.Ident) + v := val.Interface().(*ast.Ident) + return p == nil && v == nil || p != nil && v != nil && p.Name == v.Name + case objectPtrType, positionType: + // object pointers and token positions always match + return true + case callExprType: + // For calls, the Ellipsis fields (token.Position) must + // match since that is how f(x) and f(x...) are different. + // Check them here but fall through for the remaining fields. + p := pattern.Interface().(*ast.CallExpr) + v := val.Interface().(*ast.CallExpr) + if p.Ellipsis.IsValid() != v.Ellipsis.IsValid() { + return false + } + } + + p := reflect.Indirect(pattern) + v := reflect.Indirect(val) + if !p.IsValid() || !v.IsValid() { + return !p.IsValid() && !v.IsValid() + } + + switch p.Kind() { + case reflect.Slice: + if p.Len() != v.Len() { + return false + } + for i := 0; i < p.Len(); i++ { + if !match(m, p.Index(i), v.Index(i)) { + return false + } + } + return true + + case reflect.Struct: + for i := 0; i < p.NumField(); i++ { + if !match(m, p.Field(i), v.Field(i)) { + return false + } + } + return true + + case reflect.Interface: + return match(m, p.Elem(), v.Elem()) + } + + // Handle token integers, etc. + return p.Interface() == v.Interface() +} + +// subst returns a copy of pattern with values from m substituted in place +// of wildcards and pos used as the position of tokens from the pattern. +// if m == nil, subst returns a copy of pattern and doesn't change the line +// number information. +func subst(m map[string]reflect.Value, pattern reflect.Value, pos reflect.Value) reflect.Value { + if !pattern.IsValid() { + return reflect.Value{} + } + + // Wildcard gets replaced with map value. + if m != nil && pattern.Type() == identType { + name := pattern.Interface().(*ast.Ident).Name + if isWildcard(name) { + if old, ok := m[name]; ok { + return subst(nil, old, reflect.Value{}) + } + } + } + + if pos.IsValid() && pattern.Type() == positionType { + // use new position only if old position was valid in the first place + if old := pattern.Interface().(token.Pos); !old.IsValid() { + return pattern + } + return pos + } + + // Otherwise copy. + switch p := pattern; p.Kind() { + case reflect.Slice: + v := reflect.MakeSlice(p.Type(), p.Len(), p.Len()) + for i := 0; i < p.Len(); i++ { + v.Index(i).Set(subst(m, p.Index(i), pos)) + } + return v + + case reflect.Struct: + v := reflect.New(p.Type()).Elem() + for i := 0; i < p.NumField(); i++ { + v.Field(i).Set(subst(m, p.Field(i), pos)) + } + return v + + case reflect.Ptr: + v := reflect.New(p.Type()).Elem() + if elem := p.Elem(); elem.IsValid() { + v.Set(subst(m, elem, pos).Addr()) + } + return v + + case reflect.Interface: + v := reflect.New(p.Type()).Elem() + if elem := p.Elem(); elem.IsValid() { + v.Set(subst(m, elem, pos)) + } + return v + } + + return pattern +} diff --git a/vendor/github.com/klauspost/cpuid/private/cpuid.go b/vendor/github.com/klauspost/cpuid/private/cpuid.go new file mode 100644 index 000000000..21712142c --- /dev/null +++ b/vendor/github.com/klauspost/cpuid/private/cpuid.go @@ -0,0 +1,1024 @@ +// Generated, DO NOT EDIT, +// but copy it to your own project and rename the package. +// See more at http://github.com/klauspost/cpuid + +package cpuid + +import "strings" + +// Vendor is a representation of a CPU vendor. +type vendor int + +const ( + other vendor = iota + intel + amd + via + transmeta + nsc + kvm // Kernel-based Virtual Machine + msvm // Microsoft Hyper-V or Windows Virtual PC + vmware + xenhvm +) + +const ( + cmov = 1 << iota // i686 CMOV + nx // NX (No-Execute) bit + amd3dnow // AMD 3DNOW + amd3dnowext // AMD 3DNowExt + mmx // standard MMX + mmxext // SSE integer functions or AMD MMX ext + sse // SSE functions + sse2 // P4 SSE functions + sse3 // Prescott SSE3 functions + ssse3 // Conroe SSSE3 functions + sse4 // Penryn SSE4.1 functions + sse4a // AMD Barcelona microarchitecture SSE4a instructions + sse42 // Nehalem SSE4.2 functions + avx // AVX functions + avx2 // AVX2 functions + fma3 // Intel FMA 3 + fma4 // Bulldozer FMA4 functions + xop // Bulldozer XOP functions + f16c // Half-precision floating-point conversion + bmi1 // Bit Manipulation Instruction Set 1 + bmi2 // Bit Manipulation Instruction Set 2 + tbm // AMD Trailing Bit Manipulation + lzcnt // LZCNT instruction + popcnt // POPCNT instruction + aesni // Advanced Encryption Standard New Instructions + clmul // Carry-less Multiplication + htt // Hyperthreading (enabled) + hle // Hardware Lock Elision + rtm // Restricted Transactional Memory + rdrand // RDRAND instruction is available + rdseed // RDSEED instruction is available + adx // Intel ADX (Multi-Precision Add-Carry Instruction Extensions) + sha // Intel SHA Extensions + avx512f // AVX-512 Foundation + avx512dq // AVX-512 Doubleword and Quadword Instructions + avx512ifma // AVX-512 Integer Fused Multiply-Add Instructions + avx512pf // AVX-512 Prefetch Instructions + avx512er // AVX-512 Exponential and Reciprocal Instructions + avx512cd // AVX-512 Conflict Detection Instructions + avx512bw // AVX-512 Byte and Word Instructions + avx512vl // AVX-512 Vector Length Extensions + avx512vbmi // AVX-512 Vector Bit Manipulation Instructions + mpx // Intel MPX (Memory Protection Extensions) + erms // Enhanced REP MOVSB/STOSB + rdtscp // RDTSCP Instruction + cx16 // CMPXCHG16B Instruction + sgx // Software Guard Extensions + + // Performance indicators + sse2slow // SSE2 is supported, but usually not faster + sse3slow // SSE3 is supported, but usually not faster + atom // Atom processor, some SSSE3 instructions are slower +) + +var flagNames = map[flags]string{ + cmov: "CMOV", // i686 CMOV + nx: "NX", // NX (No-Execute) bit + amd3dnow: "AMD3DNOW", // AMD 3DNOW + amd3dnowext: "AMD3DNOWEXT", // AMD 3DNowExt + mmx: "MMX", // Standard MMX + mmxext: "MMXEXT", // SSE integer functions or AMD MMX ext + sse: "SSE", // SSE functions + sse2: "SSE2", // P4 SSE2 functions + sse3: "SSE3", // Prescott SSE3 functions + ssse3: "SSSE3", // Conroe SSSE3 functions + sse4: "SSE4.1", // Penryn SSE4.1 functions + sse4a: "SSE4A", // AMD Barcelona microarchitecture SSE4a instructions + sse42: "SSE4.2", // Nehalem SSE4.2 functions + avx: "AVX", // AVX functions + avx2: "AVX2", // AVX functions + fma3: "FMA3", // Intel FMA 3 + fma4: "FMA4", // Bulldozer FMA4 functions + xop: "XOP", // Bulldozer XOP functions + f16c: "F16C", // Half-precision floating-point conversion + bmi1: "BMI1", // Bit Manipulation Instruction Set 1 + bmi2: "BMI2", // Bit Manipulation Instruction Set 2 + tbm: "TBM", // AMD Trailing Bit Manipulation + lzcnt: "LZCNT", // LZCNT instruction + popcnt: "POPCNT", // POPCNT instruction + aesni: "AESNI", // Advanced Encryption Standard New Instructions + clmul: "CLMUL", // Carry-less Multiplication + htt: "HTT", // Hyperthreading (enabled) + hle: "HLE", // Hardware Lock Elision + rtm: "RTM", // Restricted Transactional Memory + rdrand: "RDRAND", // RDRAND instruction is available + rdseed: "RDSEED", // RDSEED instruction is available + adx: "ADX", // Intel ADX (Multi-Precision Add-Carry Instruction Extensions) + sha: "SHA", // Intel SHA Extensions + avx512f: "AVX512F", // AVX-512 Foundation + avx512dq: "AVX512DQ", // AVX-512 Doubleword and Quadword Instructions + avx512ifma: "AVX512IFMA", // AVX-512 Integer Fused Multiply-Add Instructions + avx512pf: "AVX512PF", // AVX-512 Prefetch Instructions + avx512er: "AVX512ER", // AVX-512 Exponential and Reciprocal Instructions + avx512cd: "AVX512CD", // AVX-512 Conflict Detection Instructions + avx512bw: "AVX512BW", // AVX-512 Byte and Word Instructions + avx512vl: "AVX512VL", // AVX-512 Vector Length Extensions + avx512vbmi: "AVX512VBMI", // AVX-512 Vector Bit Manipulation Instructions + mpx: "MPX", // Intel MPX (Memory Protection Extensions) + erms: "ERMS", // Enhanced REP MOVSB/STOSB + rdtscp: "RDTSCP", // RDTSCP Instruction + cx16: "CX16", // CMPXCHG16B Instruction + sgx: "SGX", // Software Guard Extensions + + // Performance indicators + sse2slow: "SSE2SLOW", // SSE2 supported, but usually not faster + sse3slow: "SSE3SLOW", // SSE3 supported, but usually not faster + atom: "ATOM", // Atom processor, some SSSE3 instructions are slower + +} + +// CPUInfo contains information about the detected system CPU. +type cpuInfo struct { + brandname string // Brand name reported by the CPU + vendorid vendor // Comparable CPU vendor ID + features flags // Features of the CPU + physicalcores int // Number of physical processor cores in your CPU. Will be 0 if undetectable. + threadspercore int // Number of threads per physical core. Will be 1 if undetectable. + logicalcores int // Number of physical cores times threads that can run on each core through the use of hyperthreading. Will be 0 if undetectable. + family int // CPU family number + model int // CPU model number + cacheline int // Cache line size in bytes. Will be 0 if undetectable. + cache struct { + l1i int // L1 Instruction Cache (per core or shared). Will be -1 if undetected + l1d int // L1 Data Cache (per core or shared). Will be -1 if undetected + l2 int // L2 Cache (per core or shared). Will be -1 if undetected + l3 int // L3 Instruction Cache (per core or shared). Will be -1 if undetected + } + sgx sgxsupport + maxFunc uint32 + maxExFunc uint32 +} + +var cpuid func(op uint32) (eax, ebx, ecx, edx uint32) +var cpuidex func(op, op2 uint32) (eax, ebx, ecx, edx uint32) +var xgetbv func(index uint32) (eax, edx uint32) +var rdtscpAsm func() (eax, ebx, ecx, edx uint32) + +// CPU contains information about the CPU as detected on startup, +// or when Detect last was called. +// +// Use this as the primary entry point to you data, +// this way queries are +var cpu cpuInfo + +func init() { + initCPU() + detect() +} + +// Detect will re-detect current CPU info. +// This will replace the content of the exported CPU variable. +// +// Unless you expect the CPU to change while you are running your program +// you should not need to call this function. +// If you call this, you must ensure that no other goroutine is accessing the +// exported CPU variable. +func detect() { + cpu.maxFunc = maxFunctionID() + cpu.maxExFunc = maxExtendedFunction() + cpu.brandname = brandName() + cpu.cacheline = cacheLine() + cpu.family, cpu.model = familyModel() + cpu.features = support() + cpu.sgx = hasSGX(cpu.features&sgx != 0) + cpu.threadspercore = threadsPerCore() + cpu.logicalcores = logicalCores() + cpu.physicalcores = physicalCores() + cpu.vendorid = vendorID() + cpu.cacheSize() +} + +// Generated here: http://play.golang.org/p/BxFH2Gdc0G + +// Cmov indicates support of CMOV instructions +func (c cpuInfo) cmov() bool { + return c.features&cmov != 0 +} + +// Amd3dnow indicates support of AMD 3DNOW! instructions +func (c cpuInfo) amd3dnow() bool { + return c.features&amd3dnow != 0 +} + +// Amd3dnowExt indicates support of AMD 3DNOW! Extended instructions +func (c cpuInfo) amd3dnowext() bool { + return c.features&amd3dnowext != 0 +} + +// MMX indicates support of MMX instructions +func (c cpuInfo) mmx() bool { + return c.features&mmx != 0 +} + +// MMXExt indicates support of MMXEXT instructions +// (SSE integer functions or AMD MMX ext) +func (c cpuInfo) mmxext() bool { + return c.features&mmxext != 0 +} + +// SSE indicates support of SSE instructions +func (c cpuInfo) sse() bool { + return c.features&sse != 0 +} + +// SSE2 indicates support of SSE 2 instructions +func (c cpuInfo) sse2() bool { + return c.features&sse2 != 0 +} + +// SSE3 indicates support of SSE 3 instructions +func (c cpuInfo) sse3() bool { + return c.features&sse3 != 0 +} + +// SSSE3 indicates support of SSSE 3 instructions +func (c cpuInfo) ssse3() bool { + return c.features&ssse3 != 0 +} + +// SSE4 indicates support of SSE 4 (also called SSE 4.1) instructions +func (c cpuInfo) sse4() bool { + return c.features&sse4 != 0 +} + +// SSE42 indicates support of SSE4.2 instructions +func (c cpuInfo) sse42() bool { + return c.features&sse42 != 0 +} + +// AVX indicates support of AVX instructions +// and operating system support of AVX instructions +func (c cpuInfo) avx() bool { + return c.features&avx != 0 +} + +// AVX2 indicates support of AVX2 instructions +func (c cpuInfo) avx2() bool { + return c.features&avx2 != 0 +} + +// FMA3 indicates support of FMA3 instructions +func (c cpuInfo) fma3() bool { + return c.features&fma3 != 0 +} + +// FMA4 indicates support of FMA4 instructions +func (c cpuInfo) fma4() bool { + return c.features&fma4 != 0 +} + +// XOP indicates support of XOP instructions +func (c cpuInfo) xop() bool { + return c.features&xop != 0 +} + +// F16C indicates support of F16C instructions +func (c cpuInfo) f16c() bool { + return c.features&f16c != 0 +} + +// BMI1 indicates support of BMI1 instructions +func (c cpuInfo) bmi1() bool { + return c.features&bmi1 != 0 +} + +// BMI2 indicates support of BMI2 instructions +func (c cpuInfo) bmi2() bool { + return c.features&bmi2 != 0 +} + +// TBM indicates support of TBM instructions +// (AMD Trailing Bit Manipulation) +func (c cpuInfo) tbm() bool { + return c.features&tbm != 0 +} + +// Lzcnt indicates support of LZCNT instruction +func (c cpuInfo) lzcnt() bool { + return c.features&lzcnt != 0 +} + +// Popcnt indicates support of POPCNT instruction +func (c cpuInfo) popcnt() bool { + return c.features&popcnt != 0 +} + +// HTT indicates the processor has Hyperthreading enabled +func (c cpuInfo) htt() bool { + return c.features&htt != 0 +} + +// SSE2Slow indicates that SSE2 may be slow on this processor +func (c cpuInfo) sse2slow() bool { + return c.features&sse2slow != 0 +} + +// SSE3Slow indicates that SSE3 may be slow on this processor +func (c cpuInfo) sse3slow() bool { + return c.features&sse3slow != 0 +} + +// AesNi indicates support of AES-NI instructions +// (Advanced Encryption Standard New Instructions) +func (c cpuInfo) aesni() bool { + return c.features&aesni != 0 +} + +// Clmul indicates support of CLMUL instructions +// (Carry-less Multiplication) +func (c cpuInfo) clmul() bool { + return c.features&clmul != 0 +} + +// NX indicates support of NX (No-Execute) bit +func (c cpuInfo) nx() bool { + return c.features&nx != 0 +} + +// SSE4A indicates support of AMD Barcelona microarchitecture SSE4a instructions +func (c cpuInfo) sse4a() bool { + return c.features&sse4a != 0 +} + +// HLE indicates support of Hardware Lock Elision +func (c cpuInfo) hle() bool { + return c.features&hle != 0 +} + +// RTM indicates support of Restricted Transactional Memory +func (c cpuInfo) rtm() bool { + return c.features&rtm != 0 +} + +// Rdrand indicates support of RDRAND instruction is available +func (c cpuInfo) rdrand() bool { + return c.features&rdrand != 0 +} + +// Rdseed indicates support of RDSEED instruction is available +func (c cpuInfo) rdseed() bool { + return c.features&rdseed != 0 +} + +// ADX indicates support of Intel ADX (Multi-Precision Add-Carry Instruction Extensions) +func (c cpuInfo) adx() bool { + return c.features&adx != 0 +} + +// SHA indicates support of Intel SHA Extensions +func (c cpuInfo) sha() bool { + return c.features&sha != 0 +} + +// AVX512F indicates support of AVX-512 Foundation +func (c cpuInfo) avx512f() bool { + return c.features&avx512f != 0 +} + +// AVX512DQ indicates support of AVX-512 Doubleword and Quadword Instructions +func (c cpuInfo) avx512dq() bool { + return c.features&avx512dq != 0 +} + +// AVX512IFMA indicates support of AVX-512 Integer Fused Multiply-Add Instructions +func (c cpuInfo) avx512ifma() bool { + return c.features&avx512ifma != 0 +} + +// AVX512PF indicates support of AVX-512 Prefetch Instructions +func (c cpuInfo) avx512pf() bool { + return c.features&avx512pf != 0 +} + +// AVX512ER indicates support of AVX-512 Exponential and Reciprocal Instructions +func (c cpuInfo) avx512er() bool { + return c.features&avx512er != 0 +} + +// AVX512CD indicates support of AVX-512 Conflict Detection Instructions +func (c cpuInfo) avx512cd() bool { + return c.features&avx512cd != 0 +} + +// AVX512BW indicates support of AVX-512 Byte and Word Instructions +func (c cpuInfo) avx512bw() bool { + return c.features&avx512bw != 0 +} + +// AVX512VL indicates support of AVX-512 Vector Length Extensions +func (c cpuInfo) avx512vl() bool { + return c.features&avx512vl != 0 +} + +// AVX512VBMI indicates support of AVX-512 Vector Bit Manipulation Instructions +func (c cpuInfo) avx512vbmi() bool { + return c.features&avx512vbmi != 0 +} + +// MPX indicates support of Intel MPX (Memory Protection Extensions) +func (c cpuInfo) mpx() bool { + return c.features&mpx != 0 +} + +// ERMS indicates support of Enhanced REP MOVSB/STOSB +func (c cpuInfo) erms() bool { + return c.features&erms != 0 +} + +// RDTSCP Instruction is available. +func (c cpuInfo) rdtscp() bool { + return c.features&rdtscp != 0 +} + +// CX16 indicates if CMPXCHG16B instruction is available. +func (c cpuInfo) cx16() bool { + return c.features&cx16 != 0 +} + +// TSX is split into HLE (Hardware Lock Elision) and RTM (Restricted Transactional Memory) detection. +// So TSX simply checks that. +func (c cpuInfo) tsx() bool { + return c.features&(mpx|rtm) == mpx|rtm +} + +// Atom indicates an Atom processor +func (c cpuInfo) atom() bool { + return c.features&atom != 0 +} + +// Intel returns true if vendor is recognized as Intel +func (c cpuInfo) intel() bool { + return c.vendorid == intel +} + +// AMD returns true if vendor is recognized as AMD +func (c cpuInfo) amd() bool { + return c.vendorid == amd +} + +// Transmeta returns true if vendor is recognized as Transmeta +func (c cpuInfo) transmeta() bool { + return c.vendorid == transmeta +} + +// NSC returns true if vendor is recognized as National Semiconductor +func (c cpuInfo) nsc() bool { + return c.vendorid == nsc +} + +// VIA returns true if vendor is recognized as VIA +func (c cpuInfo) via() bool { + return c.vendorid == via +} + +// RTCounter returns the 64-bit time-stamp counter +// Uses the RDTSCP instruction. The value 0 is returned +// if the CPU does not support the instruction. +func (c cpuInfo) rtcounter() uint64 { + if !c.rdtscp() { + return 0 + } + a, _, _, d := rdtscpAsm() + return uint64(a) | (uint64(d) << 32) +} + +// Ia32TscAux returns the IA32_TSC_AUX part of the RDTSCP. +// This variable is OS dependent, but on Linux contains information +// about the current cpu/core the code is running on. +// If the RDTSCP instruction isn't supported on the CPU, the value 0 is returned. +func (c cpuInfo) ia32tscaux() uint32 { + if !c.rdtscp() { + return 0 + } + _, _, ecx, _ := rdtscpAsm() + return ecx +} + +// LogicalCPU will return the Logical CPU the code is currently executing on. +// This is likely to change when the OS re-schedules the running thread +// to another CPU. +// If the current core cannot be detected, -1 will be returned. +func (c cpuInfo) logicalcpu() int { + if c.maxFunc < 1 { + return -1 + } + _, ebx, _, _ := cpuid(1) + return int(ebx >> 24) +} + +// VM Will return true if the cpu id indicates we are in +// a virtual machine. This is only a hint, and will very likely +// have many false negatives. +func (c cpuInfo) vm() bool { + switch c.vendorid { + case msvm, kvm, vmware, xenhvm: + return true + } + return false +} + +// Flags contains detected cpu features and caracteristics +type flags uint64 + +// String returns a string representation of the detected +// CPU features. +func (f flags) String() string { + return strings.Join(f.strings(), ",") +} + +// Strings returns and array of the detected features. +func (f flags) strings() []string { + s := support() + r := make([]string, 0, 20) + for i := uint(0); i < 64; i++ { + key := flags(1 << i) + val := flagNames[key] + if s&key != 0 { + r = append(r, val) + } + } + return r +} + +func maxExtendedFunction() uint32 { + eax, _, _, _ := cpuid(0x80000000) + return eax +} + +func maxFunctionID() uint32 { + a, _, _, _ := cpuid(0) + return a +} + +func brandName() string { + if maxExtendedFunction() >= 0x80000004 { + v := make([]uint32, 0, 48) + for i := uint32(0); i < 3; i++ { + a, b, c, d := cpuid(0x80000002 + i) + v = append(v, a, b, c, d) + } + return strings.Trim(string(valAsString(v...)), " ") + } + return "unknown" +} + +func threadsPerCore() int { + mfi := maxFunctionID() + if mfi < 0x4 || vendorID() != intel { + return 1 + } + + if mfi < 0xb { + _, b, _, d := cpuid(1) + if (d & (1 << 28)) != 0 { + // v will contain logical core count + v := (b >> 16) & 255 + if v > 1 { + a4, _, _, _ := cpuid(4) + // physical cores + v2 := (a4 >> 26) + 1 + if v2 > 0 { + return int(v) / int(v2) + } + } + } + return 1 + } + _, b, _, _ := cpuidex(0xb, 0) + if b&0xffff == 0 { + return 1 + } + return int(b & 0xffff) +} + +func logicalCores() int { + mfi := maxFunctionID() + switch vendorID() { + case intel: + // Use this on old Intel processors + if mfi < 0xb { + if mfi < 1 { + return 0 + } + // CPUID.1:EBX[23:16] represents the maximum number of addressable IDs (initial APIC ID) + // that can be assigned to logical processors in a physical package. + // The value may not be the same as the number of logical processors that are present in the hardware of a physical package. + _, ebx, _, _ := cpuid(1) + logical := (ebx >> 16) & 0xff + return int(logical) + } + _, b, _, _ := cpuidex(0xb, 1) + return int(b & 0xffff) + case amd: + _, b, _, _ := cpuid(1) + return int((b >> 16) & 0xff) + default: + return 0 + } +} + +func familyModel() (int, int) { + if maxFunctionID() < 0x1 { + return 0, 0 + } + eax, _, _, _ := cpuid(1) + family := ((eax >> 8) & 0xf) + ((eax >> 20) & 0xff) + model := ((eax >> 4) & 0xf) + ((eax >> 12) & 0xf0) + return int(family), int(model) +} + +func physicalCores() int { + switch vendorID() { + case intel: + return logicalCores() / threadsPerCore() + case amd: + if maxExtendedFunction() >= 0x80000008 { + _, _, c, _ := cpuid(0x80000008) + return int(c&0xff) + 1 + } + } + return 0 +} + +// Except from http://en.wikipedia.org/wiki/CPUID#EAX.3D0:_Get_vendor_ID +var vendorMapping = map[string]vendor{ + "AMDisbetter!": amd, + "AuthenticAMD": amd, + "CentaurHauls": via, + "GenuineIntel": intel, + "TransmetaCPU": transmeta, + "GenuineTMx86": transmeta, + "Geode by NSC": nsc, + "VIA VIA VIA ": via, + "KVMKVMKVMKVM": kvm, + "Microsoft Hv": msvm, + "VMwareVMware": vmware, + "XenVMMXenVMM": xenhvm, +} + +func vendorID() vendor { + _, b, c, d := cpuid(0) + v := valAsString(b, d, c) + vend, ok := vendorMapping[string(v)] + if !ok { + return other + } + return vend +} + +func cacheLine() int { + if maxFunctionID() < 0x1 { + return 0 + } + + _, ebx, _, _ := cpuid(1) + cache := (ebx & 0xff00) >> 5 // cflush size + if cache == 0 && maxExtendedFunction() >= 0x80000006 { + _, _, ecx, _ := cpuid(0x80000006) + cache = ecx & 0xff // cacheline size + } + // TODO: Read from Cache and TLB Information + return int(cache) +} + +func (c *cpuInfo) cacheSize() { + c.cache.l1d = -1 + c.cache.l1i = -1 + c.cache.l2 = -1 + c.cache.l3 = -1 + vendor := vendorID() + switch vendor { + case intel: + if maxFunctionID() < 4 { + return + } + for i := uint32(0); ; i++ { + eax, ebx, ecx, _ := cpuidex(4, i) + cacheType := eax & 15 + if cacheType == 0 { + break + } + cacheLevel := (eax >> 5) & 7 + coherency := int(ebx&0xfff) + 1 + partitions := int((ebx>>12)&0x3ff) + 1 + associativity := int((ebx>>22)&0x3ff) + 1 + sets := int(ecx) + 1 + size := associativity * partitions * coherency * sets + switch cacheLevel { + case 1: + if cacheType == 1 { + // 1 = Data Cache + c.cache.l1d = size + } else if cacheType == 2 { + // 2 = Instruction Cache + c.cache.l1i = size + } else { + if c.cache.l1d < 0 { + c.cache.l1i = size + } + if c.cache.l1i < 0 { + c.cache.l1i = size + } + } + case 2: + c.cache.l2 = size + case 3: + c.cache.l3 = size + } + } + case amd: + // Untested. + if maxExtendedFunction() < 0x80000005 { + return + } + _, _, ecx, edx := cpuid(0x80000005) + c.cache.l1d = int(((ecx >> 24) & 0xFF) * 1024) + c.cache.l1i = int(((edx >> 24) & 0xFF) * 1024) + + if maxExtendedFunction() < 0x80000006 { + return + } + _, _, ecx, _ = cpuid(0x80000006) + c.cache.l2 = int(((ecx >> 16) & 0xFFFF) * 1024) + } + + return +} + +type sgxsupport struct { + available bool + sgx1supported bool + sgx2supported bool + maxenclavesizenot64 int64 + maxenclavesize64 int64 +} + +func hasSGX(available bool) (rval sgxsupport) { + rval.available = available + + if !available { + return + } + + a, _, _, d := cpuidex(0x12, 0) + rval.sgx1supported = a&0x01 != 0 + rval.sgx2supported = a&0x02 != 0 + rval.maxenclavesizenot64 = 1 << (d & 0xFF) // pow 2 + rval.maxenclavesize64 = 1 << ((d >> 8) & 0xFF) // pow 2 + + return +} + +func support() flags { + mfi := maxFunctionID() + vend := vendorID() + if mfi < 0x1 { + return 0 + } + rval := uint64(0) + _, _, c, d := cpuid(1) + if (d & (1 << 15)) != 0 { + rval |= cmov + } + if (d & (1 << 23)) != 0 { + rval |= mmx + } + if (d & (1 << 25)) != 0 { + rval |= mmxext + } + if (d & (1 << 25)) != 0 { + rval |= sse + } + if (d & (1 << 26)) != 0 { + rval |= sse2 + } + if (c & 1) != 0 { + rval |= sse3 + } + if (c & 0x00000200) != 0 { + rval |= ssse3 + } + if (c & 0x00080000) != 0 { + rval |= sse4 + } + if (c & 0x00100000) != 0 { + rval |= sse42 + } + if (c & (1 << 25)) != 0 { + rval |= aesni + } + if (c & (1 << 1)) != 0 { + rval |= clmul + } + if c&(1<<23) != 0 { + rval |= popcnt + } + if c&(1<<30) != 0 { + rval |= rdrand + } + if c&(1<<29) != 0 { + rval |= f16c + } + if c&(1<<13) != 0 { + rval |= cx16 + } + if vend == intel && (d&(1<<28)) != 0 && mfi >= 4 { + if threadsPerCore() > 1 { + rval |= htt + } + } + + // Check XGETBV, OXSAVE and AVX bits + if c&(1<<26) != 0 && c&(1<<27) != 0 && c&(1<<28) != 0 { + // Check for OS support + eax, _ := xgetbv(0) + if (eax & 0x6) == 0x6 { + rval |= avx + if (c & 0x00001000) != 0 { + rval |= fma3 + } + } + } + + // Check AVX2, AVX2 requires OS support, but BMI1/2 don't. + if mfi >= 7 { + _, ebx, ecx, _ := cpuidex(7, 0) + if (rval&avx) != 0 && (ebx&0x00000020) != 0 { + rval |= avx2 + } + if (ebx & 0x00000008) != 0 { + rval |= bmi1 + if (ebx & 0x00000100) != 0 { + rval |= bmi2 + } + } + if ebx&(1<<2) != 0 { + rval |= sgx + } + if ebx&(1<<4) != 0 { + rval |= hle + } + if ebx&(1<<9) != 0 { + rval |= erms + } + if ebx&(1<<11) != 0 { + rval |= rtm + } + if ebx&(1<<14) != 0 { + rval |= mpx + } + if ebx&(1<<18) != 0 { + rval |= rdseed + } + if ebx&(1<<19) != 0 { + rval |= adx + } + if ebx&(1<<29) != 0 { + rval |= sha + } + + // Only detect AVX-512 features if XGETBV is supported + if c&((1<<26)|(1<<27)) == (1<<26)|(1<<27) { + // Check for OS support + eax, _ := xgetbv(0) + + // Verify that XCR0[7:5] = ‘111b’ (OPMASK state, upper 256-bit of ZMM0-ZMM15 and + // ZMM16-ZMM31 state are enabled by OS) + /// and that XCR0[2:1] = ‘11b’ (XMM state and YMM state are enabled by OS). + if (eax>>5)&7 == 7 && (eax>>1)&3 == 3 { + if ebx&(1<<16) != 0 { + rval |= avx512f + } + if ebx&(1<<17) != 0 { + rval |= avx512dq + } + if ebx&(1<<21) != 0 { + rval |= avx512ifma + } + if ebx&(1<<26) != 0 { + rval |= avx512pf + } + if ebx&(1<<27) != 0 { + rval |= avx512er + } + if ebx&(1<<28) != 0 { + rval |= avx512cd + } + if ebx&(1<<30) != 0 { + rval |= avx512bw + } + if ebx&(1<<31) != 0 { + rval |= avx512vl + } + // ecx + if ecx&(1<<1) != 0 { + rval |= avx512vbmi + } + } + } + } + + if maxExtendedFunction() >= 0x80000001 { + _, _, c, d := cpuid(0x80000001) + if (c & (1 << 5)) != 0 { + rval |= lzcnt + rval |= popcnt + } + if (d & (1 << 31)) != 0 { + rval |= amd3dnow + } + if (d & (1 << 30)) != 0 { + rval |= amd3dnowext + } + if (d & (1 << 23)) != 0 { + rval |= mmx + } + if (d & (1 << 22)) != 0 { + rval |= mmxext + } + if (c & (1 << 6)) != 0 { + rval |= sse4a + } + if d&(1<<20) != 0 { + rval |= nx + } + if d&(1<<27) != 0 { + rval |= rdtscp + } + + /* Allow for selectively disabling SSE2 functions on AMD processors + with SSE2 support but not SSE4a. This includes Athlon64, some + Opteron, and some Sempron processors. MMX, SSE, or 3DNow! are faster + than SSE2 often enough to utilize this special-case flag. + AV_CPU_FLAG_SSE2 and AV_CPU_FLAG_SSE2SLOW are both set in this case + so that SSE2 is used unless explicitly disabled by checking + AV_CPU_FLAG_SSE2SLOW. */ + if vendorID() != intel && + rval&sse2 != 0 && (c&0x00000040) == 0 { + rval |= sse2slow + } + + /* XOP and FMA4 use the AVX instruction coding scheme, so they can't be + * used unless the OS has AVX support. */ + if (rval & avx) != 0 { + if (c & 0x00000800) != 0 { + rval |= xop + } + if (c & 0x00010000) != 0 { + rval |= fma4 + } + } + + if vendorID() == intel { + family, model := familyModel() + if family == 6 && (model == 9 || model == 13 || model == 14) { + /* 6/9 (pentium-m "banias"), 6/13 (pentium-m "dothan"), and + * 6/14 (core1 "yonah") theoretically support sse2, but it's + * usually slower than mmx. */ + if (rval & sse2) != 0 { + rval |= sse2slow + } + if (rval & sse3) != 0 { + rval |= sse3slow + } + } + /* The Atom processor has SSSE3 support, which is useful in many cases, + * but sometimes the SSSE3 version is slower than the SSE2 equivalent + * on the Atom, but is generally faster on other processors supporting + * SSSE3. This flag allows for selectively disabling certain SSSE3 + * functions on the Atom. */ + if family == 6 && model == 28 { + rval |= atom + } + } + } + return flags(rval) +} + +func valAsString(values ...uint32) []byte { + r := make([]byte, 4*len(values)) + for i, v := range values { + dst := r[i*4:] + dst[0] = byte(v & 0xff) + dst[1] = byte((v >> 8) & 0xff) + dst[2] = byte((v >> 16) & 0xff) + dst[3] = byte((v >> 24) & 0xff) + switch { + case dst[0] == 0: + return r[:i*4] + case dst[1] == 0: + return r[:i*4+1] + case dst[2] == 0: + return r[:i*4+2] + case dst[3] == 0: + return r[:i*4+3] + } + } + return r +} diff --git a/vendor/github.com/klauspost/cpuid/private/cpuid_386.s b/vendor/github.com/klauspost/cpuid/private/cpuid_386.s new file mode 100644 index 000000000..4d731711e --- /dev/null +++ b/vendor/github.com/klauspost/cpuid/private/cpuid_386.s @@ -0,0 +1,42 @@ +// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file. + +// +build 386,!gccgo + +// func asmCpuid(op uint32) (eax, ebx, ecx, edx uint32) +TEXT ·asmCpuid(SB), 7, $0 + XORL CX, CX + MOVL op+0(FP), AX + CPUID + MOVL AX, eax+4(FP) + MOVL BX, ebx+8(FP) + MOVL CX, ecx+12(FP) + MOVL DX, edx+16(FP) + RET + +// func asmCpuidex(op, op2 uint32) (eax, ebx, ecx, edx uint32) +TEXT ·asmCpuidex(SB), 7, $0 + MOVL op+0(FP), AX + MOVL op2+4(FP), CX + CPUID + MOVL AX, eax+8(FP) + MOVL BX, ebx+12(FP) + MOVL CX, ecx+16(FP) + MOVL DX, edx+20(FP) + RET + +// func xgetbv(index uint32) (eax, edx uint32) +TEXT ·asmXgetbv(SB), 7, $0 + MOVL index+0(FP), CX + BYTE $0x0f; BYTE $0x01; BYTE $0xd0 // XGETBV + MOVL AX, eax+4(FP) + MOVL DX, edx+8(FP) + RET + +// func asmRdtscpAsm() (eax, ebx, ecx, edx uint32) +TEXT ·asmRdtscpAsm(SB), 7, $0 + BYTE $0x0F; BYTE $0x01; BYTE $0xF9 // RDTSCP + MOVL AX, eax+0(FP) + MOVL BX, ebx+4(FP) + MOVL CX, ecx+8(FP) + MOVL DX, edx+12(FP) + RET diff --git a/vendor/github.com/klauspost/cpuid/private/cpuid_amd64.s b/vendor/github.com/klauspost/cpuid/private/cpuid_amd64.s new file mode 100644 index 000000000..3c1d60e42 --- /dev/null +++ b/vendor/github.com/klauspost/cpuid/private/cpuid_amd64.s @@ -0,0 +1,42 @@ +// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file. + +//+build amd64,!gccgo + +// func asmCpuid(op uint32) (eax, ebx, ecx, edx uint32) +TEXT ·asmCpuid(SB), 7, $0 + XORQ CX, CX + MOVL op+0(FP), AX + CPUID + MOVL AX, eax+8(FP) + MOVL BX, ebx+12(FP) + MOVL CX, ecx+16(FP) + MOVL DX, edx+20(FP) + RET + +// func asmCpuidex(op, op2 uint32) (eax, ebx, ecx, edx uint32) +TEXT ·asmCpuidex(SB), 7, $0 + MOVL op+0(FP), AX + MOVL op2+4(FP), CX + CPUID + MOVL AX, eax+8(FP) + MOVL BX, ebx+12(FP) + MOVL CX, ecx+16(FP) + MOVL DX, edx+20(FP) + RET + +// func asmXgetbv(index uint32) (eax, edx uint32) +TEXT ·asmXgetbv(SB), 7, $0 + MOVL index+0(FP), CX + BYTE $0x0f; BYTE $0x01; BYTE $0xd0 // XGETBV + MOVL AX, eax+8(FP) + MOVL DX, edx+12(FP) + RET + +// func asmRdtscpAsm() (eax, ebx, ecx, edx uint32) +TEXT ·asmRdtscpAsm(SB), 7, $0 + BYTE $0x0F; BYTE $0x01; BYTE $0xF9 // RDTSCP + MOVL AX, eax+0(FP) + MOVL BX, ebx+4(FP) + MOVL CX, ecx+8(FP) + MOVL DX, edx+12(FP) + RET diff --git a/vendor/github.com/klauspost/cpuid/private/cpuid_detect_intel.go b/vendor/github.com/klauspost/cpuid/private/cpuid_detect_intel.go new file mode 100644 index 000000000..a5f04dd6d --- /dev/null +++ b/vendor/github.com/klauspost/cpuid/private/cpuid_detect_intel.go @@ -0,0 +1,17 @@ +// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file. + +// +build 386,!gccgo amd64,!gccgo + +package cpuid + +func asmCpuid(op uint32) (eax, ebx, ecx, edx uint32) +func asmCpuidex(op, op2 uint32) (eax, ebx, ecx, edx uint32) +func asmXgetbv(index uint32) (eax, edx uint32) +func asmRdtscpAsm() (eax, ebx, ecx, edx uint32) + +func initCPU() { + cpuid = asmCpuid + cpuidex = asmCpuidex + xgetbv = asmXgetbv + rdtscpAsm = asmRdtscpAsm +} diff --git a/vendor/github.com/klauspost/cpuid/private/cpuid_detect_ref.go b/vendor/github.com/klauspost/cpuid/private/cpuid_detect_ref.go new file mode 100644 index 000000000..909c5d9a7 --- /dev/null +++ b/vendor/github.com/klauspost/cpuid/private/cpuid_detect_ref.go @@ -0,0 +1,23 @@ +// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file. + +// +build !amd64,!386 gccgo + +package cpuid + +func initCPU() { + cpuid = func(op uint32) (eax, ebx, ecx, edx uint32) { + return 0, 0, 0, 0 + } + + cpuidex = func(op, op2 uint32) (eax, ebx, ecx, edx uint32) { + return 0, 0, 0, 0 + } + + xgetbv = func(index uint32) (eax, edx uint32) { + return 0, 0 + } + + rdtscpAsm = func() (eax, ebx, ecx, edx uint32) { + return 0, 0, 0, 0 + } +} diff --git a/vendor/manifest b/vendor/manifest index a44a451e6..b8d136533 100644 --- a/vendor/manifest +++ b/vendor/manifest @@ -117,6 +117,14 @@ "path": "/basic", "notests": true }, + { + "importpath": "github.com/klauspost/cpuid", + "repository": "https://github.com/klauspost/cpuid", + "vcs": "git", + "revision": "ae832f27941af41db13bd6d8efd2493e3b22415a", + "branch": "master", + "notests": true + }, { "importpath": "github.com/lucas-clemente/aes12", "repository": "https://github.com/lucas-clemente/aes12", From 6c17e4d4c8df4eb286df6bed19d08b489c043558 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 8 Feb 2018 21:15:28 -0700 Subject: [PATCH 03/28] diagnostics: Add a few tests --- diagnostics/collection.go | 3 +- diagnostics/collection_test.go | 109 ++++++++++++++++++++++++++++++++ diagnostics/diagnostics.go | 54 ++++++++-------- diagnostics/diagnostics_test.go | 59 +++++++++++++++++ 4 files changed, 198 insertions(+), 27 deletions(-) create mode 100644 diagnostics/collection_test.go create mode 100644 diagnostics/diagnostics_test.go diff --git a/diagnostics/collection.go b/diagnostics/collection.go index 40e42e5b7..f3361c564 100644 --- a/diagnostics/collection.go +++ b/diagnostics/collection.go @@ -32,7 +32,8 @@ func Init(instanceID uuid.UUID) { if enabled { panic("already initialized") } - if instanceID.String() == "" { + if str := instanceID.String(); str == "" || + instanceID.String() == "00000000-0000-0000-0000-000000000000" { panic("empty UUID") } instanceUUID = instanceID diff --git a/diagnostics/collection_test.go b/diagnostics/collection_test.go new file mode 100644 index 000000000..e895a0a4e --- /dev/null +++ b/diagnostics/collection_test.go @@ -0,0 +1,109 @@ +// Copyright 2015 Light Code Labs, LLC +// +// 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 diagnostics + +import ( + "fmt" + "testing" + + "github.com/google/uuid" +) + +func TestInit(t *testing.T) { + reset() + + id := doInit(t) // should not panic + + defer func() { + if r := recover(); r == nil { + t.Errorf("Second call to Init should have panicked") + } + }() + Init(id) // should panic +} + +func TestInitEmptyUUID(t *testing.T) { + reset() + defer func() { + if r := recover(); r == nil { + t.Errorf("Call to Init with empty UUID should have panicked") + } + }() + Init(uuid.UUID([16]byte{})) +} + +func TestSet(t *testing.T) { + reset() + + // should be no-op since we haven't called Init() yet + Set("test1", "foobar") + if _, ok := buffer["test"]; ok { + t.Errorf("Should not have inserted item when not initialized") + } + + // should work after we've initialized + doInit(t) + Set("test1", "foobar") + val, ok := buffer["test1"] + if !ok { + t.Errorf("Expected value to be in buffer, but it wasn't") + } else if val.(string) != "foobar" { + t.Errorf("Expected 'foobar', got '%v'", val) + } + + // should not overfill buffer + maxBufferItemsTmp := maxBufferItems + maxBufferItems = 10 + for i := 0; i < maxBufferItems+1; i++ { + Set(fmt.Sprintf("overfill_%d", i), "foobar") + } + if len(buffer) > maxBufferItems { + t.Errorf("Should not exceed max buffer size (%d); has %d items", + maxBufferItems, len(buffer)) + } + maxBufferItems = maxBufferItemsTmp + + // Should overwrite values + Set("test1", "foobar2") + val, ok = buffer["test1"] + if !ok { + t.Errorf("Expected value to be in buffer, but it wasn't") + } else if val.(string) != "foobar2" { + t.Errorf("Expected 'foobar2', got '%v'", val) + } +} + +// doInit calls Init() with a valid UUID +// and returns it. +func doInit(t *testing.T) uuid.UUID { + id, err := uuid.Parse(testUUID) + if err != nil { + t.Fatalf("Could not make UUID: %v", err) + } + Init(id) + return id +} + +// reset resets all the lovely package-level state; +// can be used as a set up function in tests. +func reset() { + instanceUUID = uuid.UUID{} + buffer = make(map[string]interface{}) + bufferItemCount = 0 + updating = false + enabled = false +} + +const testUUID = "0b6cfa22-0d4c-11e8-b11b-7a0058e13201" diff --git a/diagnostics/diagnostics.go b/diagnostics/diagnostics.go index dd4cac87d..2d6be6e70 100644 --- a/diagnostics/diagnostics.go +++ b/diagnostics/diagnostics.go @@ -216,32 +216,39 @@ type Payload struct { Data map[string]interface{} `json:"data,omitempty"` } -// httpClient should be used for HTTP requests. It -// is configured with a timeout for reliability. -var httpClient = http.Client{Timeout: 1 * time.Minute} +var ( + // httpClient should be used for HTTP requests. It + // is configured with a timeout for reliability. + httpClient = http.Client{Timeout: 1 * time.Minute} -// buffer holds the data that we are building up to send. -var buffer = make(map[string]interface{}) -var bufferItemCount = 0 -var bufferMu sync.RWMutex // protects both the buffer and its count + // buffer holds the data that we are building up to send. + buffer = make(map[string]interface{}) + bufferItemCount = 0 + bufferMu sync.RWMutex // protects both the buffer and its count -// updating is used to ensure only one -// update happens at a time. -var updating bool -var updateMu sync.Mutex + // updating is used to ensure only one + // update happens at a time. + updating bool + updateMu sync.Mutex -// updateTimer fires off the next update. -// If no update is scheduled, this is nil. -var updateTimer *time.Timer -var updateTimerMu sync.Mutex + // updateTimer fires off the next update. + // If no update is scheduled, this is nil. + updateTimer *time.Timer + updateTimerMu sync.Mutex -// instanceUUID is the ID of the current instance. -// This MUST be set to emit diagnostics. -var instanceUUID uuid.UUID + // instanceUUID is the ID of the current instance. + // This MUST be set to emit diagnostics. + instanceUUID uuid.UUID -// enabled indicates whether the package has -// been initialized and can be actively used. -var enabled bool + // enabled indicates whether the package has + // been initialized and can be actively used. + enabled bool + + // maxBufferItems is the maximum number of items we'll allow + // in the buffer before we start dropping new ones, in a + // rough (simple) attempt to keep memory use under control. + maxBufferItems = 100000 +) const ( // endpoint is the base URL to remote diagnostics server; @@ -255,9 +262,4 @@ const ( // this value should be a long duration to help alleviate // extra load on the server. defaultUpdateInterval = 1 * time.Hour - - // maxBufferItems is the maximum number of items we'll allow - // in the buffer before we start dropping new ones, in a - // rough (simple) attempt to keep memory use under control. - maxBufferItems = 100000 ) diff --git a/diagnostics/diagnostics_test.go b/diagnostics/diagnostics_test.go new file mode 100644 index 000000000..af458d99b --- /dev/null +++ b/diagnostics/diagnostics_test.go @@ -0,0 +1,59 @@ +// Copyright 2015 Light Code Labs, LLC +// +// 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 diagnostics + +import ( + "encoding/json" + "testing" +) + +func TestMakePayloadAndResetBuffer(t *testing.T) { + reset() + id := doInit(t) + + buffer = map[string]interface{}{ + "foo1": "bar1", + "foo2": "bar2", + } + bufferItemCount = 2 + + payloadBytes, err := makePayloadAndResetBuffer() + if err != nil { + t.Fatalf("Error making payload bytes: %v", err) + } + + if len(buffer) != 0 { + t.Errorf("Expected buffer len to be 0, got %d", len(buffer)) + } + if bufferItemCount != 0 { + t.Errorf("Expected buffer item count to be 0, got %d", bufferItemCount) + } + + var payload Payload + err = json.Unmarshal(payloadBytes, &payload) + if err != nil { + t.Fatalf("Error deserializing payload: %v", err) + } + + if payload.InstanceID != id.String() { + t.Errorf("Expected instance ID to be set to '%s' but got '%s'", testUUID, payload.InstanceID) + } + if payload.Data == nil { + t.Errorf("Expected data to be set, but was nil") + } + if payload.Timestamp.IsZero() { + t.Errorf("Expected timestamp to be set, but was zero value") + } +} From 3e00e18adcdb6f73992fe6628f07ed2439dbcf53 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 8 Feb 2018 23:37:42 -0700 Subject: [PATCH 04/28] diagnostics: Point to staging endpoint --- diagnostics/diagnostics.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diagnostics/diagnostics.go b/diagnostics/diagnostics.go index 2d6be6e70..79296ab22 100644 --- a/diagnostics/diagnostics.go +++ b/diagnostics/diagnostics.go @@ -253,7 +253,7 @@ var ( const ( // endpoint is the base URL to remote diagnostics server; // the instance ID will be appended to it. - endpoint = "http://localhost:8081/update/" + endpoint = "https://diagnostics-staging.caddyserver.com/update/" // TODO: make configurable, "http://localhost:8081/update/" // defaultUpdateInterval is how long to wait before emitting // more diagnostic data. This value is only used if the From 703cf7bf8bab24f23773f8aeefaa2bf54e3c385b Mon Sep 17 00:00:00 2001 From: elcore Date: Fri, 9 Feb 2018 18:39:21 +0100 Subject: [PATCH 05/28] vendor: delete github.com/codahale/aesnicheck in favor of cpuid (#2020) --- caddytls/config.go | 4 ++-- caddytls/config_test.go | 4 ++-- vendor/github.com/codahale/aesnicheck/LICENSE | 21 ------------------ .../codahale/aesnicheck/asm_amd64.s | 9 -------- .../codahale/aesnicheck/check_asm.go | 6 ----- .../codahale/aesnicheck/check_generic.go | 8 ------- .../aesnicheck/cmd/aesnicheck/aesnicheck.go | 22 ------------------- vendor/github.com/codahale/aesnicheck/docs.go | 9 -------- vendor/manifest | 8 ------- 9 files changed, 4 insertions(+), 87 deletions(-) delete mode 100644 vendor/github.com/codahale/aesnicheck/LICENSE delete mode 100644 vendor/github.com/codahale/aesnicheck/asm_amd64.s delete mode 100644 vendor/github.com/codahale/aesnicheck/check_asm.go delete mode 100644 vendor/github.com/codahale/aesnicheck/check_generic.go delete mode 100644 vendor/github.com/codahale/aesnicheck/cmd/aesnicheck/aesnicheck.go delete mode 100644 vendor/github.com/codahale/aesnicheck/docs.go diff --git a/caddytls/config.go b/caddytls/config.go index d3468e348..f008d1eaf 100644 --- a/caddytls/config.go +++ b/caddytls/config.go @@ -23,7 +23,7 @@ import ( "net/url" "strings" - "github.com/codahale/aesnicheck" + "github.com/klauspost/cpuid" "github.com/mholt/caddy" "github.com/xenolf/lego/acme" ) @@ -505,7 +505,7 @@ var defaultCiphersNonAESNI = []uint16{ // // See https://github.com/mholt/caddy/issues/1674 func getPreferredDefaultCiphers() []uint16 { - if aesnicheck.HasAESNI() { + if cpuid.CPU.AesNi() { return defaultCiphers } diff --git a/caddytls/config_test.go b/caddytls/config_test.go index 0294e0314..c69f2d550 100644 --- a/caddytls/config_test.go +++ b/caddytls/config_test.go @@ -21,7 +21,7 @@ import ( "reflect" "testing" - "github.com/codahale/aesnicheck" + "github.com/klauspost/cpuid" ) func TestConvertTLSConfigProtocolVersions(t *testing.T) { @@ -98,7 +98,7 @@ func TestConvertTLSConfigCipherSuites(t *testing.T) { func TestGetPreferredDefaultCiphers(t *testing.T) { expectedCiphers := defaultCiphers - if !aesnicheck.HasAESNI() { + if !cpuid.CPU.AesNi() { expectedCiphers = defaultCiphersNonAESNI } diff --git a/vendor/github.com/codahale/aesnicheck/LICENSE b/vendor/github.com/codahale/aesnicheck/LICENSE deleted file mode 100644 index f9835c241..000000000 --- a/vendor/github.com/codahale/aesnicheck/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Coda Hale - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/vendor/github.com/codahale/aesnicheck/asm_amd64.s b/vendor/github.com/codahale/aesnicheck/asm_amd64.s deleted file mode 100644 index d7ee7ee38..000000000 --- a/vendor/github.com/codahale/aesnicheck/asm_amd64.s +++ /dev/null @@ -1,9 +0,0 @@ -// func HasAESNI() bool -TEXT ·HasAESNI(SB),$0 - XORQ AX, AX - INCL AX - CPUID - SHRQ $25, CX - ANDQ $1, CX - MOVB CX, ret+0(FP) - RET diff --git a/vendor/github.com/codahale/aesnicheck/check_asm.go b/vendor/github.com/codahale/aesnicheck/check_asm.go deleted file mode 100644 index 7b4d332cd..000000000 --- a/vendor/github.com/codahale/aesnicheck/check_asm.go +++ /dev/null @@ -1,6 +0,0 @@ -// +build amd64 - -package aesnicheck - -// HasAESNI returns whether AES-NI is supported by the CPU. -func HasAESNI() bool diff --git a/vendor/github.com/codahale/aesnicheck/check_generic.go b/vendor/github.com/codahale/aesnicheck/check_generic.go deleted file mode 100644 index b80816038..000000000 --- a/vendor/github.com/codahale/aesnicheck/check_generic.go +++ /dev/null @@ -1,8 +0,0 @@ -// +build !amd64 - -package aesnicheck - -// HasAESNI returns whether AES-NI is supported by the CPU. -func HasAESNI() bool { - return false -} diff --git a/vendor/github.com/codahale/aesnicheck/cmd/aesnicheck/aesnicheck.go b/vendor/github.com/codahale/aesnicheck/cmd/aesnicheck/aesnicheck.go deleted file mode 100644 index ecfe1ce81..000000000 --- a/vendor/github.com/codahale/aesnicheck/cmd/aesnicheck/aesnicheck.go +++ /dev/null @@ -1,22 +0,0 @@ -// Command aesnicheck queries the CPU for AES-NI support. If AES-NI is supported, -// aesnicheck will print "supported" and exit with a status of 0. If AES-NI is -// not supported, aesnicheck will print "unsupported" and exit with a status of -// -1. -package main - -import ( - "fmt" - "os" - - "github.com/codahale/aesnicheck" -) - -func main() { - if aesnicheck.HasAESNI() { - fmt.Println("supported") - os.Exit(0) - } else { - fmt.Println("unsupported") - os.Exit(-1) - } -} diff --git a/vendor/github.com/codahale/aesnicheck/docs.go b/vendor/github.com/codahale/aesnicheck/docs.go deleted file mode 100644 index 54fa03e61..000000000 --- a/vendor/github.com/codahale/aesnicheck/docs.go +++ /dev/null @@ -1,9 +0,0 @@ -// Package aesnicheck provides a simple check to see if crypto/aes is using -// AES-NI instructions or if the AES transform is being done in software. AES-NI -// is constant-time, which makes it impervious to cache-level timing attacks. For -// security-conscious deployments on public cloud infrastructure (Amazon EC2, -// Google Compute Engine, Microsoft Azure, etc.) this may be critical. -// -// See http://eprint.iacr.org/2014/248 for details on cross-VM timing attacks on -// AES keys. -package aesnicheck diff --git a/vendor/manifest b/vendor/manifest index b8d136533..a2475e608 100644 --- a/vendor/manifest +++ b/vendor/manifest @@ -34,14 +34,6 @@ "branch": "master", "notests": true }, - { - "importpath": "github.com/codahale/aesnicheck", - "repository": "https://github.com/codahale/aesnicheck", - "vcs": "git", - "revision": "349fcc471aaccc29cd074e1275f1a494323826cd", - "branch": "master", - "notests": true - }, { "importpath": "github.com/dustin/go-humanize", "repository": "https://github.com/dustin/go-humanize", From 6b3c2212a1171ff4849d994bc1ffcac5c8434aa2 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Sat, 10 Feb 2018 12:59:23 -0700 Subject: [PATCH 06/28] diagnostics: AppendUnique(), restructure sets, add metrics, fix bugs --- caddy.go | 3 ++ caddy/caddymain/run.go | 10 ++--- caddyfile/parse.go | 3 ++ caddyhttp/httpserver/mitm.go | 9 +++++ caddyhttp/httpserver/plugin.go | 3 -- caddyhttp/httpserver/server.go | 2 +- caddytls/handshake.go | 19 +++++++++ diagnostics/collection.go | 74 +++++++++++----------------------- diagnostics/diagnostics.go | 43 ++++++++++++++++---- plugins.go | 57 +++++++++++++++++++------- 10 files changed, 141 insertions(+), 82 deletions(-) diff --git a/caddy.go b/caddy.go index 8da6d4db8..7a4b06869 100644 --- a/caddy.go +++ b/caddy.go @@ -44,6 +44,7 @@ import ( "time" "github.com/mholt/caddy/caddyfile" + "github.com/mholt/caddy/diagnostics" ) // Configurable application parameters @@ -573,6 +574,8 @@ func ValidateAndExecuteDirectives(cdyfile Input, inst *Instance, justValidate bo return err } + diagnostics.Set("num_server_blocks", len(sblocks)) + return executeDirectives(inst, cdyfile.Path(), stype.Directives(), sblocks, justValidate) } diff --git a/caddy/caddymain/run.go b/caddy/caddymain/run.go index 6ffcd5c6c..c856d54e4 100644 --- a/caddy/caddymain/run.go +++ b/caddy/caddymain/run.go @@ -152,18 +152,18 @@ func Run() { // Begin diagnostics (these are no-ops if diagnostics disabled) diagnostics.Set("caddy_version", appVersion) - // TODO: plugins diagnostics.Set("num_listeners", len(instance.Servers())) + diagnostics.Set("server_type", serverType) diagnostics.Set("os", runtime.GOOS) diagnostics.Set("arch", runtime.GOARCH) diagnostics.Set("cpu", struct { - NumLogical int `json:"num_logical"` - AESNI bool `json:"aes_ni"` - BrandName string `json:"brand_name"` + BrandName string `json:"brand_name,omitempty"` + NumLogical int `json:"num_logical,omitempty"` + AESNI bool `json:"aes_ni,omitempty"` }{ + BrandName: cpuid.CPU.BrandName, NumLogical: runtime.NumCPU(), AESNI: cpuid.CPU.AesNi(), - BrandName: cpuid.CPU.BrandName, }) diagnostics.StartEmitting() diff --git a/caddyfile/parse.go b/caddyfile/parse.go index 142a87f93..9851e1c52 100644 --- a/caddyfile/parse.go +++ b/caddyfile/parse.go @@ -20,6 +20,8 @@ import ( "os" "path/filepath" "strings" + + "github.com/mholt/caddy/diagnostics" ) // Parse parses the input just enough to group tokens, in @@ -369,6 +371,7 @@ func (p *parser) directive() error { // The directive itself is appended as a relevant token p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor]) + diagnostics.AppendUnique("directives", dir) for p.Next() { if p.Val() == "{" { diff --git a/caddyhttp/httpserver/mitm.go b/caddyhttp/httpserver/mitm.go index 93209baa2..1c1d57110 100644 --- a/caddyhttp/httpserver/mitm.go +++ b/caddyhttp/httpserver/mitm.go @@ -24,6 +24,8 @@ import ( "strconv" "strings" "sync" + + "github.com/mholt/caddy/diagnostics" ) // tlsHandler is a http.Handler that will inject a value @@ -97,6 +99,13 @@ func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if checked { r = r.WithContext(context.WithValue(r.Context(), MitmCtxKey, mitm)) + if mitm { + go diagnostics.AppendUnique("mitm", "likely") + } else { + go diagnostics.AppendUnique("mitm", "unlikely") + } + } else { + go diagnostics.AppendUnique("mitm", "unknown") } if mitm && h.closeOnMITM { diff --git a/caddyhttp/httpserver/plugin.go b/caddyhttp/httpserver/plugin.go index 58a636196..643eea7f7 100644 --- a/caddyhttp/httpserver/plugin.go +++ b/caddyhttp/httpserver/plugin.go @@ -29,7 +29,6 @@ import ( "github.com/mholt/caddy/caddyfile" "github.com/mholt/caddy/caddyhttp/staticfiles" "github.com/mholt/caddy/caddytls" - "github.com/mholt/caddy/diagnostics" ) const serverType = "http" @@ -206,8 +205,6 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) { } } - diagnostics.Set("num_sites", len(h.siteConfigs)) - // we must map (group) each config to a bind address groups, err := groupSiteConfigsByListenAddr(h.siteConfigs) if err != nil { diff --git a/caddyhttp/httpserver/server.go b/caddyhttp/httpserver/server.go index 5033bb21e..9f42c2e17 100644 --- a/caddyhttp/httpserver/server.go +++ b/caddyhttp/httpserver/server.go @@ -346,7 +346,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } }() - go diagnostics.AppendUniqueString("user_agent", r.Header.Get("User-Agent")) + go diagnostics.AppendUnique("user_agent", r.Header.Get("User-Agent")) // copy the original, unchanged URL into the context // so it can be referenced by middlewares diff --git a/caddytls/handshake.go b/caddytls/handshake.go index c50e8ab63..27b9961d2 100644 --- a/caddytls/handshake.go +++ b/caddytls/handshake.go @@ -25,6 +25,8 @@ import ( "sync" "sync/atomic" "time" + + "github.com/mholt/caddy/diagnostics" ) // configGroup is a type that keys configs by their hostname @@ -98,6 +100,23 @@ func (cg configGroup) GetConfigForClient(clientHello *tls.ClientHelloInfo) (*tls // // This method is safe for use as a tls.Config.GetCertificate callback. func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { + go diagnostics.Append("client_hello", struct { + NoSNI bool `json:"no_sni,omitempty"` + CipherSuites []uint16 `json:"cipher_suites,omitempty"` + SupportedCurves []tls.CurveID `json:"curves,omitempty"` + SupportedPoints []uint8 `json:"points,omitempty"` + SignatureSchemes []tls.SignatureScheme `json:"sig_scheme,omitempty"` + ALPN []string `json:"alpn,omitempty"` + SupportedVersions []uint16 `json:"versions,omitempty"` + }{ + NoSNI: clientHello.ServerName == "", + CipherSuites: clientHello.CipherSuites, + SupportedCurves: clientHello.SupportedCurves, + SupportedPoints: clientHello.SupportedPoints, + SignatureSchemes: clientHello.SignatureSchemes, + ALPN: clientHello.SupportedProtos, + SupportedVersions: clientHello.SupportedVersions, + }) cert, err := cfg.getCertDuringHandshake(strings.ToLower(clientHello.ServerName), true, true) return &cert.Certificate, err } diff --git a/diagnostics/collection.go b/diagnostics/collection.go index f3361c564..1849aee7b 100644 --- a/diagnostics/collection.go +++ b/diagnostics/collection.go @@ -113,7 +113,7 @@ func Set(key string, val interface{}) { // Append appends value to a list named key. // If key is new, a new list will be created. // If key maps to a type that is not a list, -// an error is logged, and this is a no-op. +// a panic is logged, and this is a no-op. // // TODO: is this function needed/useful? func Append(key string, value interface{}) { @@ -142,66 +142,38 @@ func Append(key string, value interface{}) { bufferMu.Unlock() } -// AppendUniqueString adds value to a set named key. +// AppendUnique adds value to a set namedkey. // Set items are unordered. Values in the set -// are unique, but repeat values are counted. +// are unique, but how many times they are +// appended is counted. // -// If key is new, a new set will be created. -// If key maps to a type that is not a string -// set, an error is logged, and this is a no-op. -func AppendUniqueString(key, value string) { +// If key is new, a new set will be created for +// values with that key. If key maps to a type +// that is not a counting set, a panic is logged, +// and this is a no-op. +func AppendUnique(key string, value interface{}) { if !enabled { return } bufferMu.Lock() - if bufferItemCount >= maxBufferItems { - bufferMu.Unlock() - return - } bufVal, inBuffer := buffer[key] - mapVal, mapOk := bufVal.(map[string]int) - if inBuffer && !mapOk { + setVal, setOk := bufVal.(countingSet) + if inBuffer && !setOk { bufferMu.Unlock() - log.Printf("[PANIC] Diagnostics: key %s already used for non-map value", key) + log.Printf("[PANIC] Diagnostics: key %s already used for non-counting-set value", key) return } - if mapVal == nil { - buffer[key] = map[string]int{value: 1} + if setVal == nil { + // ensure the buffer is not too full, then add new unique value + if bufferItemCount >= maxBufferItems { + bufferMu.Unlock() + return + } + buffer[key] = countingSet{value: 1} bufferItemCount++ - } else if mapOk { - mapVal[value]++ - } - bufferMu.Unlock() -} - -// AppendUniqueInt adds value to a set named key. -// Set items are unordered. Values in the set -// are unique, but repeat values are counted. -// -// If key is new, a new set will be created. -// If key maps to a type that is not an integer -// set, an error is logged, and this is a no-op. -func AppendUniqueInt(key string, value int) { - if !enabled { - return - } - bufferMu.Lock() - if bufferItemCount >= maxBufferItems { - bufferMu.Unlock() - return - } - bufVal, inBuffer := buffer[key] - mapVal, mapOk := bufVal.(map[int]int) - if inBuffer && !mapOk { - bufferMu.Unlock() - log.Printf("[PANIC] Diagnostics: key %s already used for non-map value", key) - return - } - if mapVal == nil { - buffer[key] = map[int]int{value: 1} - bufferItemCount++ - } else if mapOk { - mapVal[value]++ + } else if setOk { + // unique value already exists, so just increment counter + setVal[value]++ } bufferMu.Unlock() } @@ -209,7 +181,7 @@ func AppendUniqueInt(key string, value int) { // Increment adds 1 to a value named key. // If it does not exist, it is created with // a value of 1. If key maps to a type that -// is not an integer, an error is logged, +// is not an integer, a panic is logged, // and this is a no-op. func Increment(key string) { incrementOrDecrement(key, true) diff --git a/diagnostics/diagnostics.go b/diagnostics/diagnostics.go index 79296ab22..402143d62 100644 --- a/diagnostics/diagnostics.go +++ b/diagnostics/diagnostics.go @@ -21,13 +21,16 @@ // collection/aggregation functions. Call StartEmitting() when you are // ready to begin sending diagnostic updates. // -// When collecting metrics (functions like Set, Append*, or Increment), -// it may be desirable and even recommended to run invoke them in a new +// When collecting metrics (functions like Set, AppendUnique, or Increment), +// it may be desirable and even recommended to invoke them in a new // goroutine (use the go keyword) in case there is lock contention; // they are thread-safe (unless noted), and you may not want them to // block the main thread of execution. However, sometimes blocking // may be necessary too; for example, adding startup metrics to the // buffer before the call to StartEmitting(). +// +// This package is designed to be as fast and space-efficient as reasonably +// possible, so that it does not disrupt the flow of execution. package diagnostics import ( @@ -122,11 +125,6 @@ func emit(final bool) error { continue } - // ensure we won't slam the diagnostics server - if reply.NextUpdate < 1*time.Second { - reply.NextUpdate = defaultUpdateInterval - } - // make sure we didn't send the update too soon; if so, // just wait and try again -- this is a special case of // error that we handle differently, as you can see @@ -151,6 +149,11 @@ func emit(final bool) error { // schedule the next update using our default update // interval because the server might be healthy later + // ensure we won't slam the diagnostics server + if reply.NextUpdate < 1*time.Second { + reply.NextUpdate = defaultUpdateInterval + } + // schedule the next update (if this wasn't the last one and // if the remote server didn't tell us to stop sending) if !final && !reply.Stop { @@ -216,6 +219,30 @@ type Payload struct { Data map[string]interface{} `json:"data,omitempty"` } +// countingSet implements a set that counts how many +// times a key is inserted. It marshals to JSON in a +// way such that keys are converted to values next +// to their associated counts. +type countingSet map[interface{}]int + +// MarshalJSON implements the json.Marshaler interface. +// It converts the set to an array so that the values +// are JSON object values instead of keys, since keys +// are difficult to query in databases. +func (s countingSet) MarshalJSON() ([]byte, error) { + type Item struct { + Value interface{} `json:"value"` + Count int `json:"count"` + } + var list []Item + + for k, v := range s { + list = append(list, Item{Value: k, Count: v}) + } + + return json.Marshal(list) +} + var ( // httpClient should be used for HTTP requests. It // is configured with a timeout for reliability. @@ -253,7 +280,7 @@ var ( const ( // endpoint is the base URL to remote diagnostics server; // the instance ID will be appended to it. - endpoint = "https://diagnostics-staging.caddyserver.com/update/" // TODO: make configurable, "http://localhost:8081/update/" + endpoint = "https://diagnostics-staging.caddyserver.com/update/" // TODO: make configurable, "http://localhost:8085/update/" // defaultUpdateInterval is how long to wait before emitting // more diagnostic data. This value is only used if the diff --git a/plugins.go b/plugins.go index f5372184e..8e1044c96 100644 --- a/plugins.go +++ b/plugins.go @@ -53,29 +53,59 @@ var ( // DescribePlugins returns a string describing the registered plugins. func DescribePlugins() string { + pl := ListPlugins() + str := "Server types:\n" - for name := range serverTypes { + for _, name := range pl["server_types"] { str += " " + name + "\n" } - // List the loaders in registration order str += "\nCaddyfile loaders:\n" - for _, loader := range caddyfileLoaders { - str += " " + loader.name + "\n" - } - if defaultCaddyfileLoader.name != "" { - str += " " + defaultCaddyfileLoader.name + "\n" + for _, name := range pl["caddyfile_loaders"] { + str += " " + name + "\n" } if len(eventHooks) > 0 { - // List the event hook plugins str += "\nEvent hook plugins:\n" - for hookPlugin := range eventHooks { - str += " hook." + hookPlugin + "\n" + for _, name := range pl["event_hooks"] { + str += " hook." + name + "\n" } } - // Let's alphabetize the rest of these... + str += "\nOther plugins:\n" + for _, name := range pl["others"] { + str += " " + name + "\n" + } + + return str +} + +// ListPlugins makes a list of the registered plugins, +// keyed by plugin type. +func ListPlugins() map[string][]string { + p := make(map[string][]string) + + // server type plugins + for name := range serverTypes { + p["server_types"] = append(p["server_types"], name) + } + + // caddyfile loaders in registration order + for _, loader := range caddyfileLoaders { + p["caddyfile_loaders"] = append(p["caddyfile_loaders"], loader.name) + } + if defaultCaddyfileLoader.name != "" { + p["caddyfile_loaders"] = append(p["caddyfile_loaders"], defaultCaddyfileLoader.name) + } + + // event hook plugins + if len(eventHooks) > 0 { + for name := range eventHooks { + p["event_hooks"] = append(p["event_hooks"], name) + } + } + + // alphabetize the rest of the plugins var others []string for stype, stypePlugins := range plugins { for name := range stypePlugins { @@ -89,12 +119,11 @@ func DescribePlugins() string { } sort.Strings(others) - str += "\nOther plugins:\n" for _, name := range others { - str += " " + name + "\n" + p["others"] = append(p["others"], name) } - return str + return p } // ValidDirectives returns the list of all directives that are From 5820356cf68fb7e87422133eb95dff57a8fb2c4c Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Sat, 10 Feb 2018 20:21:16 -0700 Subject: [PATCH 07/28] diagnostics: Persist UUID in string format for convenience --- caddy/caddymain/run.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/caddy/caddymain/run.go b/caddy/caddymain/run.go index c856d54e4..9f26e02fb 100644 --- a/caddy/caddymain/run.go +++ b/caddy/caddymain/run.go @@ -296,7 +296,7 @@ func initDiagnostics() { newUUID := func() uuid.UUID { id := uuid.New() - err := ioutil.WriteFile(uuidFilename, id[:], 0644) + err := ioutil.WriteFile(uuidFilename, []byte(id.String()), 0644) // human-readable this way if err != nil { log.Printf("[ERROR] Persisting instance UUID: %v", err) } @@ -319,7 +319,7 @@ func initDiagnostics() { log.Printf("[ERROR] Reading persistent UUID: %v", err) id = newUUID() } else { - id, err = uuid.FromBytes(uuidBytes) + id, err = uuid.ParseBytes(uuidBytes) if err != nil { log.Printf("[ERROR] Parsing UUID: %v", err) id = newUUID() From a6521357e5cee4b0c39134ba863b31fe9e8c70ea Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 16 Feb 2018 23:20:08 -0700 Subject: [PATCH 08/28] Fix bad merge conflict, make tests pass --- caddyhttp/basicauth/basicauth_test.go | 2 +- plugins.go | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/caddyhttp/basicauth/basicauth_test.go b/caddyhttp/basicauth/basicauth_test.go index be49d3e36..71255f763 100644 --- a/caddyhttp/basicauth/basicauth_test.go +++ b/caddyhttp/basicauth/basicauth_test.go @@ -171,7 +171,7 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61` htfh, err := ioutil.TempFile("", "basicauth-") if err != nil { - t.Skipf("Error creating temp file (%v), will skip htpassword test") + t.Skip("Error creating temp file, will skip htpassword test") return } defer os.Remove(htfh.Name()) diff --git a/plugins.go b/plugins.go index 06a73567b..f66b8baea 100644 --- a/plugins.go +++ b/plugins.go @@ -66,7 +66,7 @@ func DescribePlugins() string { str += " " + name + "\n" } - if len(eventHooks) > 0 { + if len(pl["event_hooks"]) > 0 { str += "\nEvent hook plugins:\n" for _, name := range pl["event_hooks"] { str += " hook." + name + "\n" @@ -100,15 +100,10 @@ func ListPlugins() map[string][]string { } // List the event hook plugins - hooks := "" eventHooks.Range(func(k, _ interface{}) bool { - hooks += " hook." + k.(string) + "\n" + p["event_hooks"] = append(p["event_hooks"], k.(string)) return true }) - if hooks != "" { - str += "\nEvent hook plugins:\n" - str += hooks - } // alphabetize the rest of the plugins var others []string From 385ea5330981508625548bac4a929d002a64e82f Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Sun, 18 Mar 2018 15:49:17 -0600 Subject: [PATCH 09/28] diagnostics: Use Retry-After header if decoding JSON fails Improve error message and backoff as well --- diagnostics/diagnostics.go | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/diagnostics/diagnostics.go b/diagnostics/diagnostics.go index 402143d62..0743ade3e 100644 --- a/diagnostics/diagnostics.go +++ b/diagnostics/diagnostics.go @@ -39,6 +39,7 @@ import ( "fmt" "log" "net/http" + "strconv" "strings" "sync" "time" @@ -99,8 +100,8 @@ func emit(final bool) error { if i > 0 && err != nil { // don't hammer the server; first failure might have been // a fluke, but back off more after that - log.Printf("[WARNING] Sending diagnostics (attempt %d): %v - waiting and retrying", i, err) - time.Sleep(time.Duration(i*i*i) * time.Second) + log.Printf("[WARNING] Sending diagnostics (attempt %d): %v - backing off and retrying", i, err) + time.Sleep(time.Duration((i+1)*(i+1)*(i+1)) * time.Second) } // send it @@ -113,7 +114,7 @@ func emit(final bool) error { // ensure we can read the response if ct := resp.Header.Get("Content-Type"); (resp.StatusCode < 300 || resp.StatusCode >= 400) && !strings.Contains(ct, "json") { - err = fmt.Errorf("diagnostics server replied with unknown content-type: %s", ct) + err = fmt.Errorf("diagnostics server replied with unknown content-type: '%s' and HTTP %s", ct, resp.Status) resp.Body.Close() continue } @@ -129,6 +130,12 @@ func emit(final bool) error { // just wait and try again -- this is a special case of // error that we handle differently, as you can see if resp.StatusCode == http.StatusTooManyRequests { + if reply.NextUpdate <= 0 { + raStr := resp.Header.Get("Retry-After") + if ra, err := strconv.Atoi(raStr); err == nil { + reply.NextUpdate = time.Duration(ra) * time.Second + } + } log.Printf("[NOTICE] Sending diagnostics: we were too early; waiting %s before trying again", reply.NextUpdate) time.Sleep(reply.NextUpdate) continue @@ -141,11 +148,11 @@ func emit(final bool) error { } if err == nil { // (remember, if there was an error, we return it - // below, so it will get logged if it's supposed to) + // below, so it WILL get logged if it's supposed to) log.Println("[INFO] Sending diagnostics: success") } - // even if there was an error after retrying, we should + // even if there was an error after all retries, we should // schedule the next update using our default update // interval because the server might be healthy later @@ -283,10 +290,11 @@ const ( endpoint = "https://diagnostics-staging.caddyserver.com/update/" // TODO: make configurable, "http://localhost:8085/update/" // defaultUpdateInterval is how long to wait before emitting - // more diagnostic data. This value is only used if the - // client receives a nonsensical value, or doesn't send one - // at all, indicating a likely problem with the server. Thus, - // this value should be a long duration to help alleviate - // extra load on the server. + // more diagnostic data if all retires fail. This value is + // only used if the client receives a nonsensical value, or + // doesn't send one at all, or if a connection can't be made, + // likely indicating a problem with the server. Thus, this + // value should be a long duration to help alleviate extra + // load on the server. defaultUpdateInterval = 1 * time.Hour ) From 4df8028bc3f49761308cf641ce0f9d15222723a5 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 21 Mar 2018 17:01:14 -0600 Subject: [PATCH 10/28] diagnostics: Add/remove metrics --- caddy.go | 5 +-- caddyhttp/httpserver/mitm.go | 9 ++++-- caddyhttp/httpserver/plugin.go | 42 +++++++++++++++++++++++-- caddyhttp/httpserver/server.go | 4 ++- caddytls/certificates.go | 4 +++ caddytls/client.go | 14 +++++++-- caddytls/handshake.go | 41 ++++++++++++++----------- caddytls/setup.go | 6 ++++ diagnostics/collection.go | 42 +++++++++++++------------ diagnostics/diagnostics.go | 56 ++++++++++++++++++++++++++-------- sigtrap.go | 5 +++ sigtrap_posix.go | 9 ++++++ 12 files changed, 178 insertions(+), 59 deletions(-) diff --git a/caddy.go b/caddy.go index b47376a0d..41b716485 100644 --- a/caddy.go +++ b/caddy.go @@ -123,6 +123,7 @@ type Instance struct { StorageMu sync.RWMutex } +// Instances returns the list of instances. func Instances() []*Instance { return instances } @@ -616,7 +617,7 @@ func ValidateAndExecuteDirectives(cdyfile Input, inst *Instance, justValidate bo return fmt.Errorf("error inspecting server blocks: %v", err) } - diagnostics.Set("num_server_blocks", len(sblocks)) + diagnostics.Set("http_num_server_blocks", len(sblocks)) return executeDirectives(inst, cdyfile.Path(), stype.Directives(), sblocks, justValidate) } @@ -872,7 +873,7 @@ func Stop() error { // explicitly like a common local hostname. addr must only // be a host or a host:port combination. func IsLoopback(addr string) bool { - host, _, err := net.SplitHostPort(addr) + host, _, err := net.SplitHostPort(strings.ToLower(addr)) if err != nil { host = addr // happens if the addr is just a hostname } diff --git a/caddyhttp/httpserver/mitm.go b/caddyhttp/httpserver/mitm.go index 1c1d57110..22d4610a0 100644 --- a/caddyhttp/httpserver/mitm.go +++ b/caddyhttp/httpserver/mitm.go @@ -51,6 +51,9 @@ type tlsHandler struct { // Halderman, et. al. in "The Security Impact of HTTPS Interception" (NDSS '17): // https://jhalderm.com/pub/papers/interception-ndss17.pdf func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // TODO: one request per connection, we should report UA in connection with + // handshake (reported in caddytls package) and our MITM assessment + if h.listener == nil { h.next.ServeHTTP(w, r) return @@ -100,12 +103,12 @@ func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if checked { r = r.WithContext(context.WithValue(r.Context(), MitmCtxKey, mitm)) if mitm { - go diagnostics.AppendUnique("mitm", "likely") + go diagnostics.AppendUnique("http_mitm", "likely") } else { - go diagnostics.AppendUnique("mitm", "unlikely") + go diagnostics.AppendUnique("http_mitm", "unlikely") } } else { - go diagnostics.AppendUnique("mitm", "unknown") + go diagnostics.AppendUnique("http_mitm", "unknown") } if mitm && h.closeOnMITM { diff --git a/caddyhttp/httpserver/plugin.go b/caddyhttp/httpserver/plugin.go index 93811abcb..4f04dd652 100644 --- a/caddyhttp/httpserver/plugin.go +++ b/caddyhttp/httpserver/plugin.go @@ -29,6 +29,7 @@ import ( "github.com/mholt/caddy/caddyfile" "github.com/mholt/caddy/caddyhttp/staticfiles" "github.com/mholt/caddy/caddytls" + "github.com/mholt/caddy/diagnostics" ) const serverType = "http" @@ -205,9 +206,34 @@ func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []cadd // MakeServers uses the newly-created siteConfigs to // create and return a list of server instances. func (h *httpContext) MakeServers() ([]caddy.Server, error) { - // make sure TLS is disabled for explicitly-HTTP sites - // (necessary when HTTP address shares a block containing tls) + // make a rough estimate as to whether we're in a "production + // environment/system" - start by assuming that most production + // servers will set their default CA endpoint to a public, + // trusted CA (obviously not a perfect hueristic) + var looksLikeProductionCA bool + for _, publicCAEndpoint := range caddytls.KnownACMECAs { + if strings.Contains(caddytls.DefaultCAUrl, publicCAEndpoint) { + looksLikeProductionCA = true + break + } + } + + var atLeastOneSiteLooksLikeProduction bool for _, cfg := range h.siteConfigs { + // if we aren't sure yet whether it's a "production" server, + // continue to see if all the addresses (both sites and + // listeners) are loopback + if !atLeastOneSiteLooksLikeProduction { + if !caddy.IsLoopback(cfg.Addr.Host) && + !caddy.IsLoopback(cfg.ListenHost) && + (caddytls.QualifiesForManagedTLS(cfg) || + caddytls.HostQualifies(cfg.Addr.Host)) { + atLeastOneSiteLooksLikeProduction = true + } + } + + // make sure TLS is disabled for explicitly-HTTP sites + // (necessary when HTTP address shares a block containing tls) if !cfg.TLS.Enabled { continue } @@ -246,6 +272,18 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) { servers = append(servers, s) } + // NOTE: This value is only a "good" guess. Quite often, development + // environments will use internal DNS or a local hosts file to serve + // real-looking domains in local development. We can't easily tell + // which without doing a DNS lookup, so this guess is definitely naive, + // and if we ever want a better guess, we will have to do DNS lookups. + deploymentGuess := "dev" + if looksLikeProductionCA && atLeastOneSiteLooksLikeProduction { + deploymentGuess = "production" + } + diagnostics.Set("http_deployment_guess", deploymentGuess) + diagnostics.Set("http_num_sites", len(h.siteConfigs)) + return servers, nil } diff --git a/caddyhttp/httpserver/server.go b/caddyhttp/httpserver/server.go index 6f5d84595..4f5a461eb 100644 --- a/caddyhttp/httpserver/server.go +++ b/caddyhttp/httpserver/server.go @@ -346,7 +346,9 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } }() - go diagnostics.AppendUnique("user_agent", r.Header.Get("User-Agent")) + // TODO: Somehow report UA string in conjunction with TLS handshake, if any (and just once per connection) + go diagnostics.AppendUnique("http_user_agent", r.Header.Get("User-Agent")) + go diagnostics.Increment("http_request_count") // copy the original, unchanged URL into the context // so it can be referenced by middlewares diff --git a/caddytls/certificates.go b/caddytls/certificates.go index 29c0c8c21..c78dbde4c 100644 --- a/caddytls/certificates.go +++ b/caddytls/certificates.go @@ -26,6 +26,7 @@ import ( "sync" "time" + "github.com/mholt/caddy/diagnostics" "golang.org/x/crypto/ocsp" ) @@ -165,6 +166,7 @@ func (cfg *Config) CacheManagedCertificate(domain string) (Certificate, error) { if err != nil { return cert, err } + diagnostics.Increment("tls_managed_cert_count") return cfg.cacheCertificate(cert), nil } @@ -179,6 +181,7 @@ func (cfg *Config) cacheUnmanagedCertificatePEMFile(certFile, keyFile string) er return err } cfg.cacheCertificate(cert) + diagnostics.Increment("tls_manual_cert_count") return nil } @@ -192,6 +195,7 @@ func (cfg *Config) cacheUnmanagedCertificatePEMBytes(certBytes, keyBytes []byte) return err } cfg.cacheCertificate(cert) + diagnostics.Increment("tls_manual_cert_count") return nil } diff --git a/caddytls/client.go b/caddytls/client.go index aac70006f..7aca428eb 100644 --- a/caddytls/client.go +++ b/caddytls/client.go @@ -268,7 +268,7 @@ Attempts: break } - go diagnostics.Increment("acme_certificates_obtained") + go diagnostics.Increment("tls_acme_certs_obtained") return nil } @@ -340,8 +340,7 @@ func (c *ACMEClient) Renew(name string) error { } caddy.EmitEvent(caddy.CertRenewEvent, name) - go diagnostics.Increment("acme_certificates_obtained") - go diagnostics.Increment("acme_certificates_renewed") + go diagnostics.Increment("tls_acme_certs_renewed") return saveCertResource(c.storage, newCertMeta) } @@ -368,6 +367,8 @@ func (c *ACMEClient) Revoke(name string) error { return err } + go diagnostics.Increment("tls_acme_certs_revoked") + err = c.storage.DeleteSite(name) if err != nil { return errors.New("certificate revoked, but unable to delete certificate file: " + err.Error()) @@ -419,3 +420,10 @@ func (c *nameCoordinator) Has(name string) bool { c.mu.RUnlock() return ok } + +// KnownACMECAs is a list of ACME directory endpoints of +// known, public, and trusted ACME-compatible certificate +// authorities. +var KnownACMECAs = []string{ + "https://acme-v02.api.letsencrypt.org/directory", +} diff --git a/caddytls/handshake.go b/caddytls/handshake.go index e81f05262..25133b2a3 100644 --- a/caddytls/handshake.go +++ b/caddytls/handshake.go @@ -100,24 +100,31 @@ func (cg configGroup) GetConfigForClient(clientHello *tls.ClientHelloInfo) (*tls // // This method is safe for use as a tls.Config.GetCertificate callback. func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { - go diagnostics.Append("client_hello", struct { - NoSNI bool `json:"no_sni,omitempty"` - CipherSuites []uint16 `json:"cipher_suites,omitempty"` - SupportedCurves []tls.CurveID `json:"curves,omitempty"` - SupportedPoints []uint8 `json:"points,omitempty"` - SignatureSchemes []tls.SignatureScheme `json:"sig_scheme,omitempty"` - ALPN []string `json:"alpn,omitempty"` - SupportedVersions []uint16 `json:"versions,omitempty"` - }{ - NoSNI: clientHello.ServerName == "", - CipherSuites: clientHello.CipherSuites, - SupportedCurves: clientHello.SupportedCurves, - SupportedPoints: clientHello.SupportedPoints, - SignatureSchemes: clientHello.SignatureSchemes, - ALPN: clientHello.SupportedProtos, - SupportedVersions: clientHello.SupportedVersions, - }) + // TODO: We need to collect this in a heavily de-duplicating way + // It would also be nice to associate a handshake with the UA string (but that is only for HTTP server type) + // go diagnostics.Append("tls_client_hello", struct { + // NoSNI bool `json:"no_sni,omitempty"` + // CipherSuites []uint16 `json:"cipher_suites,omitempty"` + // SupportedCurves []tls.CurveID `json:"curves,omitempty"` + // SupportedPoints []uint8 `json:"points,omitempty"` + // SignatureSchemes []tls.SignatureScheme `json:"sig_scheme,omitempty"` + // ALPN []string `json:"alpn,omitempty"` + // SupportedVersions []uint16 `json:"versions,omitempty"` + // }{ + // NoSNI: clientHello.ServerName == "", + // CipherSuites: clientHello.CipherSuites, + // SupportedCurves: clientHello.SupportedCurves, + // SupportedPoints: clientHello.SupportedPoints, + // SignatureSchemes: clientHello.SignatureSchemes, + // ALPN: clientHello.SupportedProtos, + // SupportedVersions: clientHello.SupportedVersions, + // }) cert, err := cfg.getCertDuringHandshake(strings.ToLower(clientHello.ServerName), true, true) + if err == nil { + go diagnostics.Increment("tls_handshake_count") + } else { + go diagnostics.Append("tls_handshake_error", err.Error()) + } return &cert.Certificate, err } diff --git a/caddytls/setup.go b/caddytls/setup.go index 63c2a9e6d..709837052 100644 --- a/caddytls/setup.go +++ b/caddytls/setup.go @@ -28,6 +28,7 @@ import ( "strings" "github.com/mholt/caddy" + "github.com/mholt/caddy/diagnostics" ) func init() { @@ -174,9 +175,11 @@ func setupTLS(c *caddy.Controller) error { case "max_certs": c.Args(&maxCerts) config.OnDemand = true + diagnostics.Increment("tls_on_demand_count") case "ask": c.Args(&askURL) config.OnDemand = true + diagnostics.Increment("tls_on_demand_count") case "dns": args := c.RemainingArgs() if len(args) != 1 { @@ -251,6 +254,7 @@ func setupTLS(c *caddy.Controller) error { return c.Errf("Unable to load certificate and key files for '%s': %v", c.Key, err) } log.Printf("[INFO] Successfully loaded TLS assets from %s and %s", certificateFile, keyFile) + diagnostics.Increment("tls_manual_cert_count") } // load a directory of certificates, if specified @@ -270,6 +274,7 @@ func setupTLS(c *caddy.Controller) error { if err != nil { return fmt.Errorf("self-signed: %v", err) } + diagnostics.Increment("tls_self_signed_count") } return nil @@ -350,6 +355,7 @@ func loadCertsInDir(cfg *Config, c *caddy.Controller, dir string) error { return c.Errf("%s: failed to load cert and key for '%s': %v", path, c.Key, err) } log.Printf("[INFO] Successfully loaded TLS assets from %s", path) + diagnostics.Increment("tls_manual_cert_count") } return nil }) diff --git a/diagnostics/collection.go b/diagnostics/collection.go index 1849aee7b..e2b222b38 100644 --- a/diagnostics/collection.go +++ b/diagnostics/collection.go @@ -33,7 +33,7 @@ func Init(instanceID uuid.UUID) { panic("already initialized") } if str := instanceID.String(); str == "" || - instanceID.String() == "00000000-0000-0000-0000-000000000000" { + str == "00000000-0000-0000-0000-000000000000" { panic("empty UUID") } instanceUUID = instanceID @@ -73,6 +73,10 @@ func StartEmitting() { // // It is a no-op if the package was never initialized // or if emitting was never started. +// +// NOTE: This function is blocking. Run in a goroutine if +// you want to guarantee no blocking at critical times +// like exiting the program. func StopEmitting() { if !enabled { return @@ -83,7 +87,12 @@ func StopEmitting() { return } updateTimerMu.Unlock() - logEmit(true) + logEmit(true) // likely too early; may take minutes to return +} + +// Reset empties the current payload buffer. +func Reset() { + resetBuffer() } // Set puts a value in the buffer to be included @@ -142,7 +151,7 @@ func Append(key string, value interface{}) { bufferMu.Unlock() } -// AppendUnique adds value to a set namedkey. +// AppendUnique adds value to a set named key. // Set items are unordered. Values in the set // are unique, but how many times they are // appended is counted. @@ -178,24 +187,23 @@ func AppendUnique(key string, value interface{}) { bufferMu.Unlock() } -// Increment adds 1 to a value named key. +// Add adds amount to a value named key. // If it does not exist, it is created with // a value of 1. If key maps to a type that // is not an integer, a panic is logged, // and this is a no-op. +func Add(key string, amount int) { + atomicAdd(key, amount) +} + +// Increment is a shortcut for Add(key, 1) func Increment(key string) { - incrementOrDecrement(key, true) + atomicAdd(key, 1) } -// Decrement is the same as increment except -// it subtracts 1. -func Decrement(key string) { - incrementOrDecrement(key, false) -} - -// inc == true: increment -// inc == false: decrement -func incrementOrDecrement(key string, inc bool) { +// atomicAdd adds amount (negative to subtract) +// to key. +func atomicAdd(key string, amount int) { if !enabled { return } @@ -214,10 +222,6 @@ func incrementOrDecrement(key string, inc bool) { } bufferItemCount++ } - if inc { - buffer[key] = intVal + 1 - } else { - buffer[key] = intVal - 1 - } + buffer[key] = intVal + amount bufferMu.Unlock() } diff --git a/diagnostics/diagnostics.go b/diagnostics/diagnostics.go index 0743ade3e..2c1396605 100644 --- a/diagnostics/diagnostics.go +++ b/diagnostics/diagnostics.go @@ -48,14 +48,16 @@ import ( ) // logEmit calls emit and then logs the error, if any. +// See docs for emit. func logEmit(final bool) { err := emit(final) if err != nil { - log.Printf("[ERROR] Sending diganostics: %v", err) + log.Printf("[ERROR] Sending diagnostics: %v", err) } } // emit sends an update to the diagnostics server. +// Set final to true if this is the last call to emit. // If final is true, no future updates will be scheduled. // Otherwise, the next update will be scheduled. func emit(final bool) error { @@ -136,9 +138,11 @@ func emit(final bool) error { reply.NextUpdate = time.Duration(ra) * time.Second } } - log.Printf("[NOTICE] Sending diagnostics: we were too early; waiting %s before trying again", reply.NextUpdate) - time.Sleep(reply.NextUpdate) - continue + if !final { + log.Printf("[NOTICE] Sending diagnostics: we were too early; waiting %s before trying again", reply.NextUpdate) + time.Sleep(reply.NextUpdate) + continue + } } else if resp.StatusCode >= 400 { err = fmt.Errorf("diagnostics server returned status code %d", resp.StatusCode) continue @@ -146,7 +150,7 @@ func emit(final bool) error { break } - if err == nil { + if err == nil && !final { // (remember, if there was an error, we return it // below, so it WILL get logged if it's supposed to) log.Println("[INFO] Sending diagnostics: success") @@ -181,13 +185,7 @@ func emit(final bool) error { // resulting byte slice is lost, the payload is // gone with it. func makePayloadAndResetBuffer() ([]byte, error) { - // make a local pointer to the buffer, then reset - // the buffer to an empty map to clear it out - bufferMu.Lock() - bufCopy := buffer - buffer = make(map[string]interface{}) - bufferItemCount = 0 - bufferMu.Unlock() + bufCopy := resetBuffer() // encode payload in preparation for transmission payload := Payload{ @@ -198,6 +196,21 @@ func makePayloadAndResetBuffer() ([]byte, error) { return json.Marshal(payload) } +// resetBuffer makes a local pointer to the buffer, +// then resets the buffer by assigning to be a newly- +// made value to clear it out, then sets the buffer +// item count to 0. It returns the copied pointer to +// the original map so the old buffer value can be +// used locally. +func resetBuffer() map[string]interface{} { + bufferMu.Lock() + bufCopy := buffer + buffer = make(map[string]interface{}) + bufferItemCount = 0 + bufferMu.Unlock() + return bufCopy +} + // Response contains the body of a response from the // diagnostics server. type Response struct { @@ -222,10 +235,28 @@ type Payload struct { // The UTC timestamp of the transmission Timestamp time.Time `json:"timestamp"` + // The timestamp before which the next update is expected + // (NOT populated by client - the server fills this in + // before it stores the data) + ExpectNext time.Time `json:"expect_next,omitempty"` + // The metrics Data map[string]interface{} `json:"data,omitempty"` } +// Int returns the value of the data keyed by key +// if it is an integer; otherwise it returns 0. +func (p Payload) Int(key string) int { + val, _ := p.Data[key] + switch p.Data[key].(type) { + case int: + return val.(int) + case float64: // after JSON-decoding, int becomes float64... + return int(val.(float64)) + } + return 0 +} + // countingSet implements a set that counts how many // times a key is inserted. It marshals to JSON in a // way such that keys are converted to values next @@ -272,6 +303,7 @@ var ( // instanceUUID is the ID of the current instance. // This MUST be set to emit diagnostics. + // This MUST NOT be openly exposed to clients, for privacy. instanceUUID uuid.UUID // enabled indicates whether the package has diff --git a/sigtrap.go b/sigtrap.go index ac61c59c0..feae2b194 100644 --- a/sigtrap.go +++ b/sigtrap.go @@ -19,6 +19,8 @@ import ( "os" "os/signal" "sync" + + "github.com/mholt/caddy/diagnostics" ) // TrapSignals create signal handlers for all applicable signals for this @@ -52,6 +54,9 @@ func trapSignalsCrossPlatform() { log.Println("[INFO] SIGINT: Shutting down") + diagnostics.AppendUnique("sigtrap", "SIGINT") + go diagnostics.StopEmitting() // not guaranteed to finish in time; that's OK (just don't block!) + // important cleanup actions before shutdown callbacks for _, f := range OnProcessExit { f() diff --git a/sigtrap_posix.go b/sigtrap_posix.go index cc65ccb46..0a70abe1c 100644 --- a/sigtrap_posix.go +++ b/sigtrap_posix.go @@ -21,6 +21,8 @@ import ( "os" "os/signal" "syscall" + + "github.com/mholt/caddy/diagnostics" ) // trapSignalsPosix captures POSIX-only signals. @@ -49,10 +51,15 @@ func trapSignalsPosix() { log.Printf("[ERROR] SIGTERM stop: %v", err) exitCode = 3 } + + diagnostics.AppendUnique("sigtrap", "SIGTERM") + go diagnostics.StopEmitting() // won't finish in time, but that's OK - just don't block + os.Exit(exitCode) case syscall.SIGUSR1: log.Println("[INFO] SIGUSR1: Reloading") + go diagnostics.AppendUnique("sigtrap", "SIGUSR1") // Start with the existing Caddyfile caddyfileToUse, inst, err := getCurrentCaddyfile() @@ -84,12 +91,14 @@ func trapSignalsPosix() { case syscall.SIGUSR2: log.Println("[INFO] SIGUSR2: Upgrading") + go diagnostics.AppendUnique("sigtrap", "SIGUSR2") if err := Upgrade(); err != nil { log.Printf("[ERROR] SIGUSR2: upgrading: %v", err) } case syscall.SIGHUP: // ignore; this signal is sometimes sent outside of the user's control + go diagnostics.AppendUnique("sigtrap", "SIGHUP") } } }() From 7c868afd32b6ebf130c0f8974779e69a0d1a531e Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 21 Mar 2018 17:51:07 -0600 Subject: [PATCH 11/28] diagnostics: Specially handle HTTP 410 and 451 codes An attempt to future-proof older Caddy instances so that they won't keep trying to send telemetry to endpoints that just simply aren't going to be available --- diagnostics/diagnostics.go | 45 +++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/diagnostics/diagnostics.go b/diagnostics/diagnostics.go index 2c1396605..869f731dd 100644 --- a/diagnostics/diagnostics.go +++ b/diagnostics/diagnostics.go @@ -37,6 +37,7 @@ import ( "bytes" "encoding/json" "fmt" + "io/ioutil" "log" "net/http" "strconv" @@ -83,10 +84,7 @@ func emit(final bool) error { // terminate any pending update if this is the last one if final { - updateTimerMu.Lock() - updateTimer.Stop() - updateTimer = nil - updateTimerMu.Unlock() + stopUpdateTimer() } payloadBytes, err := makePayloadAndResetBuffer() @@ -113,7 +111,37 @@ func emit(final bool) error { continue } - // ensure we can read the response + // check for any special-case response codes + if resp.StatusCode == http.StatusGone { + // the endpoint has been deprecated and is no longer servicing clients + err = fmt.Errorf("diagnostics server replied with HTTP %d; upgrade required", resp.StatusCode) + if clen := resp.Header.Get("Content-Length"); clen != "0" && clen != "" { + bodyBytes, readErr := ioutil.ReadAll(resp.Body) + if readErr != nil { + log.Printf("[ERROR] Reading response body from server: %v", readErr) + } + err = fmt.Errorf("%v - %s", err, bodyBytes) + } + resp.Body.Close() + reply.Stop = true + break + } + if resp.StatusCode == http.StatusUnavailableForLegalReasons { + // the endpoint is unavailable, at least to this client, for legal reasons (!) + err = fmt.Errorf("diagnostics server replied with HTTP %d %s: please consult the project website and developers for guidance", resp.StatusCode, resp.Status) + if clen := resp.Header.Get("Content-Length"); clen != "0" && clen != "" { + bodyBytes, readErr := ioutil.ReadAll(resp.Body) + if readErr != nil { + log.Printf("[ERROR] Reading response body from server: %v", readErr) + } + err = fmt.Errorf("%v - %s", err, bodyBytes) + } + resp.Body.Close() + reply.Stop = true + break + } + + // okay, ensure we can interpret the response if ct := resp.Header.Get("Content-Type"); (resp.StatusCode < 300 || resp.StatusCode >= 400) && !strings.Contains(ct, "json") { err = fmt.Errorf("diagnostics server replied with unknown content-type: '%s' and HTTP %s", ct, resp.Status) @@ -178,6 +206,13 @@ func emit(final bool) error { return err } +func stopUpdateTimer() { + updateTimerMu.Lock() + updateTimer.Stop() + updateTimer = nil + updateTimerMu.Unlock() +} + // makePayloadAndResetBuffer prepares a payload // by emptying the collection buffer. It returns // the bytes of the payload to send to the server. From 52316952a575b01871224e68d4d248c0e2cdf271 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 22 Mar 2018 18:05:31 -0600 Subject: [PATCH 12/28] Refactor diagnostics -> telemetry --- caddy.go | 4 +- caddy/caddymain/run.go | 34 ++++++------ caddyfile/parse.go | 4 +- caddyhttp/httpserver/mitm.go | 8 +-- caddyhttp/httpserver/plugin.go | 16 +++--- caddyhttp/httpserver/server.go | 6 +- caddytls/certificates.go | 8 +-- caddytls/client.go | 8 +-- caddytls/handshake.go | 8 +-- caddytls/setup.go | 12 ++-- sigtrap.go | 6 +- sigtrap_posix.go | 12 ++-- {diagnostics => telemetry}/collection.go | 8 +-- {diagnostics => telemetry}/collection_test.go | 2 +- .../diagnostics.go => telemetry/telemetry.go | 55 +++++++++---------- .../telemetry_test.go | 2 +- 16 files changed, 96 insertions(+), 97 deletions(-) rename {diagnostics => telemetry}/collection.go (95%) rename {diagnostics => telemetry}/collection_test.go (99%) rename diagnostics/diagnostics.go => telemetry/telemetry.go (83%) rename diagnostics/diagnostics_test.go => telemetry/telemetry_test.go (98%) diff --git a/caddy.go b/caddy.go index 41b716485..851e5315f 100644 --- a/caddy.go +++ b/caddy.go @@ -44,7 +44,7 @@ import ( "time" "github.com/mholt/caddy/caddyfile" - "github.com/mholt/caddy/diagnostics" + "github.com/mholt/caddy/telemetry" ) // Configurable application parameters @@ -617,7 +617,7 @@ func ValidateAndExecuteDirectives(cdyfile Input, inst *Instance, justValidate bo return fmt.Errorf("error inspecting server blocks: %v", err) } - diagnostics.Set("http_num_server_blocks", len(sblocks)) + telemetry.Set("http_num_server_blocks", len(sblocks)) return executeDirectives(inst, cdyfile.Path(), stype.Directives(), sblocks, justValidate) } diff --git a/caddy/caddymain/run.go b/caddy/caddymain/run.go index 9f26e02fb..cd1b47409 100644 --- a/caddy/caddymain/run.go +++ b/caddy/caddymain/run.go @@ -30,7 +30,7 @@ import ( "github.com/klauspost/cpuid" "github.com/mholt/caddy" "github.com/mholt/caddy/caddytls" - "github.com/mholt/caddy/diagnostics" + "github.com/mholt/caddy/telemetry" "github.com/xenolf/lego/acme" "gopkg.in/natefinch/lumberjack.v2" @@ -52,7 +52,6 @@ func init() { flag.StringVar(&caddytls.DefaultEmail, "email", "", "Default ACME CA account email address") flag.DurationVar(&acme.HTTPClient.Timeout, "catimeout", acme.HTTPClient.Timeout, "Default ACME CA HTTP timeout") flag.StringVar(&logfile, "log", "", "Process log file") - flag.BoolVar(&noDiag, "no-diagnostics", false, "Disable diagnostic reporting") flag.StringVar(&caddy.PidFile, "pidfile", "", "Path to write pid file") flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)") flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate") @@ -89,9 +88,9 @@ func Run() { }) } - // initialize diagnostics client - if !noDiag { - initDiagnostics() + // initialize telemetry client + if enableTelemetry { + initTelemetry() } // Check for one-time actions @@ -150,13 +149,13 @@ func Run() { // Execute instantiation events caddy.EmitEvent(caddy.InstanceStartupEvent, instance) - // Begin diagnostics (these are no-ops if diagnostics disabled) - diagnostics.Set("caddy_version", appVersion) - diagnostics.Set("num_listeners", len(instance.Servers())) - diagnostics.Set("server_type", serverType) - diagnostics.Set("os", runtime.GOOS) - diagnostics.Set("arch", runtime.GOARCH) - diagnostics.Set("cpu", struct { + // Begin telemetry (these are no-ops if telemetry disabled) + telemetry.Set("caddy_version", appVersion) + telemetry.Set("num_listeners", len(instance.Servers())) + telemetry.Set("server_type", serverType) + telemetry.Set("os", runtime.GOOS) + telemetry.Set("arch", runtime.GOARCH) + telemetry.Set("cpu", struct { BrandName string `json:"brand_name,omitempty"` NumLogical int `json:"num_logical,omitempty"` AESNI bool `json:"aes_ni,omitempty"` @@ -165,7 +164,7 @@ func Run() { NumLogical: runtime.NumCPU(), AESNI: cpuid.CPU.AesNi(), }) - diagnostics.StartEmitting() + telemetry.StartEmitting() // Twiddle your thumbs instance.Wait() @@ -290,8 +289,8 @@ func setCPU(cpu string) error { return nil } -// initDiagnostics initializes the diagnostics engine. -func initDiagnostics() { +// initTelemetry initializes the telemetry engine. +func initTelemetry() { uuidFilename := filepath.Join(caddy.AssetsPath(), "uuid") newUUID := func() uuid.UUID { @@ -327,7 +326,7 @@ func initDiagnostics() { } } - diagnostics.Init(id) + telemetry.Init(id) } const appName = "Caddy" @@ -342,7 +341,6 @@ var ( version bool plugins bool validate bool - noDiag bool ) // Build information obtained with the help of -ldflags @@ -356,4 +354,6 @@ var ( gitCommit string // git rev-parse HEAD gitShortStat string // git diff-index --shortstat gitFilesModified string // git diff-index --name-only HEAD + + enableTelemetry = true ) diff --git a/caddyfile/parse.go b/caddyfile/parse.go index 9851e1c52..41eefb4c0 100644 --- a/caddyfile/parse.go +++ b/caddyfile/parse.go @@ -21,7 +21,7 @@ import ( "path/filepath" "strings" - "github.com/mholt/caddy/diagnostics" + "github.com/mholt/caddy/telemetry" ) // Parse parses the input just enough to group tokens, in @@ -371,7 +371,7 @@ func (p *parser) directive() error { // The directive itself is appended as a relevant token p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor]) - diagnostics.AppendUnique("directives", dir) + telemetry.AppendUnique("directives", dir) for p.Next() { if p.Val() == "{" { diff --git a/caddyhttp/httpserver/mitm.go b/caddyhttp/httpserver/mitm.go index 22d4610a0..8a610358f 100644 --- a/caddyhttp/httpserver/mitm.go +++ b/caddyhttp/httpserver/mitm.go @@ -25,7 +25,7 @@ import ( "strings" "sync" - "github.com/mholt/caddy/diagnostics" + "github.com/mholt/caddy/telemetry" ) // tlsHandler is a http.Handler that will inject a value @@ -103,12 +103,12 @@ func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if checked { r = r.WithContext(context.WithValue(r.Context(), MitmCtxKey, mitm)) if mitm { - go diagnostics.AppendUnique("http_mitm", "likely") + go telemetry.AppendUnique("http_mitm", "likely") } else { - go diagnostics.AppendUnique("http_mitm", "unlikely") + go telemetry.AppendUnique("http_mitm", "unlikely") } } else { - go diagnostics.AppendUnique("http_mitm", "unknown") + go telemetry.AppendUnique("http_mitm", "unknown") } if mitm && h.closeOnMITM { diff --git a/caddyhttp/httpserver/plugin.go b/caddyhttp/httpserver/plugin.go index 4f04dd652..69be4b618 100644 --- a/caddyhttp/httpserver/plugin.go +++ b/caddyhttp/httpserver/plugin.go @@ -29,7 +29,7 @@ import ( "github.com/mholt/caddy/caddyfile" "github.com/mholt/caddy/caddyhttp/staticfiles" "github.com/mholt/caddy/caddytls" - "github.com/mholt/caddy/diagnostics" + "github.com/mholt/caddy/telemetry" ) const serverType = "http" @@ -220,9 +220,9 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) { var atLeastOneSiteLooksLikeProduction bool for _, cfg := range h.siteConfigs { - // if we aren't sure yet whether it's a "production" server, - // continue to see if all the addresses (both sites and - // listeners) are loopback + // see if all the addresses (both sites and + // listeners) are loopback to help us determine + // if this is a "production" instance or not if !atLeastOneSiteLooksLikeProduction { if !caddy.IsLoopback(cfg.Addr.Host) && !caddy.IsLoopback(cfg.ListenHost) && @@ -272,17 +272,17 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) { servers = append(servers, s) } - // NOTE: This value is only a "good" guess. Quite often, development + // NOTE: This value is only a "good guess". Quite often, development // environments will use internal DNS or a local hosts file to serve // real-looking domains in local development. We can't easily tell // which without doing a DNS lookup, so this guess is definitely naive, // and if we ever want a better guess, we will have to do DNS lookups. deploymentGuess := "dev" if looksLikeProductionCA && atLeastOneSiteLooksLikeProduction { - deploymentGuess = "production" + deploymentGuess = "prod" } - diagnostics.Set("http_deployment_guess", deploymentGuess) - diagnostics.Set("http_num_sites", len(h.siteConfigs)) + telemetry.Set("http_deployment_guess", deploymentGuess) + telemetry.Set("http_num_sites", len(h.siteConfigs)) return servers, nil } diff --git a/caddyhttp/httpserver/server.go b/caddyhttp/httpserver/server.go index 4f5a461eb..4c551c7b1 100644 --- a/caddyhttp/httpserver/server.go +++ b/caddyhttp/httpserver/server.go @@ -36,7 +36,7 @@ import ( "github.com/mholt/caddy" "github.com/mholt/caddy/caddyhttp/staticfiles" "github.com/mholt/caddy/caddytls" - "github.com/mholt/caddy/diagnostics" + "github.com/mholt/caddy/telemetry" ) // Server is the HTTP server implementation. @@ -347,8 +347,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { }() // TODO: Somehow report UA string in conjunction with TLS handshake, if any (and just once per connection) - go diagnostics.AppendUnique("http_user_agent", r.Header.Get("User-Agent")) - go diagnostics.Increment("http_request_count") + go telemetry.AppendUnique("http_user_agent", r.Header.Get("User-Agent")) + go telemetry.Increment("http_request_count") // copy the original, unchanged URL into the context // so it can be referenced by middlewares diff --git a/caddytls/certificates.go b/caddytls/certificates.go index c78dbde4c..b021134bb 100644 --- a/caddytls/certificates.go +++ b/caddytls/certificates.go @@ -26,7 +26,7 @@ import ( "sync" "time" - "github.com/mholt/caddy/diagnostics" + "github.com/mholt/caddy/telemetry" "golang.org/x/crypto/ocsp" ) @@ -166,7 +166,7 @@ func (cfg *Config) CacheManagedCertificate(domain string) (Certificate, error) { if err != nil { return cert, err } - diagnostics.Increment("tls_managed_cert_count") + telemetry.Increment("tls_managed_cert_count") return cfg.cacheCertificate(cert), nil } @@ -181,7 +181,7 @@ func (cfg *Config) cacheUnmanagedCertificatePEMFile(certFile, keyFile string) er return err } cfg.cacheCertificate(cert) - diagnostics.Increment("tls_manual_cert_count") + telemetry.Increment("tls_manual_cert_count") return nil } @@ -195,7 +195,7 @@ func (cfg *Config) cacheUnmanagedCertificatePEMBytes(certBytes, keyBytes []byte) return err } cfg.cacheCertificate(cert) - diagnostics.Increment("tls_manual_cert_count") + telemetry.Increment("tls_manual_cert_count") return nil } diff --git a/caddytls/client.go b/caddytls/client.go index 7aca428eb..08b0af38d 100644 --- a/caddytls/client.go +++ b/caddytls/client.go @@ -26,7 +26,7 @@ import ( "time" "github.com/mholt/caddy" - "github.com/mholt/caddy/diagnostics" + "github.com/mholt/caddy/telemetry" "github.com/xenolf/lego/acme" ) @@ -268,7 +268,7 @@ Attempts: break } - go diagnostics.Increment("tls_acme_certs_obtained") + go telemetry.Increment("tls_acme_certs_obtained") return nil } @@ -340,7 +340,7 @@ func (c *ACMEClient) Renew(name string) error { } caddy.EmitEvent(caddy.CertRenewEvent, name) - go diagnostics.Increment("tls_acme_certs_renewed") + go telemetry.Increment("tls_acme_certs_renewed") return saveCertResource(c.storage, newCertMeta) } @@ -367,7 +367,7 @@ func (c *ACMEClient) Revoke(name string) error { return err } - go diagnostics.Increment("tls_acme_certs_revoked") + go telemetry.Increment("tls_acme_certs_revoked") err = c.storage.DeleteSite(name) if err != nil { diff --git a/caddytls/handshake.go b/caddytls/handshake.go index 25133b2a3..2d49d9787 100644 --- a/caddytls/handshake.go +++ b/caddytls/handshake.go @@ -26,7 +26,7 @@ import ( "sync/atomic" "time" - "github.com/mholt/caddy/diagnostics" + "github.com/mholt/caddy/telemetry" ) // configGroup is a type that keys configs by their hostname @@ -102,7 +102,7 @@ func (cg configGroup) GetConfigForClient(clientHello *tls.ClientHelloInfo) (*tls func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { // TODO: We need to collect this in a heavily de-duplicating way // It would also be nice to associate a handshake with the UA string (but that is only for HTTP server type) - // go diagnostics.Append("tls_client_hello", struct { + // go telemetry.Append("tls_client_hello", struct { // NoSNI bool `json:"no_sni,omitempty"` // CipherSuites []uint16 `json:"cipher_suites,omitempty"` // SupportedCurves []tls.CurveID `json:"curves,omitempty"` @@ -121,9 +121,9 @@ func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certif // }) cert, err := cfg.getCertDuringHandshake(strings.ToLower(clientHello.ServerName), true, true) if err == nil { - go diagnostics.Increment("tls_handshake_count") + go telemetry.Increment("tls_handshake_count") } else { - go diagnostics.Append("tls_handshake_error", err.Error()) + go telemetry.Append("tls_handshake_error", err.Error()) } return &cert.Certificate, err } diff --git a/caddytls/setup.go b/caddytls/setup.go index 709837052..8100088b3 100644 --- a/caddytls/setup.go +++ b/caddytls/setup.go @@ -28,7 +28,7 @@ import ( "strings" "github.com/mholt/caddy" - "github.com/mholt/caddy/diagnostics" + "github.com/mholt/caddy/telemetry" ) func init() { @@ -175,11 +175,11 @@ func setupTLS(c *caddy.Controller) error { case "max_certs": c.Args(&maxCerts) config.OnDemand = true - diagnostics.Increment("tls_on_demand_count") + telemetry.Increment("tls_on_demand_count") case "ask": c.Args(&askURL) config.OnDemand = true - diagnostics.Increment("tls_on_demand_count") + telemetry.Increment("tls_on_demand_count") case "dns": args := c.RemainingArgs() if len(args) != 1 { @@ -254,7 +254,7 @@ func setupTLS(c *caddy.Controller) error { return c.Errf("Unable to load certificate and key files for '%s': %v", c.Key, err) } log.Printf("[INFO] Successfully loaded TLS assets from %s and %s", certificateFile, keyFile) - diagnostics.Increment("tls_manual_cert_count") + telemetry.Increment("tls_manual_cert_count") } // load a directory of certificates, if specified @@ -274,7 +274,7 @@ func setupTLS(c *caddy.Controller) error { if err != nil { return fmt.Errorf("self-signed: %v", err) } - diagnostics.Increment("tls_self_signed_count") + telemetry.Increment("tls_self_signed_count") } return nil @@ -355,7 +355,7 @@ func loadCertsInDir(cfg *Config, c *caddy.Controller, dir string) error { return c.Errf("%s: failed to load cert and key for '%s': %v", path, c.Key, err) } log.Printf("[INFO] Successfully loaded TLS assets from %s", path) - diagnostics.Increment("tls_manual_cert_count") + telemetry.Increment("tls_manual_cert_count") } return nil }) diff --git a/sigtrap.go b/sigtrap.go index feae2b194..dbb01b7ec 100644 --- a/sigtrap.go +++ b/sigtrap.go @@ -20,7 +20,7 @@ import ( "os/signal" "sync" - "github.com/mholt/caddy/diagnostics" + "github.com/mholt/caddy/telemetry" ) // TrapSignals create signal handlers for all applicable signals for this @@ -54,8 +54,8 @@ func trapSignalsCrossPlatform() { log.Println("[INFO] SIGINT: Shutting down") - diagnostics.AppendUnique("sigtrap", "SIGINT") - go diagnostics.StopEmitting() // not guaranteed to finish in time; that's OK (just don't block!) + telemetry.AppendUnique("sigtrap", "SIGINT") + go telemetry.StopEmitting() // not guaranteed to finish in time; that's OK (just don't block!) // important cleanup actions before shutdown callbacks for _, f := range OnProcessExit { diff --git a/sigtrap_posix.go b/sigtrap_posix.go index 0a70abe1c..3a53b5b56 100644 --- a/sigtrap_posix.go +++ b/sigtrap_posix.go @@ -22,7 +22,7 @@ import ( "os/signal" "syscall" - "github.com/mholt/caddy/diagnostics" + "github.com/mholt/caddy/telemetry" ) // trapSignalsPosix captures POSIX-only signals. @@ -52,14 +52,14 @@ func trapSignalsPosix() { exitCode = 3 } - diagnostics.AppendUnique("sigtrap", "SIGTERM") - go diagnostics.StopEmitting() // won't finish in time, but that's OK - just don't block + telemetry.AppendUnique("sigtrap", "SIGTERM") + go telemetry.StopEmitting() // won't finish in time, but that's OK - just don't block os.Exit(exitCode) case syscall.SIGUSR1: log.Println("[INFO] SIGUSR1: Reloading") - go diagnostics.AppendUnique("sigtrap", "SIGUSR1") + go telemetry.AppendUnique("sigtrap", "SIGUSR1") // Start with the existing Caddyfile caddyfileToUse, inst, err := getCurrentCaddyfile() @@ -91,14 +91,14 @@ func trapSignalsPosix() { case syscall.SIGUSR2: log.Println("[INFO] SIGUSR2: Upgrading") - go diagnostics.AppendUnique("sigtrap", "SIGUSR2") + go telemetry.AppendUnique("sigtrap", "SIGUSR2") if err := Upgrade(); err != nil { log.Printf("[ERROR] SIGUSR2: upgrading: %v", err) } case syscall.SIGHUP: // ignore; this signal is sometimes sent outside of the user's control - go diagnostics.AppendUnique("sigtrap", "SIGHUP") + go telemetry.AppendUnique("sigtrap", "SIGHUP") } } }() diff --git a/diagnostics/collection.go b/telemetry/collection.go similarity index 95% rename from diagnostics/collection.go rename to telemetry/collection.go index e2b222b38..d4b1e5012 100644 --- a/diagnostics/collection.go +++ b/telemetry/collection.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package diagnostics +package telemetry import ( "log" @@ -139,7 +139,7 @@ func Append(key string, value interface{}) { sliceVal, sliceOk := bufVal.([]interface{}) if inBuffer && !sliceOk { bufferMu.Unlock() - log.Printf("[PANIC] Diagnostics: key %s already used for non-slice value", key) + log.Printf("[PANIC] Telemetry: key %s already used for non-slice value", key) return } if sliceVal == nil { @@ -169,7 +169,7 @@ func AppendUnique(key string, value interface{}) { setVal, setOk := bufVal.(countingSet) if inBuffer && !setOk { bufferMu.Unlock() - log.Printf("[PANIC] Diagnostics: key %s already used for non-counting-set value", key) + log.Printf("[PANIC] Telemetry: key %s already used for non-counting-set value", key) return } if setVal == nil { @@ -212,7 +212,7 @@ func atomicAdd(key string, amount int) { intVal, intOk := bufVal.(int) if inBuffer && !intOk { bufferMu.Unlock() - log.Printf("[PANIC] Diagnostics: key %s already used for non-integer value", key) + log.Printf("[PANIC] Telemetry: key %s already used for non-integer value", key) return } if !inBuffer { diff --git a/diagnostics/collection_test.go b/telemetry/collection_test.go similarity index 99% rename from diagnostics/collection_test.go rename to telemetry/collection_test.go index e895a0a4e..e25b9b061 100644 --- a/diagnostics/collection_test.go +++ b/telemetry/collection_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package diagnostics +package telemetry import ( "fmt" diff --git a/diagnostics/diagnostics.go b/telemetry/telemetry.go similarity index 83% rename from diagnostics/diagnostics.go rename to telemetry/telemetry.go index 869f731dd..0e831fad4 100644 --- a/diagnostics/diagnostics.go +++ b/telemetry/telemetry.go @@ -12,26 +12,25 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package diagnostics implements the client for server-side diagnostics +// Package telemetry implements the client for server-side telemetry // of the network. Functions in this package are synchronous and blocking // unless otherwise specified. For convenience, most functions here do // not return errors, but errors are logged to the standard logger. // // To use this package, first call Init(). You can then call any of the // collection/aggregation functions. Call StartEmitting() when you are -// ready to begin sending diagnostic updates. +// ready to begin sending telemetry updates. // // When collecting metrics (functions like Set, AppendUnique, or Increment), // it may be desirable and even recommended to invoke them in a new -// goroutine (use the go keyword) in case there is lock contention; -// they are thread-safe (unless noted), and you may not want them to -// block the main thread of execution. However, sometimes blocking -// may be necessary too; for example, adding startup metrics to the -// buffer before the call to StartEmitting(). +// goroutine in case there is lock contention; they are thread-safe (unless +// noted), and you may not want them to block the main thread of execution. +// However, sometimes blocking may be necessary too; for example, adding +// startup metrics to the buffer before the call to StartEmitting(). // // This package is designed to be as fast and space-efficient as reasonably // possible, so that it does not disrupt the flow of execution. -package diagnostics +package telemetry import ( "bytes" @@ -53,17 +52,17 @@ import ( func logEmit(final bool) { err := emit(final) if err != nil { - log.Printf("[ERROR] Sending diagnostics: %v", err) + log.Printf("[ERROR] Sending telemetry: %v", err) } } -// emit sends an update to the diagnostics server. +// emit sends an update to the telemetry server. // Set final to true if this is the last call to emit. // If final is true, no future updates will be scheduled. // Otherwise, the next update will be scheduled. func emit(final bool) error { if !enabled { - return fmt.Errorf("diagnostics not enabled") + return fmt.Errorf("telemetry not enabled") } // ensure only one update happens at a time; @@ -71,7 +70,7 @@ func emit(final bool) error { updateMu.Lock() if updating { updateMu.Unlock() - log.Println("[NOTICE] Skipping this diagnostics update because previous one is still working") + log.Println("[NOTICE] Skipping this telemetry update because previous one is still working") return nil } updating = true @@ -100,7 +99,7 @@ func emit(final bool) error { if i > 0 && err != nil { // don't hammer the server; first failure might have been // a fluke, but back off more after that - log.Printf("[WARNING] Sending diagnostics (attempt %d): %v - backing off and retrying", i, err) + log.Printf("[WARNING] Sending telemetry (attempt %d): %v - backing off and retrying", i, err) time.Sleep(time.Duration((i+1)*(i+1)*(i+1)) * time.Second) } @@ -114,7 +113,7 @@ func emit(final bool) error { // check for any special-case response codes if resp.StatusCode == http.StatusGone { // the endpoint has been deprecated and is no longer servicing clients - err = fmt.Errorf("diagnostics server replied with HTTP %d; upgrade required", resp.StatusCode) + err = fmt.Errorf("telemetry server replied with HTTP %d; upgrade required", resp.StatusCode) if clen := resp.Header.Get("Content-Length"); clen != "0" && clen != "" { bodyBytes, readErr := ioutil.ReadAll(resp.Body) if readErr != nil { @@ -128,7 +127,7 @@ func emit(final bool) error { } if resp.StatusCode == http.StatusUnavailableForLegalReasons { // the endpoint is unavailable, at least to this client, for legal reasons (!) - err = fmt.Errorf("diagnostics server replied with HTTP %d %s: please consult the project website and developers for guidance", resp.StatusCode, resp.Status) + err = fmt.Errorf("telemetry server replied with HTTP %d %s: please consult the project website and developers for guidance", resp.StatusCode, resp.Status) if clen := resp.Header.Get("Content-Length"); clen != "0" && clen != "" { bodyBytes, readErr := ioutil.ReadAll(resp.Body) if readErr != nil { @@ -144,7 +143,7 @@ func emit(final bool) error { // okay, ensure we can interpret the response if ct := resp.Header.Get("Content-Type"); (resp.StatusCode < 300 || resp.StatusCode >= 400) && !strings.Contains(ct, "json") { - err = fmt.Errorf("diagnostics server replied with unknown content-type: '%s' and HTTP %s", ct, resp.Status) + err = fmt.Errorf("telemetry server replied with unknown content-type: '%s' and HTTP %s", ct, resp.Status) resp.Body.Close() continue } @@ -167,12 +166,12 @@ func emit(final bool) error { } } if !final { - log.Printf("[NOTICE] Sending diagnostics: we were too early; waiting %s before trying again", reply.NextUpdate) + log.Printf("[NOTICE] Sending telemetry: we were too early; waiting %s before trying again", reply.NextUpdate) time.Sleep(reply.NextUpdate) continue } } else if resp.StatusCode >= 400 { - err = fmt.Errorf("diagnostics server returned status code %d", resp.StatusCode) + err = fmt.Errorf("telemetry server returned status code %d", resp.StatusCode) continue } @@ -181,14 +180,14 @@ func emit(final bool) error { if err == nil && !final { // (remember, if there was an error, we return it // below, so it WILL get logged if it's supposed to) - log.Println("[INFO] Sending diagnostics: success") + log.Println("[INFO] Sending telemetry: success") } // even if there was an error after all retries, we should // schedule the next update using our default update // interval because the server might be healthy later - // ensure we won't slam the diagnostics server + // ensure we won't slam the telemetry server if reply.NextUpdate < 1*time.Second { reply.NextUpdate = defaultUpdateInterval } @@ -247,13 +246,13 @@ func resetBuffer() map[string]interface{} { } // Response contains the body of a response from the -// diagnostics server. +// telemetry server. type Response struct { // NextUpdate is how long to wait before the next update. NextUpdate time.Duration `json:"next_update"` - // Stop instructs the diagnostics server to stop sending - // diagnostics. This would only be done under extenuating + // Stop instructs the telemetry server to stop sending + // telemetry. This would only be done under extenuating // circumstances, but we are prepared for it nonetheless. Stop bool `json:"stop,omitempty"` @@ -262,7 +261,7 @@ type Response struct { Error string `json:"error,omitempty"` } -// Payload is the data that gets sent to the diagnostics server. +// Payload is the data that gets sent to the telemetry server. type Payload struct { // The universally unique ID of the instance InstanceID string `json:"instance_id"` @@ -337,7 +336,7 @@ var ( updateTimerMu sync.Mutex // instanceUUID is the ID of the current instance. - // This MUST be set to emit diagnostics. + // This MUST be set to emit telemetry. // This MUST NOT be openly exposed to clients, for privacy. instanceUUID uuid.UUID @@ -352,12 +351,12 @@ var ( ) const ( - // endpoint is the base URL to remote diagnostics server; + // endpoint is the base URL to remote telemetry server; // the instance ID will be appended to it. - endpoint = "https://diagnostics-staging.caddyserver.com/update/" // TODO: make configurable, "http://localhost:8085/update/" + endpoint = "https://telemetry-staging.caddyserver.com/v1/update/" // defaultUpdateInterval is how long to wait before emitting - // more diagnostic data if all retires fail. This value is + // more telemetry data if all retires fail. This value is // only used if the client receives a nonsensical value, or // doesn't send one at all, or if a connection can't be made, // likely indicating a problem with the server. Thus, this diff --git a/diagnostics/diagnostics_test.go b/telemetry/telemetry_test.go similarity index 98% rename from diagnostics/diagnostics_test.go rename to telemetry/telemetry_test.go index af458d99b..9ff4ad618 100644 --- a/diagnostics/diagnostics_test.go +++ b/telemetry/telemetry_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package diagnostics +package telemetry import ( "encoding/json" From 8bdd13b594df03a4fb393bd930a1fcdee8bacd2f Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 22 Mar 2018 19:50:38 -0600 Subject: [PATCH 13/28] telemetry: Honor the server's request to toggle certain metrics --- telemetry/collection.go | 21 +++++++++++++++------ telemetry/telemetry.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/telemetry/collection.go b/telemetry/collection.go index d4b1e5012..3e7e2fc5b 100644 --- a/telemetry/collection.go +++ b/telemetry/collection.go @@ -104,7 +104,7 @@ func Reset() { // go keyword after the call to SendHello so it // doesn't block crucial code. func Set(key string, val interface{}) { - if !enabled { + if !enabled || isDisabled(key) { return } bufferMu.Lock() @@ -123,10 +123,8 @@ func Set(key string, val interface{}) { // If key is new, a new list will be created. // If key maps to a type that is not a list, // a panic is logged, and this is a no-op. -// -// TODO: is this function needed/useful? func Append(key string, value interface{}) { - if !enabled { + if !enabled || isDisabled(key) { return } bufferMu.Lock() @@ -161,7 +159,7 @@ func Append(key string, value interface{}) { // that is not a counting set, a panic is logged, // and this is a no-op. func AppendUnique(key string, value interface{}) { - if !enabled { + if !enabled || isDisabled(key) { return } bufferMu.Lock() @@ -204,7 +202,7 @@ func Increment(key string) { // atomicAdd adds amount (negative to subtract) // to key. func atomicAdd(key string, amount int) { - if !enabled { + if !enabled || isDisabled(key) { return } bufferMu.Lock() @@ -225,3 +223,14 @@ func atomicAdd(key string, amount int) { buffer[key] = intVal + amount bufferMu.Unlock() } + +// isDisabled returns whether key is +// a disabled metric key. ALL collection +// functions should call this and not +// save the value if this returns true. +func isDisabled(key string) bool { + disabledMetricsMu.RLock() + _, ok := disabledMetrics[key] + disabledMetricsMu.RUnlock() + return ok +} diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index 0e831fad4..bc193e316 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -155,6 +155,18 @@ func emit(final bool) error { continue } + // update the list of enabled/disabled keys, if any + for _, key := range reply.EnableKeys { + disabledMetricsMu.Lock() + delete(disabledMetrics, key) + disabledMetricsMu.Unlock() + } + for _, key := range reply.DisableKeys { + disabledMetricsMu.Lock() + disabledMetrics[key] = struct{}{} + disabledMetricsMu.Unlock() + } + // make sure we didn't send the update too soon; if so, // just wait and try again -- this is a special case of // error that we handle differently, as you can see @@ -259,6 +271,18 @@ type Response struct { // Error will be populated with an error message, if any. // This field should be empty if the status code is < 400. Error string `json:"error,omitempty"` + + // DisableKeys will contain a list of keys/metrics that + // should NOT be sent until further notice. The client + // must NOT store these items in its buffer or send them + // to the telemetry server while they are disabled. If + // this list and EnableKeys have the same value (which is + // not supposed to happen), this field should dominate. + DisableKeys []string `json:"disable_keys,omitempty"` + + // EnableKeys will contain a list of keys/metrics that + // MAY be sent until further notice. + EnableKeys []string `json:"enable_keys,omitempty"` } // Payload is the data that gets sent to the telemetry server. @@ -335,6 +359,12 @@ var ( updateTimer *time.Timer updateTimerMu sync.Mutex + // disabledMetrics is a list of metric keys + // that should NOT be saved to the buffer + // or sent to the telemetry server. + disabledMetrics = make(map[string]struct{}) + disabledMetricsMu sync.RWMutex + // instanceUUID is the ID of the current instance. // This MUST be set to emit telemetry. // This MUST NOT be openly exposed to clients, for privacy. From 33aeb1cb5c4e796e06cfd3b39db280a7cf162738 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 23 Mar 2018 23:44:16 -0600 Subject: [PATCH 14/28] telemetry: Add CLI option to selectively disable some metrics Also fix a couple metrics that were named wrong or reported in excess. --- caddy.go | 2 +- caddy/caddymain/run.go | 22 +++++++++++++--------- caddytls/setup.go | 2 -- telemetry/collection.go | 11 ++++++++++- telemetry/telemetry.go | 20 +++++++++++++++----- 5 files changed, 39 insertions(+), 18 deletions(-) diff --git a/caddy.go b/caddy.go index 851e5315f..cc9dfee45 100644 --- a/caddy.go +++ b/caddy.go @@ -617,7 +617,7 @@ func ValidateAndExecuteDirectives(cdyfile Input, inst *Instance, justValidate bo return fmt.Errorf("error inspecting server blocks: %v", err) } - telemetry.Set("http_num_server_blocks", len(sblocks)) + telemetry.Set("num_server_blocks", len(sblocks)) return executeDirectives(inst, cdyfile.Path(), stype.Directives(), sblocks, justValidate) } diff --git a/caddy/caddymain/run.go b/caddy/caddymain/run.go index cd1b47409..1d9f29573 100644 --- a/caddy/caddymain/run.go +++ b/caddy/caddymain/run.go @@ -46,6 +46,7 @@ func init() { flag.StringVar(&caddytls.DefaultCAUrl, "ca", "https://acme-v01.api.letsencrypt.org/directory", "URL to certificate authority's ACME server directory") flag.BoolVar(&caddytls.DisableHTTPChallenge, "disable-http-challenge", caddytls.DisableHTTPChallenge, "Disable the ACME HTTP challenge") flag.BoolVar(&caddytls.DisableTLSSNIChallenge, "disable-tls-sni-challenge", caddytls.DisableTLSSNIChallenge, "Disable the ACME TLS-SNI challenge") + flag.StringVar(&disabledMetrics, "disabled-metrics", "", "Comma-separated list of telemetry metrics to disable") flag.StringVar(&conf, "conf", "", "Caddyfile to load (default \""+caddy.DefaultConfigFile+"\")") flag.StringVar(&cpu, "cpu", "100%", "CPU cap") flag.BoolVar(&plugins, "plugins", false, "List installed plugins") @@ -91,6 +92,8 @@ func Run() { // initialize telemetry client if enableTelemetry { initTelemetry() + } else if disabledMetrics != "" { + mustLogFatalf("[ERROR] Cannot disable specific metrics because telemetry is disabled") } // Check for one-time actions @@ -326,21 +329,22 @@ func initTelemetry() { } } - telemetry.Init(id) + telemetry.Init(id, strings.Split(disabledMetrics, ",")) } const appName = "Caddy" // Flags that control program flow or startup var ( - serverType string - conf string - cpu string - logfile string - revoke string - version bool - plugins bool - validate bool + serverType string + conf string + cpu string + logfile string + revoke string + version bool + plugins bool + validate bool + disabledMetrics string ) // Build information obtained with the help of -ldflags diff --git a/caddytls/setup.go b/caddytls/setup.go index 8100088b3..ef29ed2e0 100644 --- a/caddytls/setup.go +++ b/caddytls/setup.go @@ -254,7 +254,6 @@ func setupTLS(c *caddy.Controller) error { return c.Errf("Unable to load certificate and key files for '%s': %v", c.Key, err) } log.Printf("[INFO] Successfully loaded TLS assets from %s and %s", certificateFile, keyFile) - telemetry.Increment("tls_manual_cert_count") } // load a directory of certificates, if specified @@ -355,7 +354,6 @@ func loadCertsInDir(cfg *Config, c *caddy.Controller, dir string) error { return c.Errf("%s: failed to load cert and key for '%s': %v", path, c.Key, err) } log.Printf("[INFO] Successfully loaded TLS assets from %s", path) - telemetry.Increment("tls_manual_cert_count") } return nil }) diff --git a/telemetry/collection.go b/telemetry/collection.go index 3e7e2fc5b..91183f83b 100644 --- a/telemetry/collection.go +++ b/telemetry/collection.go @@ -28,7 +28,11 @@ import ( // may safely be used. If this function is not // called, the collector functions may still be // invoked, but they will be no-ops. -func Init(instanceID uuid.UUID) { +// +// Any metrics keys that are passed in the second +// argument will be permanently disabled for the +// lifetime of the process. +func Init(instanceID uuid.UUID, disabledMetricsKeys []string) { if enabled { panic("already initialized") } @@ -37,6 +41,11 @@ func Init(instanceID uuid.UUID) { panic("empty UUID") } instanceUUID = instanceID + disabledMetricsMu.Lock() + for _, key := range disabledMetricsKeys { + disabledMetrics[key] = false + } + disabledMetricsMu.Unlock() enabled = true } diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index bc193e316..6a83b2fa2 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -158,12 +158,15 @@ func emit(final bool) error { // update the list of enabled/disabled keys, if any for _, key := range reply.EnableKeys { disabledMetricsMu.Lock() - delete(disabledMetrics, key) + // only re-enable this metric if it is temporarily disabled + if temp, ok := disabledMetrics[key]; ok && temp { + delete(disabledMetrics, key) + } disabledMetricsMu.Unlock() } for _, key := range reply.DisableKeys { disabledMetricsMu.Lock() - disabledMetrics[key] = struct{}{} + disabledMetrics[key] = true // all remotely-disabled keys are "temporarily" disabled disabledMetricsMu.Unlock() } @@ -359,10 +362,17 @@ var ( updateTimer *time.Timer updateTimerMu sync.Mutex - // disabledMetrics is a list of metric keys + // disabledMetrics is a set of metric keys // that should NOT be saved to the buffer - // or sent to the telemetry server. - disabledMetrics = make(map[string]struct{}) + // or sent to the telemetry server. The value + // indicates whether the entry is temporary. + // If the value is true, it may be removed if + // the metric is re-enabled remotely later. If + // the value is false, it is permanent + // (presumably becaues the user explicitly + // disabled it) and can only be re-enabled + // with user consent. + disabledMetrics = make(map[string]bool) disabledMetricsMu sync.RWMutex // instanceUUID is the ID of the current instance. From 8039a7127f3a8635b95f8e1ee2f7c5a08ffff62a Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Sun, 25 Mar 2018 21:50:07 -0600 Subject: [PATCH 15/28] telemetry: Remove a metric, clarify another, and fix tests --- caddytls/handshake.go | 4 +--- telemetry/collection_test.go | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/caddytls/handshake.go b/caddytls/handshake.go index 2d49d9787..077ec6323 100644 --- a/caddytls/handshake.go +++ b/caddytls/handshake.go @@ -121,9 +121,7 @@ func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certif // }) cert, err := cfg.getCertDuringHandshake(strings.ToLower(clientHello.ServerName), true, true) if err == nil { - go telemetry.Increment("tls_handshake_count") - } else { - go telemetry.Append("tls_handshake_error", err.Error()) + go telemetry.Increment("tls_handshake_count") // TODO: This is a "best guess" for now, we need something listener-level } return &cert.Certificate, err } diff --git a/telemetry/collection_test.go b/telemetry/collection_test.go index e25b9b061..4199b684a 100644 --- a/telemetry/collection_test.go +++ b/telemetry/collection_test.go @@ -31,7 +31,7 @@ func TestInit(t *testing.T) { t.Errorf("Second call to Init should have panicked") } }() - Init(id) // should panic + Init(id, nil) // should panic } func TestInitEmptyUUID(t *testing.T) { @@ -41,7 +41,7 @@ func TestInitEmptyUUID(t *testing.T) { t.Errorf("Call to Init with empty UUID should have panicked") } }() - Init(uuid.UUID([16]byte{})) + Init(uuid.UUID([16]byte{}), nil) } func TestSet(t *testing.T) { @@ -92,7 +92,7 @@ func doInit(t *testing.T) uuid.UUID { if err != nil { t.Fatalf("Could not make UUID: %v", err) } - Init(id) + Init(id, nil) return id } From 518edd3cd45fa147a7c5bb3ca5cb717e17a624b2 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 20 Apr 2018 00:04:44 -0600 Subject: [PATCH 16/28] Corrected permissions for UUID file --- caddy/caddymain/run.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/caddy/caddymain/run.go b/caddy/caddymain/run.go index 7e1d0da77..38687fd69 100644 --- a/caddy/caddymain/run.go +++ b/caddy/caddymain/run.go @@ -298,7 +298,7 @@ func initTelemetry() { newUUID := func() uuid.UUID { id := uuid.New() - err := ioutil.WriteFile(uuidFilename, []byte(id.String()), 0644) // human-readable this way + err := ioutil.WriteFile(uuidFilename, []byte(id.String()), 0600) // human-readable as a string if err != nil { log.Printf("[ERROR] Persisting instance UUID: %v", err) } From 078770a5a6fe067070df44fff3eaa32d270906b1 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 7 May 2018 16:09:39 -0600 Subject: [PATCH 17/28] telemetry: Record TLS ClientHellos by hash of key of structured data Also improve handling of disabled metrics, and record TLS ClientHello in association with User-Agent --- caddy/caddymain/run.go | 36 +++++++++++-- caddyhttp/httpserver/mitm.go | 96 ++++++++++++++++++---------------- caddyhttp/httpserver/plugin.go | 6 +++ caddyhttp/httpserver/server.go | 8 ++- caddytls/crypto.go | 2 +- caddytls/handshake.go | 77 ++++++++++++++++++++------- telemetry/collection.go | 63 +++++++++++++++++++--- 7 files changed, 211 insertions(+), 77 deletions(-) diff --git a/caddy/caddymain/run.go b/caddy/caddymain/run.go index 38687fd69..9884b0111 100644 --- a/caddy/caddymain/run.go +++ b/caddy/caddymain/run.go @@ -91,7 +91,10 @@ func Run() { // initialize telemetry client if enableTelemetry { - initTelemetry() + err := initTelemetry() + if err != nil { + mustLogFatalf("[ERROR] Initializing telemetry: %v", err) + } } else if disabledMetrics != "" { mustLogFatalf("[ERROR] Cannot disable specific metrics because telemetry is disabled") } @@ -293,7 +296,7 @@ func setCPU(cpu string) error { } // initTelemetry initializes the telemetry engine. -func initTelemetry() { +func initTelemetry() error { uuidFilename := filepath.Join(caddy.AssetsPath(), "uuid") newUUID := func() uuid.UUID { @@ -329,7 +332,34 @@ func initTelemetry() { } } - telemetry.Init(id, strings.Split(disabledMetrics, ",")) + // parse and check the list of disabled metrics + var disabledMetricsSlice []string + if len(disabledMetrics) > 0 { + if len(disabledMetrics) > 1024 { + // mitigate disk space exhaustion at the collection endpoint + return fmt.Errorf("too many metrics to disable") + } + disabledMetricsSlice = strings.Split(disabledMetrics, ",") + for i, metric := range disabledMetricsSlice { + if metric == "instance_id" || metric == "timestamp" || metric == "disabled_metrics" { + return fmt.Errorf("instance_id, timestamp, and disabled_metrics cannot be disabled") + } + if metric == "" { + disabledMetricsSlice = append(disabledMetricsSlice[:i], disabledMetricsSlice[i+1:]...) + } + } + } + + // initialize telemetry + telemetry.Init(id, disabledMetricsSlice) + + // if any metrics were disabled, report it + if len(disabledMetricsSlice) > 0 { + telemetry.Set("disabled_metrics", disabledMetricsSlice) + log.Printf("[NOTICE] The following telemetry metrics are disabled: %s", disabledMetrics) + } + + return nil } const appName = "Caddy" diff --git a/caddyhttp/httpserver/mitm.go b/caddyhttp/httpserver/mitm.go index 8a610358f..2daa26d16 100644 --- a/caddyhttp/httpserver/mitm.go +++ b/caddyhttp/httpserver/mitm.go @@ -25,6 +25,7 @@ import ( "strings" "sync" + "github.com/mholt/caddy/caddytls" "github.com/mholt/caddy/telemetry" ) @@ -65,6 +66,9 @@ func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ua := r.Header.Get("User-Agent") + // report this request's UA in connection with this ClientHello + go telemetry.AppendUnique("tls_client_hello_ua:"+info.Key(), ua) + var checked, mitm bool if r.Header.Get("X-BlueCoat-Via") != "" || // Blue Coat (masks User-Agent header to generic values) r.Header.Get("X-FCCKV2") != "" || // Fortinet @@ -207,6 +211,11 @@ func (c *clientHelloConn) Read(b []byte) (n int, err error) { c.listener.helloInfos[c.Conn.RemoteAddr().String()] = rawParsed c.listener.helloInfosMu.Unlock() + // report this ClientHello to telemetry + chKey := rawParsed.Key() + go telemetry.SetNested("tls_client_hello", chKey, rawParsed) + go telemetry.AppendUnique("tls_client_hello_count", chKey) + c.readHello = true return } @@ -227,6 +236,7 @@ func parseRawClientHello(data []byte) (info rawHelloInfo) { if len(data) < 42 { return } + info.Version = uint16(data[4])<<8 | uint16(data[5]) sessionIDLen := int(data[38]) if sessionIDLen > 32 || len(data) < 39+sessionIDLen { return @@ -243,9 +253,9 @@ func parseRawClientHello(data []byte) (info rawHelloInfo) { } numCipherSuites := cipherSuiteLen / 2 // read in the cipher suites - info.cipherSuites = make([]uint16, numCipherSuites) + info.CipherSuites = make([]uint16, numCipherSuites) for i := 0; i < numCipherSuites; i++ { - info.cipherSuites[i] = uint16(data[2+2*i])<<8 | uint16(data[3+2*i]) + info.CipherSuites[i] = uint16(data[2+2*i])<<8 | uint16(data[3+2*i]) } data = data[2+cipherSuiteLen:] if len(data) < 1 { @@ -256,7 +266,7 @@ func parseRawClientHello(data []byte) (info rawHelloInfo) { if len(data) < 1+compressionMethodsLen { return } - info.compressionMethods = data[1 : 1+compressionMethodsLen] + info.CompressionMethods = data[1 : 1+compressionMethodsLen] data = data[1+compressionMethodsLen:] @@ -284,7 +294,7 @@ func parseRawClientHello(data []byte) (info rawHelloInfo) { } // record that the client advertised support for this extension - info.extensions = append(info.extensions, extension) + info.Extensions = append(info.Extensions, extension) switch extension { case extensionSupportedCurves: @@ -297,10 +307,10 @@ func parseRawClientHello(data []byte) (info rawHelloInfo) { return } numCurves := l / 2 - info.curves = make([]tls.CurveID, numCurves) + info.Curves = make([]tls.CurveID, numCurves) d := data[2:] for i := 0; i < numCurves; i++ { - info.curves[i] = tls.CurveID(d[0])<<8 | tls.CurveID(d[1]) + info.Curves[i] = tls.CurveID(d[0])<<8 | tls.CurveID(d[1]) d = d[2:] } case extensionSupportedPoints: @@ -312,8 +322,8 @@ func parseRawClientHello(data []byte) (info rawHelloInfo) { if length != l+1 { return } - info.points = make([]uint8, l) - copy(info.points, data[1:]) + info.Points = make([]uint8, l) + copy(info.Points, data[1:]) } data = data[length:] @@ -364,18 +374,12 @@ func (l *tlsHelloListener) Accept() (net.Conn, error) { // by Durumeric, Halderman, et. al. in // "The Security Impact of HTTPS Interception": // https://jhalderm.com/pub/papers/interception-ndss17.pdf -type rawHelloInfo struct { - cipherSuites []uint16 - extensions []uint16 - compressionMethods []byte - curves []tls.CurveID - points []uint8 -} +type rawHelloInfo struct{ caddytls.ClientHelloInfo } // advertisesHeartbeatSupport returns true if info indicates // that the client supports the Heartbeat extension. func (info rawHelloInfo) advertisesHeartbeatSupport() bool { - for _, ext := range info.extensions { + for _, ext := range info.Extensions { if ext == extensionHeartbeat { return true } @@ -398,31 +402,31 @@ func (info rawHelloInfo) looksLikeFirefox() bool { // Note: Firefox 55+ doesn't appear to advertise 0xFF03 (65283, short headers). It used to be between 5 and 13. // Note: Firefox on Fedora (or RedHat) doesn't include ECC suites because of patent liability. requiredExtensionsOrder := []uint16{23, 65281, 10, 11, 35, 16, 5, 13} - if !assertPresenceAndOrdering(requiredExtensionsOrder, info.extensions, true) { + if !assertPresenceAndOrdering(requiredExtensionsOrder, info.Extensions, true) { return false } // We check for both presence of curves and their ordering. requiredCurves := []tls.CurveID{29, 23, 24, 25} - if len(info.curves) < len(requiredCurves) { + if len(info.Curves) < len(requiredCurves) { return false } for i := range requiredCurves { - if info.curves[i] != requiredCurves[i] { + if info.Curves[i] != requiredCurves[i] { return false } } - if len(info.curves) > len(requiredCurves) { + if len(info.Curves) > len(requiredCurves) { // newer Firefox (55 Nightly?) may have additional curves at end of list allowedCurves := []tls.CurveID{256, 257} for i := range allowedCurves { - if info.curves[len(requiredCurves)+i] != allowedCurves[i] { + if info.Curves[len(requiredCurves)+i] != allowedCurves[i] { return false } } } - if hasGreaseCiphers(info.cipherSuites) { + if hasGreaseCiphers(info.CipherSuites) { return false } @@ -449,7 +453,7 @@ func (info rawHelloInfo) looksLikeFirefox() bool { tls.TLS_RSA_WITH_AES_256_CBC_SHA, // 0x35 tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, // 0xa } - return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.cipherSuites, false) + return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.CipherSuites, false) } // looksLikeChrome returns true if info looks like a handshake @@ -490,20 +494,20 @@ func (info rawHelloInfo) looksLikeChrome() bool { TLS_DHE_RSA_WITH_AES_128_CBC_SHA: {}, // 0x33 TLS_DHE_RSA_WITH_AES_256_CBC_SHA: {}, // 0x39 } - for _, ext := range info.cipherSuites { + for _, ext := range info.CipherSuites { if _, ok := chromeCipherExclusions[ext]; ok { return false } } // Chrome does not include curve 25 (CurveP521) (as of Chrome 56, Feb. 2017). - for _, curve := range info.curves { + for _, curve := range info.Curves { if curve == 25 { return false } } - if !hasGreaseCiphers(info.cipherSuites) { + if !hasGreaseCiphers(info.CipherSuites) { return false } @@ -521,19 +525,19 @@ func (info rawHelloInfo) looksLikeEdge() bool { // More specifically, the OCSP status request extension appears // *directly* before the other two extensions, which occur in that // order. (I contacted the authors for clarification and verified it.) - for i, ext := range info.extensions { + for i, ext := range info.Extensions { if ext == extensionOCSPStatusRequest { - if len(info.extensions) <= i+2 { + if len(info.Extensions) <= i+2 { return false } - if info.extensions[i+1] != extensionSupportedCurves || - info.extensions[i+2] != extensionSupportedPoints { + if info.Extensions[i+1] != extensionSupportedCurves || + info.Extensions[i+2] != extensionSupportedPoints { return false } } } - for _, cs := range info.cipherSuites { + for _, cs := range info.CipherSuites { // As of Feb. 2017, Edge does not have 0xff, but Avast adds it if cs == scsvRenegotiation { return false @@ -544,7 +548,7 @@ func (info rawHelloInfo) looksLikeEdge() bool { } } - if hasGreaseCiphers(info.cipherSuites) { + if hasGreaseCiphers(info.CipherSuites) { return false } @@ -570,23 +574,23 @@ func (info rawHelloInfo) looksLikeSafari() bool { // We check for the presence and order of the extensions. requiredExtensionsOrder := []uint16{10, 11, 13, 13172, 16, 5, 18, 23} - if !assertPresenceAndOrdering(requiredExtensionsOrder, info.extensions, true) { + if !assertPresenceAndOrdering(requiredExtensionsOrder, info.Extensions, true) { // Safari on iOS 11 (beta) uses different set/ordering of extensions requiredExtensionsOrderiOS11 := []uint16{65281, 0, 23, 13, 5, 13172, 18, 16, 11, 10} - if !assertPresenceAndOrdering(requiredExtensionsOrderiOS11, info.extensions, true) { + if !assertPresenceAndOrdering(requiredExtensionsOrderiOS11, info.Extensions, true) { return false } } else { // For these versions of Safari, expect TLS_EMPTY_RENEGOTIATION_INFO_SCSV first. - if len(info.cipherSuites) < 1 { + if len(info.CipherSuites) < 1 { return false } - if info.cipherSuites[0] != scsvRenegotiation { + if info.CipherSuites[0] != scsvRenegotiation { return false } } - if hasGreaseCiphers(info.cipherSuites) { + if hasGreaseCiphers(info.CipherSuites) { return false } @@ -611,19 +615,19 @@ func (info rawHelloInfo) looksLikeSafari() bool { tls.TLS_RSA_WITH_AES_256_CBC_SHA, // 0x35 tls.TLS_RSA_WITH_AES_128_CBC_SHA, // 0x2f } - return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.cipherSuites, true) + return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.CipherSuites, true) } // looksLikeTor returns true if the info looks like a ClientHello from Tor browser // (based on Firefox). func (info rawHelloInfo) looksLikeTor() bool { requiredExtensionsOrder := []uint16{10, 11, 16, 5, 13} - if !assertPresenceAndOrdering(requiredExtensionsOrder, info.extensions, true) { + if !assertPresenceAndOrdering(requiredExtensionsOrder, info.Extensions, true) { return false } // check for session tickets support; Tor doesn't support them to prevent tracking - for _, ext := range info.extensions { + for _, ext := range info.Extensions { if ext == 35 { return false } @@ -631,12 +635,12 @@ func (info rawHelloInfo) looksLikeTor() bool { // We check for both presence of curves and their ordering, including // an optional curve at the beginning (for Tor based on Firefox 52) - infoCurves := info.curves - if len(info.curves) == 4 { - if info.curves[0] != 29 { + infoCurves := info.Curves + if len(info.Curves) == 4 { + if info.Curves[0] != 29 { return false } - infoCurves = info.curves[1:] + infoCurves = info.Curves[1:] } requiredCurves := []tls.CurveID{23, 24, 25} if len(infoCurves) < len(requiredCurves) { @@ -648,7 +652,7 @@ func (info rawHelloInfo) looksLikeTor() bool { } } - if hasGreaseCiphers(info.cipherSuites) { + if hasGreaseCiphers(info.CipherSuites) { return false } @@ -675,7 +679,7 @@ func (info rawHelloInfo) looksLikeTor() bool { tls.TLS_RSA_WITH_AES_256_CBC_SHA, // 0x35 tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, // 0xa } - return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.cipherSuites, false) + return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.CipherSuites, false) } // assertPresenceAndOrdering will return true if candidateList contains diff --git a/caddyhttp/httpserver/plugin.go b/caddyhttp/httpserver/plugin.go index ead28d796..332756f8e 100644 --- a/caddyhttp/httpserver/plugin.go +++ b/caddyhttp/httpserver/plugin.go @@ -67,6 +67,12 @@ func init() { caddy.RegisterParsingCallback(serverType, "root", hideCaddyfile) caddy.RegisterParsingCallback(serverType, "tls", activateHTTPS) caddytls.RegisterConfigGetter(serverType, func(c *caddy.Controller) *caddytls.Config { return GetConfig(c).TLS }) + + // disable the caddytls package reporting ClientHellos + // to telemetry, since our MITM detector does this but + // with more information than the standard lib provides + // (as of May 2018) + caddytls.ClientHelloTelemetry = false } // hideCaddyfile hides the source/origin Caddyfile if it is within the diff --git a/caddyhttp/httpserver/server.go b/caddyhttp/httpserver/server.go index 76b224e94..45f1c639d 100644 --- a/caddyhttp/httpserver/server.go +++ b/caddyhttp/httpserver/server.go @@ -349,8 +349,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } }() - // TODO: Somehow report UA string in conjunction with TLS handshake, if any (and just once per connection) - go telemetry.AppendUnique("http_user_agent", r.Header.Get("User-Agent")) + // record the User-Agent string (with a cap on its length to mitigate attacks) + ua := r.Header.Get("User-Agent") + if len(ua) > 512 { + ua = ua[:512] + } + go telemetry.AppendUnique("http_user_agent", ua) go telemetry.Increment("http_request_count") // copy the original, unchanged URL into the context diff --git a/caddytls/crypto.go b/caddytls/crypto.go index 51cab7f4d..6ca51bb96 100644 --- a/caddytls/crypto.go +++ b/caddytls/crypto.go @@ -341,7 +341,7 @@ func standaloneTLSTicketKeyRotation(c *tls.Config, ticker *time.Ticker, exitChan // Do not use this for cryptographic purposes. func fastHash(input []byte) string { h := fnv.New32a() - h.Write([]byte(input)) + h.Write(input) return fmt.Sprintf("%x", h.Sum32()) } diff --git a/caddytls/handshake.go b/caddytls/handshake.go index f507b9029..ad190e338 100644 --- a/caddytls/handshake.go +++ b/caddytls/handshake.go @@ -99,25 +99,23 @@ func (cg configGroup) GetConfigForClient(clientHello *tls.ClientHelloInfo) (*tls // // This method is safe for use as a tls.Config.GetCertificate callback. func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { - // TODO: We need to collect this in a heavily de-duplicating way - // It would also be nice to associate a handshake with the UA string (but that is only for HTTP server type) - // go telemetry.Append("tls_client_hello", struct { - // NoSNI bool `json:"no_sni,omitempty"` - // CipherSuites []uint16 `json:"cipher_suites,omitempty"` - // SupportedCurves []tls.CurveID `json:"curves,omitempty"` - // SupportedPoints []uint8 `json:"points,omitempty"` - // SignatureSchemes []tls.SignatureScheme `json:"sig_scheme,omitempty"` - // ALPN []string `json:"alpn,omitempty"` - // SupportedVersions []uint16 `json:"versions,omitempty"` - // }{ - // NoSNI: clientHello.ServerName == "", - // CipherSuites: clientHello.CipherSuites, - // SupportedCurves: clientHello.SupportedCurves, - // SupportedPoints: clientHello.SupportedPoints, - // SignatureSchemes: clientHello.SignatureSchemes, - // ALPN: clientHello.SupportedProtos, - // SupportedVersions: clientHello.SupportedVersions, - // }) + if ClientHelloTelemetry { + // If no other plugin (such as the HTTP server type) is implementing ClientHello telemetry, we do it. + // NOTE: The values in the Go standard lib's ClientHelloInfo aren't guaranteed to be in order. + info := ClientHelloInfo{ + Version: clientHello.SupportedVersions[0], // report the highest + CipherSuites: clientHello.CipherSuites, + ExtensionsUnknown: true, // no extension info... :( + CompressionMethodsUnknown: true, // no compression methods... :( + Curves: clientHello.SupportedCurves, + Points: clientHello.SupportedPoints, + // We also have, but do not yet use: SignatureSchemes, ServerName, and SupportedProtos (ALPN) + // because the standard lib parses some extensions, but our MITM detector generally doesn't. + } + go telemetry.SetNested("tls_client_hello", info.Key(), info) + } + + // get the certificate and serve it up cert, err := cfg.getCertDuringHandshake(strings.ToLower(clientHello.ServerName), true, true) if err == nil { go telemetry.Increment("tls_handshake_count") // TODO: This is a "best guess" for now, we need something listener-level @@ -487,6 +485,42 @@ func (cfg *Config) renewDynamicCertificate(name string, currentCert Certificate) return cfg.getCertDuringHandshake(name, true, false) } +// ClientHelloInfo is our own version of the standard lib's +// tls.ClientHelloInfo. As of May 2018, any fields populated +// by the Go standard library are not guaranteed to have their +// values in the original order as on the wire. +type ClientHelloInfo struct { + Version uint16 `json:"version,omitempty"` + CipherSuites []uint16 `json:"cipher_suites,omitempty"` + Extensions []uint16 `json:"extensions,omitempty"` + CompressionMethods []byte `json:"compression,omitempty"` + Curves []tls.CurveID `json:"curves,omitempty"` + Points []uint8 `json:"points,omitempty"` + + // Whether a couple of fields are unknown; if not, the key will encode + // differently to reflect that, as opposed to being known empty values. + // (some fields may be unknown depending on what package is being used; + // i.e. the Go standard lib doesn't expose some things) + // (very important to NOT encode these to JSON) + ExtensionsUnknown bool `json:"-"` + CompressionMethodsUnknown bool `json:"-"` +} + +// Key returns a standardized string form of the data in info, +// useful for identifying duplicates. +func (info ClientHelloInfo) Key() string { + extensions, compressionMethods := "?", "?" + if !info.ExtensionsUnknown { + extensions = fmt.Sprintf("%x", info.Extensions) + } + if !info.CompressionMethodsUnknown { + compressionMethods = fmt.Sprintf("%x", info.CompressionMethods) + } + return fastHash([]byte(fmt.Sprintf("%x-%x-%s-%s-%x-%x", + info.Version, info.CipherSuites, extensions, + compressionMethods, info.Curves, info.Points))) +} + // obtainCertWaitChans is used to coordinate obtaining certs for each hostname. var obtainCertWaitChans = make(map[string]chan struct{}) var obtainCertWaitChansMu sync.Mutex @@ -501,3 +535,8 @@ var failedIssuanceMu sync.RWMutex // If this value is recent, do not make any on-demand certificate requests. var lastIssueTime time.Time var lastIssueTimeMu sync.Mutex + +// ClientHelloTelemetry determines whether to report +// TLS ClientHellos to telemetry. Disable if doing +// it from a different package. +var ClientHelloTelemetry = true diff --git a/telemetry/collection.go b/telemetry/collection.go index 91183f83b..38c2f89ae 100644 --- a/telemetry/collection.go +++ b/telemetry/collection.go @@ -16,6 +16,7 @@ package telemetry import ( "log" + "strings" "github.com/google/uuid" ) @@ -117,17 +118,58 @@ func Set(key string, val interface{}) { return } bufferMu.Lock() - if bufferItemCount >= maxBufferItems { - bufferMu.Unlock() - return - } if _, ok := buffer[key]; !ok { + if bufferItemCount >= maxBufferItems { + bufferMu.Unlock() + return + } bufferItemCount++ } buffer[key] = val bufferMu.Unlock() } +// SetNested puts a value in the buffer to be included +// in the next emission, nested under the top-level key +// as subkey. It overwrites any previous value. +// +// This function is safe for multiple goroutines, +// and it is recommended to call this using the +// go keyword after the call to SendHello so it +// doesn't block crucial code. +func SetNested(key, subkey string, val interface{}) { + if !enabled || isDisabled(key) { + return + } + bufferMu.Lock() + if topLevel, ok1 := buffer[key]; ok1 { + topLevelMap, ok2 := topLevel.(map[string]interface{}) + if !ok2 { + bufferMu.Unlock() + log.Printf("[PANIC] Telemetry: key %s is already used for non-nested-map value", key) + return + } + if _, ok3 := topLevelMap[subkey]; !ok3 { + // don't exceed max buffer size + if bufferItemCount >= maxBufferItems { + bufferMu.Unlock() + return + } + bufferItemCount++ + } + topLevelMap[subkey] = val + } else { + // don't exceed max buffer size + if bufferItemCount >= maxBufferItems { + bufferMu.Unlock() + return + } + bufferItemCount++ + buffer[key] = map[string]interface{}{subkey: val} + } + bufferMu.Unlock() +} + // Append appends value to a list named key. // If key is new, a new list will be created. // If key maps to a type that is not a list, @@ -161,7 +203,8 @@ func Append(key string, value interface{}) { // AppendUnique adds value to a set named key. // Set items are unordered. Values in the set // are unique, but how many times they are -// appended is counted. +// appended is counted. The value must be +// hashable. // // If key is new, a new set will be created for // values with that key. If key maps to a type @@ -238,8 +281,16 @@ func atomicAdd(key string, amount int) { // functions should call this and not // save the value if this returns true. func isDisabled(key string) bool { + // for keys that are augmented with data, such as + // "tls_client_hello_ua:", just + // check the prefix "tls_client_hello_ua" + checkKey := key + if idx := strings.Index(key, ":"); idx > -1 { + checkKey = key[:idx] + } + disabledMetricsMu.RLock() - _, ok := disabledMetrics[key] + _, ok := disabledMetrics[checkKey] disabledMetricsMu.RUnlock() return ok } From fe03c1aefa856766f9285d588d0034349444986b Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 7 May 2018 16:42:35 -0600 Subject: [PATCH 18/28] telemetry: Fix MITM tests --- caddyhttp/httpserver/mitm.go | 6 ++-- caddyhttp/httpserver/mitm_test.go | 52 +++++++++++++++++-------------- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/caddyhttp/httpserver/mitm.go b/caddyhttp/httpserver/mitm.go index 2daa26d16..6744a924e 100644 --- a/caddyhttp/httpserver/mitm.go +++ b/caddyhttp/httpserver/mitm.go @@ -67,7 +67,7 @@ func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ua := r.Header.Get("User-Agent") // report this request's UA in connection with this ClientHello - go telemetry.AppendUnique("tls_client_hello_ua:"+info.Key(), ua) + go telemetry.AppendUnique("tls_client_hello_ua:"+caddytls.ClientHelloInfo(info).Key(), ua) var checked, mitm bool if r.Header.Get("X-BlueCoat-Via") != "" || // Blue Coat (masks User-Agent header to generic values) @@ -212,7 +212,7 @@ func (c *clientHelloConn) Read(b []byte) (n int, err error) { c.listener.helloInfosMu.Unlock() // report this ClientHello to telemetry - chKey := rawParsed.Key() + chKey := caddytls.ClientHelloInfo(rawParsed).Key() go telemetry.SetNested("tls_client_hello", chKey, rawParsed) go telemetry.AppendUnique("tls_client_hello_count", chKey) @@ -374,7 +374,7 @@ func (l *tlsHelloListener) Accept() (net.Conn, error) { // by Durumeric, Halderman, et. al. in // "The Security Impact of HTTPS Interception": // https://jhalderm.com/pub/papers/interception-ndss17.pdf -type rawHelloInfo struct{ caddytls.ClientHelloInfo } +type rawHelloInfo caddytls.ClientHelloInfo // advertisesHeartbeatSupport returns true if info indicates // that the client supports the Heartbeat extension. diff --git a/caddyhttp/httpserver/mitm_test.go b/caddyhttp/httpserver/mitm_test.go index e5c2455ad..b83c6bb04 100644 --- a/caddyhttp/httpserver/mitm_test.go +++ b/caddyhttp/httpserver/mitm_test.go @@ -32,44 +32,48 @@ func TestParseClientHello(t *testing.T) { // curl 7.51.0 (x86_64-apple-darwin16.0) libcurl/7.51.0 SecureTransport zlib/1.2.8 inputHex: `010000a6030358a28c73a71bdfc1f09dee13fecdc58805dcce42ac44254df548f14645f7dc2c00004400ffc02cc02bc024c023c00ac009c008c030c02fc028c027c014c013c012009f009e006b0067003900330016009d009c003d003c0035002f000a00af00ae008d008c008b01000039000a00080006001700180019000b00020100000d00120010040102010501060104030203050306030005000501000000000012000000170000`, expected: rawHelloInfo{ - cipherSuites: []uint16{255, 49196, 49195, 49188, 49187, 49162, 49161, 49160, 49200, 49199, 49192, 49191, 49172, 49171, 49170, 159, 158, 107, 103, 57, 51, 22, 157, 156, 61, 60, 53, 47, 10, 175, 174, 141, 140, 139}, - extensions: []uint16{10, 11, 13, 5, 18, 23}, - compressionMethods: []byte{0}, - curves: []tls.CurveID{23, 24, 25}, - points: []uint8{0}, + Version: 0x303, + CipherSuites: []uint16{255, 49196, 49195, 49188, 49187, 49162, 49161, 49160, 49200, 49199, 49192, 49191, 49172, 49171, 49170, 159, 158, 107, 103, 57, 51, 22, 157, 156, 61, 60, 53, 47, 10, 175, 174, 141, 140, 139}, + Extensions: []uint16{10, 11, 13, 5, 18, 23}, + CompressionMethods: []byte{0}, + Curves: []tls.CurveID{23, 24, 25}, + Points: []uint8{0}, }, }, { // Chrome 56 inputHex: `010000c003031dae75222dae1433a5a283ddcde8ddabaefbf16d84f250eee6fdff48cdfff8a00000201a1ac02bc02fc02cc030cca9cca8cc14cc13c013c014009c009d002f0035000a010000777a7a0000ff010001000000000e000c0000096c6f63616c686f73740017000000230000000d00140012040308040401050308050501080606010201000500050100000000001200000010000e000c02683208687474702f312e3175500000000b00020100000a000a0008aaaa001d001700182a2a000100`, expected: rawHelloInfo{ - cipherSuites: []uint16{6682, 49195, 49199, 49196, 49200, 52393, 52392, 52244, 52243, 49171, 49172, 156, 157, 47, 53, 10}, - extensions: []uint16{31354, 65281, 0, 23, 35, 13, 5, 18, 16, 30032, 11, 10, 10794}, - compressionMethods: []byte{0}, - curves: []tls.CurveID{43690, 29, 23, 24}, - points: []uint8{0}, + Version: 0x303, + CipherSuites: []uint16{6682, 49195, 49199, 49196, 49200, 52393, 52392, 52244, 52243, 49171, 49172, 156, 157, 47, 53, 10}, + Extensions: []uint16{31354, 65281, 0, 23, 35, 13, 5, 18, 16, 30032, 11, 10, 10794}, + CompressionMethods: []byte{0}, + Curves: []tls.CurveID{43690, 29, 23, 24}, + Points: []uint8{0}, }, }, { // Firefox 51 inputHex: `010000bd030375f9022fc3a6562467f3540d68013b2d0b961979de6129e944efe0b35531323500001ec02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a010000760000000e000c0000096c6f63616c686f737400170000ff01000100000a000a0008001d001700180019000b00020100002300000010000e000c02683208687474702f312e31000500050100000000ff030000000d0020001e040305030603020308040805080604010501060102010402050206020202`, expected: rawHelloInfo{ - cipherSuites: []uint16{49195, 49199, 52393, 52392, 49196, 49200, 49162, 49161, 49171, 49172, 51, 57, 47, 53, 10}, - extensions: []uint16{0, 23, 65281, 10, 11, 35, 16, 5, 65283, 13}, - compressionMethods: []byte{0}, - curves: []tls.CurveID{29, 23, 24, 25}, - points: []uint8{0}, + Version: 0x303, + CipherSuites: []uint16{49195, 49199, 52393, 52392, 49196, 49200, 49162, 49161, 49171, 49172, 51, 57, 47, 53, 10}, + Extensions: []uint16{0, 23, 65281, 10, 11, 35, 16, 5, 65283, 13}, + CompressionMethods: []byte{0}, + Curves: []tls.CurveID{29, 23, 24, 25}, + Points: []uint8{0}, }, }, { // openssl s_client (OpenSSL 0.9.8zh 14 Jan 2016) inputHex: `0100012b03035d385236b8ca7b7946fa0336f164e76bf821ed90e8de26d97cc677671b6f36380000acc030c02cc028c024c014c00a00a500a300a1009f006b006a0069006800390038003700360088008700860085c032c02ec02ac026c00fc005009d003d00350084c02fc02bc027c023c013c00900a400a200a0009e00670040003f003e0033003200310030009a0099009800970045004400430042c031c02dc029c025c00ec004009c003c002f009600410007c011c007c00cc00200050004c012c008001600130010000dc00dc003000a00ff0201000055000b000403000102000a001c001a00170019001c001b0018001a0016000e000d000b000c0009000a00230000000d0020001e060106020603050105020503040104020403030103020303020102020203000f000101`, expected: rawHelloInfo{ - cipherSuites: []uint16{49200, 49196, 49192, 49188, 49172, 49162, 165, 163, 161, 159, 107, 106, 105, 104, 57, 56, 55, 54, 136, 135, 134, 133, 49202, 49198, 49194, 49190, 49167, 49157, 157, 61, 53, 132, 49199, 49195, 49191, 49187, 49171, 49161, 164, 162, 160, 158, 103, 64, 63, 62, 51, 50, 49, 48, 154, 153, 152, 151, 69, 68, 67, 66, 49201, 49197, 49193, 49189, 49166, 49156, 156, 60, 47, 150, 65, 7, 49169, 49159, 49164, 49154, 5, 4, 49170, 49160, 22, 19, 16, 13, 49165, 49155, 10, 255}, - extensions: []uint16{11, 10, 35, 13, 15}, - compressionMethods: []byte{1, 0}, - curves: []tls.CurveID{23, 25, 28, 27, 24, 26, 22, 14, 13, 11, 12, 9, 10}, - points: []uint8{0, 1, 2}, + Version: 0x303, + CipherSuites: []uint16{49200, 49196, 49192, 49188, 49172, 49162, 165, 163, 161, 159, 107, 106, 105, 104, 57, 56, 55, 54, 136, 135, 134, 133, 49202, 49198, 49194, 49190, 49167, 49157, 157, 61, 53, 132, 49199, 49195, 49191, 49187, 49171, 49161, 164, 162, 160, 158, 103, 64, 63, 62, 51, 50, 49, 48, 154, 153, 152, 151, 69, 68, 67, 66, 49201, 49197, 49193, 49189, 49166, 49156, 156, 60, 47, 150, 65, 7, 49169, 49159, 49164, 49154, 5, 4, 49170, 49160, 22, 19, 16, 13, 49165, 49155, 10, 255}, + Extensions: []uint16{11, 10, 35, 13, 15}, + CompressionMethods: []byte{1, 0}, + Curves: []tls.CurveID{23, 25, 28, 27, 24, 26, 22, 14, 13, 11, 12, 9, 10}, + Points: []uint8{0, 1, 2}, }, }, } { @@ -338,8 +342,8 @@ func TestHeuristicFunctionsAndHandler(t *testing.T) { (isEdge && (isChrome || isFirefox || isSafari || isTor)) || (isTor && (isChrome || isFirefox || isSafari || isEdge)) { t.Errorf("[%s] Test %d: Multiple fingerprinting functions matched: "+ - "Chrome=%v Firefox=%v Safari=%v Edge=%v Tor=%v\n\tparsed hello dec: %+v\n\tparsed hello hex: %#x\n", - client, i, isChrome, isFirefox, isSafari, isEdge, isTor, parsed, parsed) + "Chrome=%v Firefox=%v Safari=%v Edge=%v Tor=%v\n\tparsed hello dec: %+v\n", + client, i, isChrome, isFirefox, isSafari, isEdge, isTor, parsed) } // test the handler and detection results @@ -367,8 +371,8 @@ func TestHeuristicFunctionsAndHandler(t *testing.T) { if got != want { t.Errorf("[%s] Test %d: Expected MITM=%v but got %v (type assertion OK (checked)=%v)", client, i, want, got, checked) - t.Errorf("[%s] Test %d: Looks like Chrome=%v Firefox=%v Safari=%v Edge=%v Tor=%v\n\tparsed hello dec: %+v\n\tparsed hello hex: %#x\n", - client, i, isChrome, isFirefox, isSafari, isEdge, isTor, parsed, parsed) + t.Errorf("[%s] Test %d: Looks like Chrome=%v Firefox=%v Safari=%v Edge=%v Tor=%v\n\tparsed hello dec: %+v\n", + client, i, isChrome, isFirefox, isSafari, isEdge, isTor, parsed) } } } From ef48e17e79ad866dd31777ef5961fd98f841bae0 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 7 May 2018 17:04:39 -0600 Subject: [PATCH 19/28] caddytls: Fix tests --- caddytls/handshake.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/caddytls/handshake.go b/caddytls/handshake.go index ad190e338..8b2639845 100644 --- a/caddytls/handshake.go +++ b/caddytls/handshake.go @@ -99,7 +99,7 @@ func (cg configGroup) GetConfigForClient(clientHello *tls.ClientHelloInfo) (*tls // // This method is safe for use as a tls.Config.GetCertificate callback. func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { - if ClientHelloTelemetry { + if ClientHelloTelemetry && len(clientHello.SupportedVersions) > 0 { // If no other plugin (such as the HTTP server type) is implementing ClientHello telemetry, we do it. // NOTE: The values in the Go standard lib's ClientHelloInfo aren't guaranteed to be in order. info := ClientHelloInfo{ From b05006663f92fd481062c3ba6aba0afa1de5188e Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 8 May 2018 22:54:12 -0600 Subject: [PATCH 20/28] telemetry: Add variance to retry interval, and disable keepalive --- caddy/caddymain/run.go | 4 ++-- telemetry/collection.go | 2 +- telemetry/telemetry.go | 13 ++++++++++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/caddy/caddymain/run.go b/caddy/caddymain/run.go index 9884b0111..96eb1a1e9 100644 --- a/caddy/caddymain/run.go +++ b/caddy/caddymain/run.go @@ -388,6 +388,6 @@ var ( gitCommit string // git rev-parse HEAD gitShortStat string // git diff-index --shortstat gitFilesModified string // git diff-index --name-only HEAD - - enableTelemetry = true ) + +const enableTelemetry = true diff --git a/telemetry/collection.go b/telemetry/collection.go index 38c2f89ae..a46e2caf1 100644 --- a/telemetry/collection.go +++ b/telemetry/collection.go @@ -44,7 +44,7 @@ func Init(instanceID uuid.UUID, disabledMetricsKeys []string) { instanceUUID = instanceID disabledMetricsMu.Lock() for _, key := range disabledMetricsKeys { - disabledMetrics[key] = false + disabledMetrics[strings.TrimSpace(key)] = false } disabledMetricsMu.Unlock() enabled = true diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index 6a83b2fa2..183ee6ea7 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -38,6 +38,7 @@ import ( "fmt" "io/ioutil" "log" + "math/rand" "net/http" "strconv" "strings" @@ -202,9 +203,9 @@ func emit(final bool) error { // schedule the next update using our default update // interval because the server might be healthy later - // ensure we won't slam the telemetry server + // ensure we won't slam the telemetry server; add a little variance if reply.NextUpdate < 1*time.Second { - reply.NextUpdate = defaultUpdateInterval + reply.NextUpdate = defaultUpdateInterval + time.Duration(rand.Intn(int(1*time.Minute))) } // schedule the next update (if this wasn't the last one and @@ -345,7 +346,13 @@ func (s countingSet) MarshalJSON() ([]byte, error) { var ( // httpClient should be used for HTTP requests. It // is configured with a timeout for reliability. - httpClient = http.Client{Timeout: 1 * time.Minute} + httpClient = http.Client{ + Transport: &http.Transport{ + TLSHandshakeTimeout: 30 * time.Second, + DisableKeepAlives: true, + }, + Timeout: 1 * time.Minute, + } // buffer holds the data that we are building up to send. buffer = make(map[string]interface{}) From 86fd2f22fb0752a5c977300f224ef93e1d9cf5b1 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 9 May 2018 17:20:11 -0600 Subject: [PATCH 21/28] telemetry: Add in_container metric Knowing whether Caddy is running in a container is super-useful for debugging and troubleshooting, as well as for making development-time decisions, because Docker is one of the top contributors to our user support burden. Thanks to Eldin for helping to test it. --- caddy/caddymain/run.go | 44 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/caddy/caddymain/run.go b/caddy/caddymain/run.go index 96eb1a1e9..e684c6fe3 100644 --- a/caddy/caddymain/run.go +++ b/caddy/caddymain/run.go @@ -15,6 +15,7 @@ package caddymain import ( + "bufio" "errors" "flag" "fmt" @@ -170,6 +171,9 @@ func Run() { NumLogical: runtime.NumCPU(), AESNI: cpuid.CPU.AesNi(), }) + if containerized := detectContainer(); containerized { + telemetry.Set("in_container", containerized) + } telemetry.StartEmitting() // Twiddle your thumbs @@ -295,6 +299,46 @@ func setCPU(cpu string) error { return nil } +// detectContainer attemps to determine whether the process is +// being run inside a container. References: +// https://tuhrig.de/how-to-know-you-are-inside-a-docker-container/ +// https://stackoverflow.com/a/20012536/1048862 +// https://gist.github.com/anantkamath/623ce7f5432680749e087cf8cfba9b69 +func detectContainer() bool { + if runtime.GOOS != "linux" { + return false + } + + file, err := os.Open("/proc/1/cgroup") + if err != nil { + return false + } + defer file.Close() + + i := 0 + scanner := bufio.NewScanner(file) + for scanner.Scan() { + i++ + if i > 1000 { + return false + } + + line := scanner.Text() + parts := strings.SplitN(line, ":", 3) + if len(parts) < 3 { + continue + } + + if strings.Contains(parts[2], "docker") || + strings.Contains(parts[2], "lxc") || + strings.Contains(parts[2], "moby") { + return true + } + } + + return false +} + // initTelemetry initializes the telemetry engine. func initTelemetry() error { uuidFilename := filepath.Join(caddy.AssetsPath(), "uuid") From df7cdc3faea8e2b85172b697ef8a8f95a0cc50d7 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 9 May 2018 22:36:23 -0600 Subject: [PATCH 22/28] telemetry: Add memory and goroutine metrics, rename container And fix a typo in a comment, sigh --- caddy/caddymain/run.go | 6 +++--- telemetry/telemetry.go | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/caddy/caddymain/run.go b/caddy/caddymain/run.go index e684c6fe3..4c8adcb4f 100644 --- a/caddy/caddymain/run.go +++ b/caddy/caddymain/run.go @@ -172,7 +172,7 @@ func Run() { AESNI: cpuid.CPU.AesNi(), }) if containerized := detectContainer(); containerized { - telemetry.Set("in_container", containerized) + telemetry.Set("container", containerized) } telemetry.StartEmitting() @@ -299,7 +299,7 @@ func setCPU(cpu string) error { return nil } -// detectContainer attemps to determine whether the process is +// detectContainer attempts to determine whether the process is // being run inside a container. References: // https://tuhrig.de/how-to-know-you-are-inside-a-docker-container/ // https://stackoverflow.com/a/20012536/1048862 @@ -397,7 +397,7 @@ func initTelemetry() error { // initialize telemetry telemetry.Init(id, disabledMetricsSlice) - // if any metrics were disabled, report it + // if any metrics were disabled, report which ones (so we know how representative the data is) if len(disabledMetricsSlice) > 0 { telemetry.Set("disabled_metrics", disabledMetricsSlice) log.Printf("[NOTICE] The following telemetry metrics are disabled: %s", disabledMetrics) diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index 183ee6ea7..df29ad336 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -40,6 +40,7 @@ import ( "log" "math/rand" "net/http" + "runtime" "strconv" "strings" "sync" @@ -66,6 +67,9 @@ func emit(final bool) error { return fmt.Errorf("telemetry not enabled") } + // some metrics are updated/set at time of emission + setEmitTimeMetrics() + // ensure only one update happens at a time; // skip update if previous one still in progress updateMu.Lock() @@ -228,6 +232,17 @@ func stopUpdateTimer() { updateTimerMu.Unlock() } +// setEmitTimeMetrics sets some metrics that should +// be recorded just before emitting. +func setEmitTimeMetrics() { + Set("goroutines", runtime.NumGoroutine()) + + var mem runtime.MemStats + runtime.ReadMemStats(&mem) + SetNested("memory", "heap_alloc", mem.HeapAlloc) + SetNested("memory", "sys", mem.Sys) +} + // makePayloadAndResetBuffer prepares a payload // by emptying the collection buffer. It returns // the bytes of the payload to send to the server. From 9160789b421ae1d28c813c8ed3297cedd469722a Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 10 May 2018 08:57:25 -0600 Subject: [PATCH 23/28] telemetry: Make http_user_agent a normalized field This way we store a short 8-byte hash of the UA instead of the full string; exactly the same way we store TLS ClientHello info. --- caddyhttp/httpserver/mitm.go | 3 ++- caddyhttp/httpserver/server.go | 4 +++- caddytls/handshake.go | 2 +- telemetry/collection.go | 11 +++++++++++ 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/caddyhttp/httpserver/mitm.go b/caddyhttp/httpserver/mitm.go index 6744a924e..d2faf5f3f 100644 --- a/caddyhttp/httpserver/mitm.go +++ b/caddyhttp/httpserver/mitm.go @@ -65,9 +65,10 @@ func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.listener.helloInfosMu.RUnlock() ua := r.Header.Get("User-Agent") + uaHash := telemetry.FastHash([]byte(ua)) // report this request's UA in connection with this ClientHello - go telemetry.AppendUnique("tls_client_hello_ua:"+caddytls.ClientHelloInfo(info).Key(), ua) + go telemetry.AppendUnique("tls_client_hello_ua:"+caddytls.ClientHelloInfo(info).Key(), uaHash) var checked, mitm bool if r.Header.Get("X-BlueCoat-Via") != "" || // Blue Coat (masks User-Agent header to generic values) diff --git a/caddyhttp/httpserver/server.go b/caddyhttp/httpserver/server.go index 45f1c639d..800f921de 100644 --- a/caddyhttp/httpserver/server.go +++ b/caddyhttp/httpserver/server.go @@ -354,7 +354,9 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { if len(ua) > 512 { ua = ua[:512] } - go telemetry.AppendUnique("http_user_agent", ua) + uaHash := telemetry.FastHash([]byte(ua)) // this is a normalized field + go telemetry.SetNested("http_user_agent", uaHash, ua) + go telemetry.AppendUnique("http_user_agent_count", uaHash) go telemetry.Increment("http_request_count") // copy the original, unchanged URL into the context diff --git a/caddytls/handshake.go b/caddytls/handshake.go index 8b2639845..7e2ccb95e 100644 --- a/caddytls/handshake.go +++ b/caddytls/handshake.go @@ -516,7 +516,7 @@ func (info ClientHelloInfo) Key() string { if !info.CompressionMethodsUnknown { compressionMethods = fmt.Sprintf("%x", info.CompressionMethods) } - return fastHash([]byte(fmt.Sprintf("%x-%x-%s-%s-%x-%x", + return telemetry.FastHash([]byte(fmt.Sprintf("%x-%x-%s-%s-%x-%x", info.Version, info.CipherSuites, extensions, compressionMethods, info.Curves, info.Points))) } diff --git a/telemetry/collection.go b/telemetry/collection.go index a46e2caf1..07cf0dc9c 100644 --- a/telemetry/collection.go +++ b/telemetry/collection.go @@ -15,6 +15,8 @@ package telemetry import ( + "fmt" + "hash/fnv" "log" "strings" @@ -276,6 +278,15 @@ func atomicAdd(key string, amount int) { bufferMu.Unlock() } +// FastHash hashes input using a 32-bit hashing algorithm +// that is fast, and returns the hash as a hex-encoded string. +// Do not use this for cryptographic purposes. +func FastHash(input []byte) string { + h := fnv.New32a() + h.Write(input) + return fmt.Sprintf("%x", h.Sum32()) +} + // isDisabled returns whether key is // a disabled metric key. ALL collection // functions should call this and not From b321c00a8fe63eacb38d900da830777504f80aa0 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 10 May 2018 09:27:03 -0600 Subject: [PATCH 24/28] telemetry: Use production endpoint --- telemetry/telemetry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index df29ad336..0f3ace7f2 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -415,7 +415,7 @@ var ( const ( // endpoint is the base URL to remote telemetry server; // the instance ID will be appended to it. - endpoint = "https://telemetry-staging.caddyserver.com/v1/update/" + endpoint = "https://telemetry.caddyserver.com/v1/update/" // defaultUpdateInterval is how long to wait before emitting // more telemetry data if all retires fail. This value is From c667f8186649da6143b14e2f50cbd753d7a78ea0 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 10 May 2018 09:41:57 -0600 Subject: [PATCH 25/28] telemetry: Use int64 constant for duration interval Otherwise it overflows int type on 32-bit builds --- telemetry/telemetry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index 0f3ace7f2..59ab75be2 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -209,7 +209,7 @@ func emit(final bool) error { // ensure we won't slam the telemetry server; add a little variance if reply.NextUpdate < 1*time.Second { - reply.NextUpdate = defaultUpdateInterval + time.Duration(rand.Intn(int(1*time.Minute))) + reply.NextUpdate = defaultUpdateInterval + time.Duration(rand.Int63n(int64(1*time.Minute))) } // schedule the next update (if this wasn't the last one and From 1f7b5abc80679fb71ee0e04ed98cbe284b1fc181 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 10 May 2018 09:42:07 -0600 Subject: [PATCH 26/28] Version 0.11 --- dist/CHANGES.txt | 6 ++++++ dist/README.txt | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/dist/CHANGES.txt b/dist/CHANGES.txt index 15e154854..c1d8139f5 100644 --- a/dist/CHANGES.txt +++ b/dist/CHANGES.txt @@ -1,5 +1,11 @@ CHANGES +0.11 (May 10, 2018) +- Built with Go 1.10.2 +- Integrated optional telemetry client +- proxy: Fixed file descriptor leak + + 0.10.14 (April 19, 2018) - tls: Fix error handling bug when obtaining certificates diff --git a/dist/README.txt b/dist/README.txt index 1457bc98b..2c67bc9f6 100644 --- a/dist/README.txt +++ b/dist/README.txt @@ -1,4 +1,4 @@ -CADDY 0.10.14 +CADDY 0.11 Website https://caddyserver.com From 13268db5361dd73d5504a7704c60d76d4755a71f Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 10 May 2018 11:31:31 -0600 Subject: [PATCH 27/28] Update readme with regards to telemetry --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8655205b9..c6455d7c4 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,8 @@ Then make sure the `caddy` binary is in your PATH. To build for other platforms, use build.go with the `--goos` and `--goarch` flags. +When building from source, telemetry is enabled by default. You can disable it by changing `enableTelemetry` in run.go before compiling, or use the `-disabled-metrics` flag at runtime to disable only certain metrics. + ## Quick Start From f058419042c720f6634428c0ee7e4705efa9b732 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 15 May 2018 19:39:15 -0600 Subject: [PATCH 28/28] Change UUID file with CADDY_UUID_FILE environment variable --- caddy/caddymain/run.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/caddy/caddymain/run.go b/caddy/caddymain/run.go index 4c8adcb4f..9a19ed05b 100644 --- a/caddy/caddymain/run.go +++ b/caddy/caddymain/run.go @@ -342,6 +342,9 @@ func detectContainer() bool { // initTelemetry initializes the telemetry engine. func initTelemetry() error { uuidFilename := filepath.Join(caddy.AssetsPath(), "uuid") + if customUUIDFile := os.Getenv("CADDY_UUID_FILE"); customUUIDFile != "" { + uuidFilename = customUUIDFile + } newUUID := func() uuid.UUID { id := uuid.New()