mirror of
https://github.com/tladesignz/dnstt.git
synced 2026-09-25 08:17:58 +03:00
-utls option and random TLS fingerprint selection.
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
-doh and -dot mode use uTLS to camouflage their TLS Client Hello
|
||||
fingerprint. This change means that it is no longer possible to use a
|
||||
proxy in -doh mode by setting the HTTP_PROXY or HTTPS_PROXY environment
|
||||
fingerprint. The fingerprint to use is chosen randomly from a weighted
|
||||
distribution. You can control this distribution using the new -utls
|
||||
option. This change means that it is no longer possible to use a proxy
|
||||
in -doh mode by setting the HTTP_PROXY or HTTPS_PROXY environment
|
||||
variables; this was an undocumented side effect of using the Go net/http
|
||||
package with no TLS camouflage.
|
||||
|
||||
|
||||
@@ -284,7 +284,18 @@ tunnel server is acting as a proxy, for example), unless that data has
|
||||
been separately encrypted before being sent through the tunnel.
|
||||
|
||||
dnstt-client disguises its TLS fingerprint using uTLS
|
||||
(https://github.com/refraction-networking/utls).
|
||||
(https://github.com/refraction-networking/utls). By default, a specific
|
||||
TLS Client Hello fingerprint is selected randomly from a weighted
|
||||
distribution. You can control the distribution of fingerprints (or
|
||||
select a specific single fingerprint) using the `-utls` option. The
|
||||
syntax of the option's argument is a comma-separated list of fingerprint
|
||||
names, each optionally preceded by an integer weight and `*`.
|
||||
```
|
||||
$ ./dnstt-client -utls '3*Firefox,2*Chrome,1*iOS' ...
|
||||
$ ./dnstt-client -utls Firefox ...
|
||||
```
|
||||
Run `./dnstt-client -help` to see the available fingerprint names and
|
||||
the default distribution.
|
||||
|
||||
|
||||
## Encryption and authentication
|
||||
|
||||
+15
-12
@@ -18,13 +18,6 @@ import (
|
||||
// header in an HTTP response.
|
||||
const defaultRetryAfter = 10 * time.Second
|
||||
|
||||
// The *http.Client shared by instances of HTTPPacketConn. We use this instead
|
||||
// of http.DefaultClient in order to set a timeout and a uTLS fingerprint.
|
||||
var httpClient = &http.Client{
|
||||
Transport: NewUTLSRoundTripper(nil, utlsClientHelloID),
|
||||
Timeout: 1 * time.Minute,
|
||||
}
|
||||
|
||||
// HTTPPacketConn is an HTTP-based transport for DNS messages, used for DNS over
|
||||
// HTTPS (DoH). Its WriteTo and ReadFrom methods exchange DNS messages over HTTP
|
||||
// requests and responses.
|
||||
@@ -35,6 +28,11 @@ var httpClient = &http.Client{
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc8484
|
||||
type HTTPPacketConn struct {
|
||||
// client is the http.Client used to make requests. We use this instead
|
||||
// of http.DefaultClient in order to support setting a timeout and a
|
||||
// uTLS fingerprint.
|
||||
client *http.Client
|
||||
|
||||
// urlString is the URL to which HTTP requests will be sent, for example
|
||||
// "https://doh.example/dns-query".
|
||||
urlString string
|
||||
@@ -57,11 +55,16 @@ type HTTPPacketConn struct {
|
||||
}
|
||||
|
||||
// NewHTTPPacketConn creates a new HTTPPacketConn configured to use the HTTP
|
||||
// server at urlString as a DNS over HTTP resolver. urlString should include any
|
||||
// necessary path components; e.g., "/dns-query". numSenders is the number of
|
||||
// concurrent sender-receiver goroutines to run.
|
||||
func NewHTTPPacketConn(urlString string, numSenders int) (*HTTPPacketConn, error) {
|
||||
// server at urlString as a DNS over HTTP resolver. client is the http.Client
|
||||
// that will be used to make requests. urlString should include any necessary
|
||||
// path components; e.g., "/dns-query". numSenders is the number of concurrent
|
||||
// sender-receiver goroutines to run.
|
||||
func NewHTTPPacketConn(rt http.RoundTripper, urlString string, numSenders int) (*HTTPPacketConn, error) {
|
||||
c := &HTTPPacketConn{
|
||||
client: &http.Client{
|
||||
Transport: rt,
|
||||
Timeout: 1 * time.Minute,
|
||||
},
|
||||
urlString: urlString,
|
||||
QueuePacketConn: turbotunnel.NewQueuePacketConn(turbotunnel.DummyAddr{}, 0),
|
||||
}
|
||||
@@ -81,7 +84,7 @@ func (c *HTTPPacketConn) send(p []byte) error {
|
||||
req.Header.Set("Accept", "application/dns-message")
|
||||
req.Header.Set("Content-Type", "application/dns-message")
|
||||
req.Header.Set("User-Agent", "") // Disable default "Go-http-client/1.1".
|
||||
resp, err := httpClient.Do(req)
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+65
-5
@@ -23,9 +23,17 @@
|
||||
//
|
||||
// LOCALADDR is the TCP address that will listen for connections and forward
|
||||
// them over the tunnel.
|
||||
//
|
||||
// In -doh and -dot modes, the program's TLS fingerprint is camouflaged with
|
||||
// uTLS. By default, the specific TLS fingerprint is selected randomly from a
|
||||
// weighted distribution. You can set your own distribution (or specific single
|
||||
// fingerprint) using the -utls option:
|
||||
// -utls '3*Firefox,2*Chrome,1*iOS'
|
||||
// -utls Firefox
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
@@ -33,6 +41,7 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -47,9 +56,6 @@ import (
|
||||
// smux streams will be closed after this much time without receiving data.
|
||||
const idleTimeout = 2 * time.Minute
|
||||
|
||||
// uTLS Client Hello fingerprint to use in all TLS connections.
|
||||
var utlsClientHelloID = &utls.HelloFirefox_Auto
|
||||
|
||||
// dnsNameCapacity returns the number of bytes remaining for encoded data after
|
||||
// including domain in a DNS name.
|
||||
func dnsNameCapacity(domain dns.Name) int {
|
||||
@@ -80,6 +86,26 @@ func readKeyFromFile(filename string) ([]byte, error) {
|
||||
return noise.ReadKey(f)
|
||||
}
|
||||
|
||||
// sampleUTLSDistribution parses a weighted uTLS Client Hello ID distribution
|
||||
// string of the form "3*Firefox,2*Chrome,1*iOS", matches each label to a
|
||||
// utls.ClientHelloID from utlsClientHelloIDMap, and randomly samples one
|
||||
// utls.ClientHelloID from the distribution.
|
||||
func sampleUTLSDistribution(spec string) (*utls.ClientHelloID, error) {
|
||||
weights, labels, err := parseWeightedList(spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := make([]*utls.ClientHelloID, 0, len(labels))
|
||||
for _, label := range labels {
|
||||
id := utlsLookup(label)
|
||||
if id == nil {
|
||||
return nil, fmt.Errorf("unknown TLS fingerprint %q", label)
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids[sampleWeighted(weights)], nil
|
||||
}
|
||||
|
||||
func handle(local *net.TCPConn, sess *smux.Session, conv uint32) error {
|
||||
stream, err := sess.OpenStream()
|
||||
if err != nil {
|
||||
@@ -204,6 +230,7 @@ func main() {
|
||||
var pubkeyFilename string
|
||||
var pubkeyString string
|
||||
var udpAddr string
|
||||
var utlsDistribution string
|
||||
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(flag.CommandLine.Output(), `Usage:
|
||||
@@ -215,12 +242,35 @@ Examples:
|
||||
|
||||
`, os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
labels := make([]string, 0, len(utlsClientHelloIDMap))
|
||||
for _, entry := range utlsClientHelloIDMap {
|
||||
labels = append(labels, entry.Label)
|
||||
}
|
||||
fmt.Fprintf(flag.CommandLine.Output(), `
|
||||
Known TLS fingerprints for -utls are:
|
||||
`)
|
||||
i := 0
|
||||
for i < len(labels) {
|
||||
var line strings.Builder
|
||||
fmt.Fprintf(&line, " %s", labels[i])
|
||||
w := 2 + len(labels[i])
|
||||
i++
|
||||
for i < len(labels) && w+1+len(labels[i]) <= 72 {
|
||||
fmt.Fprintf(&line, " %s", labels[i])
|
||||
w += 1 + len(labels[i])
|
||||
i++
|
||||
}
|
||||
fmt.Fprintln(flag.CommandLine.Output(), line.String())
|
||||
}
|
||||
}
|
||||
flag.StringVar(&dohURL, "doh", "", "URL of DoH resolver")
|
||||
flag.StringVar(&dotAddr, "dot", "", "address of DoT resolver")
|
||||
flag.StringVar(&pubkeyString, "pubkey", "", fmt.Sprintf("server public key (%d hex digits)", noise.KeyLen*2))
|
||||
flag.StringVar(&pubkeyFilename, "pubkey-file", "", "read server public key from file")
|
||||
flag.StringVar(&udpAddr, "udp", "", "address of UDP DNS resolver")
|
||||
flag.StringVar(&utlsDistribution, "utls",
|
||||
"3*Firefox_65,1*Firefox_63,3*Chrome_83,1*Chrome_72,1*iOS_12_1",
|
||||
"choose TLS fingerprint from weighted distribution")
|
||||
flag.Parse()
|
||||
|
||||
log.SetFlags(log.LstdFlags | log.LUTC)
|
||||
@@ -264,6 +314,13 @@ Examples:
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
utlsClientHelloID, err := sampleUTLSDistribution(utlsDistribution)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "parsing -utls: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Printf("uTLS fingerprint %s %s", utlsClientHelloID.Client, utlsClientHelloID.Version)
|
||||
|
||||
// Iterate over the remote resolver address options and select one and
|
||||
// only one.
|
||||
var remoteAddr net.Addr
|
||||
@@ -275,13 +332,16 @@ Examples:
|
||||
// -doh
|
||||
{dohURL, func(s string) (net.Addr, net.PacketConn, error) {
|
||||
addr := turbotunnel.DummyAddr{}
|
||||
pconn, err := NewHTTPPacketConn(dohURL, 32)
|
||||
pconn, err := NewHTTPPacketConn(NewUTLSRoundTripper(nil, utlsClientHelloID), dohURL, 32)
|
||||
return addr, pconn, err
|
||||
}},
|
||||
// -dot
|
||||
{dotAddr, func(s string) (net.Addr, net.PacketConn, error) {
|
||||
addr := turbotunnel.DummyAddr{}
|
||||
pconn, err := NewTLSPacketConn(dotAddr)
|
||||
dialTLSContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return utlsDialContext(ctx, network, addr, nil, utlsClientHelloID)
|
||||
}
|
||||
pconn, err := NewTLSPacketConn(dotAddr, dialTLSContext)
|
||||
return addr, pconn, err
|
||||
}},
|
||||
// -udp
|
||||
|
||||
+3
-4
@@ -10,7 +10,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
utls "github.com/refraction-networking/utls"
|
||||
"www.bamsoftware.com/git/dnstt.git/turbotunnel"
|
||||
)
|
||||
|
||||
@@ -37,11 +36,11 @@ type TLSPacketConn struct {
|
||||
// server at addr as a DNS over TLS resolver. It maintains a TLS connection to
|
||||
// the resolver, reconnecting as necessary. It closes the connection if any
|
||||
// reconnection attempt fails.
|
||||
func NewTLSPacketConn(addr string) (*TLSPacketConn, error) {
|
||||
dial := func() (*utls.UConn, error) {
|
||||
func NewTLSPacketConn(addr string, dialTLSContext func(ctx context.Context, network, addr string) (net.Conn, error)) (*TLSPacketConn, error) {
|
||||
dial := func() (net.Conn, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dialTimeout)
|
||||
defer cancel()
|
||||
return utlsDialContext(ctx, "tcp", addr, nil, utlsClientHelloID)
|
||||
return dialTLSContext(ctx, "tcp", addr)
|
||||
}
|
||||
// We maintain one TLS connection at a time, redialing it whenever it
|
||||
// becomes disconnected. We do the first dial here, outside the
|
||||
|
||||
@@ -9,12 +9,46 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
utls "github.com/refraction-networking/utls"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
// utlsClientHelloIDMap is a correspondence between human-readable labels and
|
||||
// supported utls.ClientHelloIDs.
|
||||
var utlsClientHelloIDMap = []struct {
|
||||
Label string
|
||||
ID *utls.ClientHelloID
|
||||
}{
|
||||
{"Firefox", &utls.HelloFirefox_Auto},
|
||||
{"Firefox_55", &utls.HelloFirefox_55},
|
||||
{"Firefox_56", &utls.HelloFirefox_56},
|
||||
{"Firefox_63", &utls.HelloFirefox_63},
|
||||
{"Firefox_65", &utls.HelloFirefox_65},
|
||||
{"Chrome", &utls.HelloChrome_Auto},
|
||||
{"Chrome_58", &utls.HelloChrome_58},
|
||||
{"Chrome_62", &utls.HelloChrome_62},
|
||||
{"Chrome_70", &utls.HelloChrome_70},
|
||||
{"Chrome_72", &utls.HelloChrome_72},
|
||||
{"Chrome_83", &utls.HelloChrome_83},
|
||||
{"iOS", &utls.HelloIOS_Auto},
|
||||
{"iOS_11_1", &utls.HelloIOS_11_1},
|
||||
{"iOS_12_1", &utls.HelloIOS_12_1},
|
||||
}
|
||||
|
||||
// utlsLookup returns a *utls.ClientHelloID from utlsClientHelloIDMap by a
|
||||
// case-insensitive label match, or nil if there is no match.
|
||||
func utlsLookup(label string) *utls.ClientHelloID {
|
||||
for _, entry := range utlsClientHelloIDMap {
|
||||
if strings.ToLower(label) == strings.ToLower(entry.Label) {
|
||||
return entry.ID
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// utlsDialContext connects to the given network address and initiates a TLS
|
||||
// handshake with the provided ClientHelloID, and returns the resulting TLS
|
||||
// connection.
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
package main
|
||||
|
||||
// Random selection from weighted distributions, and strings for specifying such
|
||||
// distributions.
|
||||
|
||||
import (
|
||||
cryptorand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
mathrand "math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// parseWeightedList parses a list of text labels with optional numeric weights,
|
||||
// and returns parallel slices of weights and labels. If a weight is omitted for
|
||||
// a label, the weight is 1.
|
||||
//
|
||||
// An example weighted list string is "2*apple,orange,10*cookie". This example
|
||||
// results in the slices [2, 1, 10] and ["apple", "orange", "cookie"].
|
||||
// Bytes may be escaped by backslashes.
|
||||
//
|
||||
// list ::= entry [ "," entry ] ;
|
||||
// entry ::= [ weight, "*" ] label ;
|
||||
func parseWeightedList(s string) ([]uint32, []string, error) {
|
||||
const (
|
||||
kindEOF = iota
|
||||
kindComma
|
||||
kindAsterisk
|
||||
kindText
|
||||
kindError
|
||||
)
|
||||
type token struct {
|
||||
Kind int
|
||||
Text string
|
||||
}
|
||||
|
||||
var i int
|
||||
// nextToken incrementally consumes s and returns tokens.
|
||||
nextToken := func() token {
|
||||
if !(i < len(s)) {
|
||||
return token{Kind: kindEOF}
|
||||
}
|
||||
if s[i] == ',' {
|
||||
i++
|
||||
return token{Kind: kindComma}
|
||||
}
|
||||
if s[i] == '*' {
|
||||
i++
|
||||
return token{Kind: kindAsterisk}
|
||||
}
|
||||
var text strings.Builder
|
||||
for i < len(s) && s[i] != ',' && s[i] != '*' {
|
||||
if s[i] == '\\' {
|
||||
i++
|
||||
if !(i < len(s)) {
|
||||
return token{Kind: kindError, Text: fmt.Sprintf("%q at end of string", s[i])}
|
||||
}
|
||||
}
|
||||
text.WriteByte(s[i])
|
||||
i++
|
||||
}
|
||||
return token{Kind: kindText, Text: text.String()}
|
||||
}
|
||||
peekToken := func() token {
|
||||
saved := i
|
||||
t := nextToken()
|
||||
i = saved
|
||||
return t
|
||||
}
|
||||
|
||||
const (
|
||||
stateBeginEntry = iota
|
||||
stateLabel
|
||||
stateEndEntry
|
||||
stateDone
|
||||
stateUnexpected
|
||||
)
|
||||
|
||||
var weights []uint32
|
||||
var labels []string
|
||||
var weightString, label string
|
||||
var t token
|
||||
for state := stateBeginEntry; state != stateDone; {
|
||||
switch state {
|
||||
// Beginning of a new entry (at the beginning of the input or
|
||||
// after a comma).
|
||||
case stateBeginEntry:
|
||||
t = nextToken()
|
||||
switch t.Kind {
|
||||
case kindText:
|
||||
// If the next token is an asterisk, this text
|
||||
// represents a weight; otherwise it represents
|
||||
// a label (with a weight of "1").
|
||||
switch peekToken().Kind {
|
||||
case kindAsterisk:
|
||||
nextToken() // Consume the asterisk token.
|
||||
weightString = t.Text
|
||||
state = stateLabel
|
||||
default:
|
||||
weightString = "1"
|
||||
label = t.Text
|
||||
state = stateEndEntry
|
||||
}
|
||||
default:
|
||||
state = stateUnexpected
|
||||
}
|
||||
// weightString is assigned and we have seen an asterisk, now
|
||||
// expect a text label.
|
||||
case stateLabel:
|
||||
t = nextToken()
|
||||
switch t.Kind {
|
||||
case kindText:
|
||||
label = t.Text
|
||||
state = stateEndEntry
|
||||
default:
|
||||
state = stateUnexpected
|
||||
}
|
||||
// weightString and label are assigned, now emit the entry and
|
||||
// expect a comma or EOF.
|
||||
case stateEndEntry:
|
||||
w, err := strconv.ParseUint(weightString, 10, 32)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
weights = append(weights, uint32(w))
|
||||
labels = append(labels, label)
|
||||
t = nextToken()
|
||||
switch t.Kind {
|
||||
case kindEOF:
|
||||
state = stateDone
|
||||
case kindComma:
|
||||
state = stateBeginEntry
|
||||
default:
|
||||
state = stateUnexpected
|
||||
}
|
||||
case stateUnexpected:
|
||||
if t.Kind == kindError {
|
||||
return nil, nil, fmt.Errorf("%s", t.Text)
|
||||
} else {
|
||||
var ttext string
|
||||
switch t.Kind {
|
||||
case kindEOF:
|
||||
ttext = "end of string"
|
||||
case kindComma:
|
||||
ttext = "\",\""
|
||||
case kindAsterisk:
|
||||
ttext = "\"*\""
|
||||
case kindText:
|
||||
ttext = fmt.Sprintf("%+q", t.Text)
|
||||
}
|
||||
return nil, nil, fmt.Errorf("unexpected %s", ttext)
|
||||
}
|
||||
default:
|
||||
panic(state)
|
||||
}
|
||||
}
|
||||
|
||||
return weights, labels, nil
|
||||
}
|
||||
|
||||
// cryptoSource is a math/rand Source that reads from the crypto/rand Reader.
|
||||
// The Seed method does not affect the sequence of numbers returned from the
|
||||
// Int63 method.
|
||||
type cryptoSource struct{}
|
||||
|
||||
func (s cryptoSource) Seed(_ int64) {}
|
||||
|
||||
func (s cryptoSource) Int63() int64 {
|
||||
var n int64
|
||||
err := binary.Read(cryptorand.Reader, binary.BigEndian, &n)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
n &= (1 << 63) - 1
|
||||
return n
|
||||
}
|
||||
|
||||
// sampleWeighted returns the index of a randomly selected element of the
|
||||
// weights slice, weighted by the values stored in the slice. Panics if
|
||||
// the sum of the weights is zero or does not fit in an int64.
|
||||
func sampleWeighted(weights []uint32) int {
|
||||
var sum int64 = 0
|
||||
for _, w := range weights {
|
||||
sum += int64(w)
|
||||
if sum < int64(w) {
|
||||
panic("weights overflow")
|
||||
}
|
||||
}
|
||||
if sum == 0 {
|
||||
panic("total weight is zero")
|
||||
}
|
||||
r := uint64(mathrand.New(&cryptoSource{}).Int63n(sum))
|
||||
for i, w := range weights {
|
||||
if r < uint64(w) {
|
||||
return i
|
||||
}
|
||||
r -= uint64(w)
|
||||
}
|
||||
panic("impossible")
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseWeightedList(t *testing.T) {
|
||||
// Good inputs.
|
||||
for _, test := range []struct {
|
||||
input string
|
||||
expectedWeights []uint32
|
||||
expectedLabels []string
|
||||
}{
|
||||
{"a", []uint32{1}, []string{"a"}},
|
||||
{"apple", []uint32{1}, []string{"apple"}},
|
||||
{"1*apple", []uint32{1}, []string{"apple"}},
|
||||
{"apple,2*carrot,1*apple", []uint32{1, 2, 1}, []string{"apple", "carrot", "apple"}},
|
||||
{"\\a", []uint32{1}, []string{"a"}},
|
||||
{"\\*", []uint32{1}, []string{"*"}},
|
||||
{"\\,", []uint32{1}, []string{","}},
|
||||
{"3\\*apple\\,car\\rot,100*orange", []uint32{1, 100}, []string{"3*apple,carrot", "orange"}},
|
||||
} {
|
||||
weights, labels, err := parseWeightedList(test.input)
|
||||
if err != nil {
|
||||
t.Errorf("%+q resulted in error: %v", test.input, err)
|
||||
continue
|
||||
}
|
||||
i := 0
|
||||
for ; i < len(weights) && i < len(labels) && i < len(test.expectedWeights) && i < len(test.expectedLabels); i++ {
|
||||
if weights[i] != test.expectedWeights[i] {
|
||||
break
|
||||
}
|
||||
if labels[i] != test.expectedLabels[i] {
|
||||
break
|
||||
}
|
||||
}
|
||||
if i < len(test.expectedWeights) || i < len(test.expectedLabels) {
|
||||
t.Errorf("%+q: expected %v, %v, got %v, %v", test.input,
|
||||
test.expectedWeights, test.expectedLabels, weights, labels)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Bad inputs.
|
||||
for _, input := range []string{
|
||||
"",
|
||||
"apple*1",
|
||||
",",
|
||||
",apple",
|
||||
"apple,",
|
||||
"apple,,carrot",
|
||||
"*",
|
||||
"**",
|
||||
"5*apple*5",
|
||||
"-5*apple",
|
||||
"5.5*apple",
|
||||
} {
|
||||
_, _, err := parseWeightedList(input)
|
||||
if err == nil {
|
||||
t.Errorf("%+q resulted in no error", input)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSampleWeighted(t *testing.T) {
|
||||
// Total weight of zero should result in a panic.
|
||||
for _, weights := range [][]uint32{
|
||||
{},
|
||||
{0},
|
||||
{0, 0, 0, 0, 0},
|
||||
} {
|
||||
func() {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
t.Errorf("%v: expected panic", weights)
|
||||
}
|
||||
}()
|
||||
sampleWeighted(weights)
|
||||
}()
|
||||
}
|
||||
|
||||
// If there is only one nonzero weight, it should be always selected.
|
||||
for _, test := range []struct {
|
||||
weights []uint32
|
||||
index int
|
||||
}{
|
||||
{[]uint32{1}, 0},
|
||||
{[]uint32{1, 0, 0, 0, 0}, 0},
|
||||
{[]uint32{0, 0, 0, 0, 1}, 4},
|
||||
{[]uint32{0, 0, 0xffffffff, 0, 1}, 2},
|
||||
} {
|
||||
for i := 0; i < 100; i++ {
|
||||
index := sampleWeighted(test.weights)
|
||||
if index != test.index {
|
||||
t.Errorf("%v: expected %d, got %d", test.weights, test.index, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user