...

Source file src/github.com/google/certificate-transparency-go/trillian/integration/ct_hammer/main.go

Documentation: github.com/google/certificate-transparency-go/trillian/integration/ct_hammer

     1  // Copyright 2017 Google LLC. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // ct_hammer is a stress/load test for a CT log.
    16  package main
    17  
    18  import (
    19  	"bytes"
    20  	"compress/gzip"
    21  	"context"
    22  	"crypto/tls"
    23  	"encoding/base64"
    24  	"flag"
    25  	"fmt"
    26  	"io"
    27  	"net/http"
    28  	"os"
    29  	"strings"
    30  	"sync"
    31  	"time"
    32  
    33  	"github.com/google/certificate-transparency-go/client"
    34  	"github.com/google/certificate-transparency-go/jsonclient"
    35  	"github.com/google/certificate-transparency-go/loglist3"
    36  	"github.com/google/certificate-transparency-go/trillian/ctfe"
    37  	"github.com/google/certificate-transparency-go/trillian/ctfe/configpb"
    38  	"github.com/google/certificate-transparency-go/trillian/integration"
    39  	"github.com/google/certificate-transparency-go/x509util"
    40  	"github.com/google/trillian/monitoring"
    41  	"github.com/google/trillian/monitoring/prometheus"
    42  	"github.com/prometheus/client_golang/prometheus/promhttp"
    43  	"golang.org/x/time/rate"
    44  	"k8s.io/klog/v2"
    45  )
    46  
    47  var (
    48  	banner      = flag.Bool("banner", true, "Display intro")
    49  	httpServers = flag.String("ct_http_servers", "localhost:8092", "Comma-separated list of (assumed interchangeable) servers, each as address:port")
    50  
    51  	// Options for synthetic cert generation.
    52  	testDir      = flag.String("testdata_dir", "testdata", "Name of directory with test data")
    53  	leafNotAfter = flag.String("leaf_not_after", "", "Not-After date to use for leaf certs, RFC3339/ISO-8601 format (e.g. 2017-11-26T12:29:19Z)")
    54  	// Options for copied-cert generation.
    55  	srcLogURI       = flag.String("src_log_uri", "", "URI for source log to copy certificates from")
    56  	srcPubKey       = flag.String("src_pub_key", "", "Name of file containing source log's public key")
    57  	srcLogName      = flag.String("src_log_name", "", "Name of source log to copy certificate from  (from --log_list)")
    58  	logList         = flag.String("log_list", loglist3.AllLogListURL, "Location of master log list (URL or filename)")
    59  	skipHTTPSVerify = flag.Bool("skip_https_verify", false, "Skip verification of HTTPS transport connection to source log")
    60  	chainBufSize    = flag.Int("buffered_chains", 100, "Number of buffered certificate chains to hold")
    61  	startIndex      = flag.Int64("start_index", 0, "Index of start point in source log to scan from (-1 for random start index)")
    62  	batchSize       = flag.Int("batch_size", 500, "Max number of entries to request at per call to get-entries")
    63  	parallelFetch   = flag.Int("parallel_fetch", 2, "Number of concurrent GetEntries fetches")
    64  
    65  	metricsEndpoint     = flag.String("metrics_endpoint", "", "Endpoint for serving metrics; if left empty, metrics will not be exposed")
    66  	logConfig           = flag.String("log_config", "", "File holding log config in JSON")
    67  	mmd                 = flag.Duration("mmd", 2*time.Minute, "Default MMD for logs")
    68  	operations          = flag.Uint64("operations", ^uint64(0), "Number of operations to perform")
    69  	minGetEntries       = flag.Int("min_get_entries", 1, "Minimum get-entries request size")
    70  	maxGetEntries       = flag.Int("max_get_entries", 500, "Maximum get-entries request size")
    71  	oversizedGetEntries = flag.Bool("oversized_get_entries", false, "Whether get-entries requests can go beyond log size")
    72  	maxParallelChains   = flag.Int("max_parallel_chains", 2, "Maximum number of chains to add in parallel (will always add at least 1 chain)")
    73  	limit               = flag.Int("rate_limit", 0, "Maximum rate of requests to an individual log; 0 for no rate limit")
    74  	ignoreErrors        = flag.Bool("ignore_errors", false, "Whether to ignore errors and retry the operation")
    75  	maxRetry            = flag.Duration("max_retry", 60*time.Second, "How long to keep retrying when ignore_errors is set")
    76  	reqDeadline         = flag.Duration("req_deadline", 10*time.Second, "Deadline to set on individual requests")
    77  )
    78  var (
    79  	addChainBias             = flag.Int("add_chain", 20, "Bias for add-chain operations")
    80  	addPreChainBias          = flag.Int("add_pre_chain", 20, "Bias for add-pre-chain operations")
    81  	getSTHBias               = flag.Int("get_sth", 2, "Bias for get-sth operations")
    82  	getSTHConsistencyBias    = flag.Int("get_sth_consistency", 2, "Bias for get-sth-consistency operations")
    83  	getProofByHashBias       = flag.Int("get_proof_by_hash", 2, "Bias for get-proof-by-hash operations")
    84  	getEntriesBias           = flag.Int("get_entries", 2, "Bias for get-entries operations")
    85  	getRootsBias             = flag.Int("get_roots", 1, "Bias for get-roots operations")
    86  	getEntryAndProofBias     = flag.Int("get_entry_and_proof", 0, "Bias for get-entry-and-proof operations")
    87  	invalidChance            = flag.Int("invalid_chance", 10, "Chance of generating an invalid operation, as the N in 1-in-N (0 for never)")
    88  	dupeChance               = flag.Int("duplicate_chance", 10, "Chance of generating a duplicate submission, as the N in 1-in-N (0 for never)")
    89  	strictSTHConsistencySize = flag.Bool("strict_sth_consistency_size", true, "If set to true, hammer will use only tree sizes from STHs it's seen for consistency proofs, otherwise it'll choose a random size for the smaller tree")
    90  )
    91  
    92  func newLimiter(limit int) integration.Limiter {
    93  	if limit <= 0 {
    94  		return nil
    95  	}
    96  	return rate.NewLimiter(rate.Limit(limit), 1)
    97  }
    98  
    99  // copierGeneratorFactory returns a function that creates per-Log ChainGenerator instances
   100  // that are based off a source CT log specified by the command line arguments.
   101  func copierGeneratorFactory(ctx context.Context) integration.GeneratorFactory {
   102  	var tlsCfg *tls.Config
   103  	if *skipHTTPSVerify {
   104  		klog.Warning("Skipping HTTPS connection verification")
   105  		tlsCfg = &tls.Config{InsecureSkipVerify: *skipHTTPSVerify}
   106  	}
   107  	httpClient := &http.Client{
   108  		Timeout: 60 * time.Second,
   109  		Transport: &http.Transport{
   110  			TLSHandshakeTimeout:   30 * time.Second,
   111  			ResponseHeaderTimeout: 30 * time.Second,
   112  			MaxIdleConnsPerHost:   10,
   113  			DisableKeepAlives:     false,
   114  			MaxIdleConns:          100,
   115  			IdleConnTimeout:       90 * time.Second,
   116  			ExpectContinueTimeout: 1 * time.Second,
   117  			TLSClientConfig:       tlsCfg,
   118  		},
   119  	}
   120  	uri := *srcLogURI
   121  	var opts jsonclient.Options
   122  	if *srcPubKey != "" {
   123  		pubkey, err := os.ReadFile(*srcPubKey)
   124  		if err != nil {
   125  			klog.Exit(err)
   126  		}
   127  		opts.PublicKey = string(pubkey)
   128  	}
   129  	if len(*srcLogName) > 0 {
   130  		llData, err := x509util.ReadFileOrURL(*logList, httpClient)
   131  		if err != nil {
   132  			klog.Exitf("Failed to read log list: %v", err)
   133  		}
   134  		ll, err := loglist3.NewFromJSON(llData)
   135  		if err != nil {
   136  			klog.Exitf("Failed to build log list: %v", err)
   137  		}
   138  
   139  		logs := ll.FindLogByName(*srcLogName)
   140  		if len(logs) == 0 {
   141  			klog.Exitf("No log with name like %q found in loglist %q", *srcLogName, *logList)
   142  		}
   143  		if len(logs) > 1 {
   144  			logNames := make([]string, len(logs))
   145  			for i, log := range logs {
   146  				logNames[i] = fmt.Sprintf("%q", log.Description)
   147  			}
   148  			klog.Exitf("Multiple logs with name like %q found in loglist: %s", *srcLogName, strings.Join(logNames, ","))
   149  		}
   150  		uri = "https://" + logs[0].URL
   151  		if opts.PublicKey == "" {
   152  			opts.PublicKeyDER = logs[0].Key
   153  		}
   154  	}
   155  
   156  	logClient, err := client.New(uri, httpClient, opts)
   157  	if err != nil {
   158  		klog.Exitf("Failed to create client for %q: %v", uri, err)
   159  	}
   160  	klog.Infof("Testing with certs copied from log at %s starting at index %d", uri, *startIndex)
   161  	genOpts := integration.CopyChainOptions{
   162  		StartIndex:    *startIndex,
   163  		BufSize:       *chainBufSize,
   164  		BatchSize:     *batchSize,
   165  		ParallelFetch: *parallelFetch,
   166  	}
   167  	return func(c *configpb.LogConfig) (integration.ChainGenerator, error) {
   168  		return integration.NewCopyChainGeneratorFromOpts(ctx, logClient, c, genOpts)
   169  	}
   170  }
   171  
   172  func main() {
   173  	klog.InitFlags(nil)
   174  	flag.Parse()
   175  	if *logConfig == "" {
   176  		klog.Exit("Test aborted as no log config provided (via --log_config)")
   177  	}
   178  
   179  	cfg, err := ctfe.LogConfigFromFile(*logConfig)
   180  	if err != nil {
   181  		klog.Exitf("Failed to read log config: %v", err)
   182  	}
   183  	ctx, cancel := context.WithCancel(context.Background())
   184  	defer cancel()
   185  
   186  	var generatorFactory integration.GeneratorFactory
   187  	if len(*srcLogURI) > 0 || len(*srcLogName) > 0 {
   188  		// Test cert chains will be generated by copying from a source log.
   189  		generatorFactory = copierGeneratorFactory(ctx)
   190  	} else if *testDir != "" {
   191  		// Test cert chains will be generated as synthetic certs from a template.
   192  		// Retrieve the test data holding the template and key.
   193  		klog.Infof("Testing with synthetic certs based on data from %s", *testDir)
   194  		generatorFactory, err = integration.SyntheticGeneratorFactory(*testDir, *leafNotAfter)
   195  		if err != nil {
   196  			klog.Exitf("Failed to make cert generator: %v", err)
   197  		}
   198  	}
   199  
   200  	if generatorFactory == nil {
   201  		klog.Warningf("Warning: add-[pre-]chain operations disabled as no cert generation method available")
   202  		*addChainBias = 0
   203  		*addPreChainBias = 0
   204  		generatorFactory = func(c *configpb.LogConfig) (integration.ChainGenerator, error) {
   205  			return nil, nil
   206  		}
   207  	}
   208  
   209  	bias := integration.HammerBias{
   210  		Bias: map[ctfe.EntrypointName]int{
   211  			ctfe.AddChainName:          *addChainBias,
   212  			ctfe.AddPreChainName:       *addPreChainBias,
   213  			ctfe.GetSTHName:            *getSTHBias,
   214  			ctfe.GetSTHConsistencyName: *getSTHConsistencyBias,
   215  			ctfe.GetProofByHashName:    *getProofByHashBias,
   216  			ctfe.GetEntriesName:        *getEntriesBias,
   217  			ctfe.GetRootsName:          *getRootsBias,
   218  			ctfe.GetEntryAndProofName:  *getEntryAndProofBias,
   219  		},
   220  		InvalidChance: map[ctfe.EntrypointName]int{
   221  			ctfe.AddChainName:          *invalidChance,
   222  			ctfe.AddPreChainName:       *invalidChance,
   223  			ctfe.GetSTHName:            0,
   224  			ctfe.GetSTHConsistencyName: *invalidChance,
   225  			ctfe.GetProofByHashName:    *invalidChance,
   226  			ctfe.GetEntriesName:        *invalidChance,
   227  			ctfe.GetRootsName:          0,
   228  			ctfe.GetEntryAndProofName:  0,
   229  		},
   230  	}
   231  
   232  	var mf monitoring.MetricFactory
   233  	if *metricsEndpoint != "" {
   234  		mf = prometheus.MetricFactory{}
   235  		http.Handle("/metrics", promhttp.Handler())
   236  		server := http.Server{Addr: *metricsEndpoint, Handler: nil}
   237  		klog.Infof("Serving metrics at %v", *metricsEndpoint)
   238  		go func() {
   239  			err := server.ListenAndServe()
   240  			klog.Warningf("Metrics server exited: %v", err)
   241  		}()
   242  	} else {
   243  		mf = monitoring.InertMetricFactory{}
   244  	}
   245  
   246  	if *banner {
   247  		fmt.Print("\n\nStop")
   248  		for i := 0; i < 8; i++ {
   249  			time.Sleep(100 * time.Millisecond)
   250  			fmt.Print(".")
   251  		}
   252  		mc := "H4sIAAAAAAAA/4xVPbLzMAjsv1OkU8FI9LqDOAUFDUNBxe2/QXYSS/HLe5SeXZYfsf73+D1KB8D2B2RxZpGw8gcsSoQYeH1ya0fof1BpnhpuUR+P8ijorESq8Yto6WYWqsrMGh4qSkdI/YFZWu8d3AAAkklEHBGTNAYxbpKltWRgRzQ3A3CImDIjVSVCicThbLK0VjsiAGAGIIKbmUcIq/KkqYo4BNZDqtgZMAPNPSJCRISZZ36d5OiTUbqJZAOYIoCHUreImJsCPMobQ20SqjBbLWWbBGRREhHQU2MMUu9TwB12cC7X3SNrs1yPKvv5gD4yn+kzshOfMg69fVknJNbdcsjuDvgNXWPmTXCuEnuvP4NdlSWymIQjfsFWzbERZ5sz730NpbvoOGMOzu7eeBUaW3w8r4z2iRuD4uY6W9wgZ96+YZvpHW7SabvlH7CviKWQyp81EL2zj7Fcbee7MpSuNHzj2z18LdAvAkAr8pr/3cGFUO+apa2n64TK3XouTBpEch2Rf8GnzajAFY438+SzgURfV7sXT+q1FNTJYdLF9WxJzFheAyNmXfKuiel5/mW2QqSx2umlQ+L2GpTPWZBu5tvpXW5/fy4xTYd2ly+vR052dZbjTIh0u4vzyRDF6kPzoRLRfhp2pqnr5wce5eAGP6onaRv8EYdl7gfd5zIId/gxYvr4pWW7KnbjoU6kRL62e25b44ZQz7Oaf4GrTovnqemNsyOdL40Dls11ocMPn29nYeUvmt3S1v8DAAD//wEAAP//TRo+KHEIAAA="
   253  		mcData, _ := base64.StdEncoding.DecodeString(mc)
   254  		b := bytes.NewReader(mcData)
   255  		r, _ := gzip.NewReader(b)
   256  		if _, err := io.Copy(os.Stdout, r); err != nil {
   257  			klog.Exitf("Failed to print banner!")
   258  		}
   259  		r.Close()
   260  		fmt.Print("\n\nHammer Time\n\n")
   261  	}
   262  
   263  	type result struct {
   264  		prefix string
   265  		err    error
   266  	}
   267  	results := make(chan result, len(cfg))
   268  	var wg sync.WaitGroup
   269  	for _, c := range cfg {
   270  		wg.Add(1)
   271  		pool, err := integration.NewRandomPool(*httpServers, c.PublicKey, c.Prefix)
   272  		if err != nil {
   273  			klog.Exitf("Failed to create client pool: %v", err)
   274  		}
   275  
   276  		mmd := *mmd
   277  		// Note: Although the (usually lower than MMD) expected merge delay is not
   278  		// a guarantee, it should be OK for testing.
   279  		if emd := c.ExpectedMergeDelaySec; emd != 0 {
   280  			mmd = time.Second * time.Duration(emd)
   281  		}
   282  
   283  		generator, err := generatorFactory(c)
   284  		if err != nil {
   285  			klog.Exitf("Failed to build chain generator: %v", err)
   286  		}
   287  
   288  		cfg := integration.HammerConfig{
   289  			LogCfg:                   c,
   290  			MetricFactory:            mf,
   291  			MMD:                      mmd,
   292  			ChainGenerator:           generator,
   293  			ClientPool:               pool,
   294  			EPBias:                   bias,
   295  			MinGetEntries:            *minGetEntries,
   296  			MaxGetEntries:            *maxGetEntries,
   297  			OversizedGetEntries:      *oversizedGetEntries,
   298  			Operations:               *operations,
   299  			Limiter:                  newLimiter(*limit),
   300  			MaxParallelChains:        *maxParallelChains,
   301  			IgnoreErrors:             *ignoreErrors,
   302  			MaxRetryDuration:         *maxRetry,
   303  			RequestDeadline:          *reqDeadline,
   304  			DuplicateChance:          *dupeChance,
   305  			StrictSTHConsistencySize: *strictSTHConsistencySize,
   306  		}
   307  		go func(cfg integration.HammerConfig) {
   308  			defer wg.Done()
   309  			err := integration.HammerCTLog(ctx, cfg)
   310  			results <- result{prefix: cfg.LogCfg.Prefix, err: err}
   311  		}(cfg)
   312  	}
   313  	wg.Wait()
   314  
   315  	klog.Infof("completed tests on all %d logs:", len(cfg))
   316  	close(results)
   317  	errCount := 0
   318  	for e := range results {
   319  		if e.err != nil {
   320  			errCount++
   321  			klog.Errorf("  %s: failed with %v", e.prefix, e.err)
   322  		}
   323  	}
   324  	if errCount > 0 {
   325  		klog.Exitf("non-zero error count (%d), exiting", errCount)
   326  	}
   327  	klog.Info("  no errors; done")
   328  }
   329  

View as plain text