Просмотр исходного кода

Add DNSResolverQName parameters

Miro 1 год назад
Родитель
Сommit
263065de21

+ 4 - 0
psiphon/common/parameters/parameters.go

@@ -358,6 +358,8 @@ const (
 	DNSResolverIncludeEDNS0Probability                 = "DNSResolverIncludeEDNS0Probability"
 	DNSResolverCacheExtensionInitialTTL                = "DNSResolverCacheExtensionInitialTTL"
 	DNSResolverCacheExtensionVerifiedTTL               = "DNSResolverCacheExtensionVerifiedTTL"
+	DNSResolverQNameRandomizeCasingProbability         = "DNSResolverQNameRandomizeCasingProbability"
+	DNSResolverQNameMustMatchProbability               = "DNSResolverQNameMustMatchProbability"
 	AddFrontingProviderPsiphonFrontingHeader           = "AddFrontingProviderPsiphonFrontingHeader"
 	DirectHTTPProtocolTransformSpecs                   = "DirectHTTPProtocolTransformSpecs"
 	DirectHTTPProtocolTransformScopedSpecNames         = "DirectHTTPProtocolTransformScopedSpecNames"
@@ -880,6 +882,8 @@ var defaultParameters = map[string]struct {
 	DNSResolverIncludeEDNS0Probability:          {value: 0.0, minimum: 0.0},
 	DNSResolverCacheExtensionInitialTTL:         {value: time.Duration(0), minimum: time.Duration(0)},
 	DNSResolverCacheExtensionVerifiedTTL:        {value: time.Duration(0), minimum: time.Duration(0)},
+	DNSResolverQNameRandomizeCasingProbability:  {value: 0.0, minimum: 0.0},
+	DNSResolverQNameMustMatchProbability:        {value: 0.0, minimum: 0.0},
 
 	AddFrontingProviderPsiphonFrontingHeader: {value: protocol.LabeledTunnelProtocols{}},
 

+ 12 - 1
psiphon/common/protocol/packed.go

@@ -802,7 +802,18 @@ func init() {
 		{149, "quic_dial_early", intConverter},
 		{150, "quic_obfuscated_psk", intConverter},
 
-		// Next key value = 151
+		// Specs:
+		// parameters.DNSResolverQNameRandomizeCasingProbability
+		// parameters.DNSResolverQNameMustMatchProbability
+
+		{151, "dns_qname_random_casing", intConverter},
+		{152, "dns_qname_must_match", intConverter},
+		{153, "inproxy_broker_dns_qname_random_casing", intConverter},
+		{154, "inproxy_broker_dns_qname_must_match", intConverter},
+		{155, "inproxy_webrtc_dns_qname_random_casing", intConverter},
+		{156, "inproxy_webrtc_dns_qname_must_match", intConverter},
+
+		// Next key value = 157
 	}
 
 	for _, spec := range packedAPIParameterSpecs {

+ 54 - 6
psiphon/common/resolver/resolver.go

@@ -208,6 +208,21 @@ type ResolveParameters struct {
 	// specify the same seed.
 	ProtocolTransformSeed *prng.Seed
 
+	// RandomQNameCasingSeed specifies the seed for randomizing the casing of
+	// the QName (hostname) in the DNS request. If not set, the QName casing
+	// will remain unchanged. To reproduce the same random casing, use the same
+	// seed.
+	RandomQNameCasingSeed *prng.Seed
+
+	// ResponseQNameMustMatch specifies whether the response's question section
+	// must contain exactly one entry, and that entry's QName (hostname) must
+	// exactly match the QName sent in the DNS request.
+	//
+	// RFC 1035 does not specify that the question section in the response must
+	// exactly match the question section in the request, but this behavior is
+	// common.
+	ResponseQNameMustMatch bool
+
 	// IncludeEDNS0 indicates whether to include the EDNS(0) UDP maximum
 	// response size extension in DNS requests. The resolver can handle
 	// responses larger than 512 bytes (RFC 1035 maximum) regardless of
@@ -443,6 +458,16 @@ func (r *Resolver) MakeResolveParameters(
 		}
 	}
 
+	if p.WeightedCoinFlip(parameters.DNSResolverQNameRandomizeCasingProbability) {
+		var err error
+		params.RandomQNameCasingSeed, err = prng.NewSeed()
+		if err != nil {
+			return nil, errors.Trace(err)
+		}
+	}
+
+	params.ResponseQNameMustMatch = p.WeightedCoinFlip(parameters.DNSResolverQNameMustMatchProbability)
+
 	if p.WeightedCoinFlip(parameters.DNSResolverIncludeEDNS0Probability) {
 		params.IncludeEDNS0 = true
 	}
@@ -728,9 +753,11 @@ func (r *Resolver) ResolveIP(
 
 		server := servers[index]
 
-		// Only the first attempt pair tries transforms, as it's not certain
-		// the transforms will be compatible with DNS servers.
+		// Only the first attempt pair tries techniques that may not be
+		// compatible with all DNS servers.
 		useProtocolTransform := (i == 0 && params.ProtocolTransformSpec != nil)
+		useRandomQNameCasing := (i == 0 && params.RandomQNameCasingSeed != nil)
+		responseQNameMustMatch := (i == 0 && params.ResponseQNameMustMatch)
 
 		// Send A and AAAA requests concurrently.
 		questionTypes := []resolverQuestionType{resolverQuestionTypeA, resolverQuestionTypeAAAA}
@@ -752,7 +779,7 @@ func (r *Resolver) ResolveIP(
 			inFlight += 1
 			r.updateMetricPeakInFlight(inFlight)
 
-			go func(attempt int, questionType resolverQuestionType, useProtocolTransform bool) {
+			go func(attempt int, questionType resolverQuestionType, useProtocolTransform, useRandomQNameCasing, responseQNameMustMatch bool) {
 				defer waitGroup.Done()
 
 				// Always send a result back to the main loop, even if this
@@ -834,9 +861,11 @@ func (r *Resolver) ResolveIP(
 					r.networkConfig.logWarning,
 					params,
 					useProtocolTransform,
+					useRandomQNameCasing,
 					conn,
 					questionType,
-					hostname)
+					hostname,
+					responseQNameMustMatch)
 
 				// Update the min/max RTT metric when reported (>=0) even if
 				// the result is an error; i.e., the even if there was an
@@ -880,7 +909,7 @@ func (r *Resolver) ResolveIP(
 					}
 				}
 
-			}(i+1, questionType, useProtocolTransform)
+			}(i+1, questionType, useProtocolTransform, useRandomQNameCasing, responseQNameMustMatch)
 		}
 
 		resetTimer(requestTimeout)
@@ -1472,9 +1501,11 @@ func performDNSQuery(
 	logWarning func(error),
 	params *ResolveParameters,
 	useProtocolTransform bool,
+	useRandomQNameCasing bool,
 	conn net.Conn,
 	questionType resolverQuestionType,
-	hostname string) ([]net.IP, []time.Duration, time.Duration, error) {
+	hostname string,
+	responseQNameMustMatch bool) ([]net.IP, []time.Duration, time.Duration, error) {
 
 	if useProtocolTransform {
 		if params.ProtocolTransformSpec == nil ||
@@ -1494,6 +1525,10 @@ func performDNSQuery(
 		}
 	}
 
+	if useRandomQNameCasing {
+		hostname = common.ToRandomCasing(hostname, params.RandomQNameCasingSeed)
+	}
+
 	// UDPSize sets the receive buffer to > 512, even when we don't include
 	// EDNS(0), which will mitigate issues with RFC 1035 non-compliant
 	// servers. See Go issue 51127.
@@ -1571,6 +1606,19 @@ func performDNSQuery(
 			continue
 		}
 
+		if responseQNameMustMatch {
+			if len(response.Question) != 1 {
+				lastErr = errors.Tracef("unexpected QDCount")
+				logWarning(lastErr)
+				continue
+			}
+			if response.Question[0].Name != dns.Fqdn(hostname) {
+				lastErr = errors.Tracef("unexpected QName")
+				logWarning(lastErr)
+				continue
+			}
+		}
+
 		// Check the RCode.
 		//
 		// For IPv4, we expect RCodeSuccess as Psiphon will typically only

+ 77 - 18
psiphon/common/resolver/resolver_test.go

@@ -80,6 +80,8 @@ func runTestMakeResolveParameters() error {
 		"DNSResolverProtocolTransformProbability":     1.0,
 		"DNSResolverProtocolTransformSpecs":           transforms.Specs{transformName: exampleTransform},
 		"DNSResolverProtocolTransformScopedSpecNames": transforms.ScopedSpecNames{preferredAlternateDNSServer: []string{transformName}},
+		"DNSResolverQNameRandomizeCasingProbability":  1.0,
+		"DNSResolverQNameMustMatchProbability":        1.0,
 		"DNSResolverIncludeEDNS0Probability":          1.0,
 	}
 
@@ -132,7 +134,7 @@ func runTestMakeResolveParameters() error {
 		}
 	}
 
-	// Test: Preferred/Transform/EDNS(0)
+	// Test: Preferred/Transform/RandomQNameCasing/QNameMustMatch/EDNS(0)
 
 	paramValues["DNSResolverPreresolvedIPAddressProbability"] = 0.0
 
@@ -157,6 +159,8 @@ func runTestMakeResolveParameters() error {
 		resolverParams.PreferAlternateDNSServer != true ||
 		resolverParams.ProtocolTransformName != transformName ||
 		resolverParams.ProtocolTransformSpec == nil ||
+		resolverParams.RandomQNameCasingSeed == nil ||
+		resolverParams.ResponseQNameMustMatch != true ||
 		resolverParams.IncludeEDNS0 != true {
 		return errors.Tracef("unexpected resolver parameters: %+v", resolverParams)
 	}
@@ -165,6 +169,8 @@ func runTestMakeResolveParameters() error {
 
 	paramValues["DNSResolverPreferAlternateServerProbability"] = 0.0
 	paramValues["DNSResolverProtocolTransformProbability"] = 0.0
+	paramValues["DNSResolverQNameRandomizeCasingProbability"] = 0.0
+	paramValues["DNSResolverQNameMustMatchProbability"] = 0.0
 	paramValues["DNSResolverIncludeEDNS0Probability"] = 0.0
 
 	_, err = params.Set("", 0, paramValues)
@@ -188,6 +194,8 @@ func runTestMakeResolveParameters() error {
 		resolverParams.PreferAlternateDNSServer != false ||
 		resolverParams.ProtocolTransformName != "" ||
 		resolverParams.ProtocolTransformSpec != nil ||
+		resolverParams.RandomQNameCasingSeed != nil ||
+		resolverParams.ResponseQNameMustMatch != false ||
 		resolverParams.IncludeEDNS0 != false {
 		return errors.Tracef("unexpected resolver parameters: %+v", resolverParams)
 	}
@@ -198,14 +206,14 @@ func runTestMakeResolveParameters() error {
 func runTestResolver() error {
 
 	// noResponseServer will not respond to requests
-	noResponseServer, err := newTestDNSServer(false, false, false)
+	noResponseServer, err := newTestDNSServer(false, false, false, false)
 	if err != nil {
 		return errors.Trace(err)
 	}
 	defer noResponseServer.stop()
 
 	// invalidIPServer will respond with an invalid IP
-	invalidIPServer, err := newTestDNSServer(true, false, false)
+	invalidIPServer, err := newTestDNSServer(true, false, false, false)
 	if err != nil {
 		return errors.Trace(err)
 	}
@@ -213,7 +221,7 @@ func runTestResolver() error {
 
 	// okServer will respond to correct requests (expected domain) with the
 	// correct response (expected IPv4 or IPv6 address)
-	okServer, err := newTestDNSServer(true, true, false)
+	okServer, err := newTestDNSServer(true, true, false, false)
 	if err != nil {
 		return errors.Trace(err)
 	}
@@ -221,7 +229,7 @@ func runTestResolver() error {
 
 	// alternateOkServer behaves like okServer; getRequestCount is used to
 	// confirm that the alternate server was indeed used
-	alternateOkServer, err := newTestDNSServer(true, true, false)
+	alternateOkServer, err := newTestDNSServer(true, true, false, false)
 	if err != nil {
 		return errors.Trace(err)
 	}
@@ -230,12 +238,18 @@ func runTestResolver() error {
 	// transformOkServer behaves like okServer but only responds if the
 	// transform was applied; other servers do not respond if the transform
 	// is applied
-	transformOkServer, err := newTestDNSServer(true, true, true)
+	transformOkServer, err := newTestDNSServer(true, true, true, false)
 	if err != nil {
 		return errors.Trace(err)
 	}
 	defer transformOkServer.stop()
 
+	randomQNameCasingOkServer, err := newTestDNSServer(true, true, false, true)
+	if err != nil {
+		return errors.Trace(err)
+	}
+	defer randomQNameCasingOkServer.stop()
+
 	servers := []string{noResponseServer.getAddr(), invalidIPServer.getAddr(), okServer.getAddr()}
 
 	networkConfig := &NetworkConfig{
@@ -529,6 +543,7 @@ func runTestResolver() error {
 
 	resolver.cache.Flush()
 
+	params.AttemptsPerServer = 0
 	params.AlternateDNSServer = transformOkServer.getAddr()
 	params.PreferAlternateDNSServer = true
 
@@ -555,12 +570,52 @@ func runTestResolver() error {
 		return errors.TraceNew("unexpected transform server request count")
 	}
 
+	params.AttemptsPerServer = 1
 	params.AlternateDNSServer = ""
 	params.PreferAlternateDNSServer = false
 	params.ProtocolTransformName = ""
 	params.ProtocolTransformSpec = nil
 	params.ProtocolTransformSeed = nil
 
+	// Test: random QName casing
+
+	if randomQNameCasingOkServer.getRequestCount() != 0 {
+		return errors.TraceNew("unexpected random QName casing server request count")
+	}
+
+	resolver.cache.Flush()
+
+	params.AttemptsPerServer = 0
+	params.AlternateDNSServer = randomQNameCasingOkServer.getAddr()
+	params.PreferAlternateDNSServer = true
+	params.RandomQNameCasingSeed = seed
+
+	params.ResponseQNameMustMatch = true
+	_, err = resolver.ResolveIP(ctx, networkID, params, exampleDomain)
+	if err == nil {
+		errors.TraceNew("unexpected success")
+	}
+
+	params.ResponseQNameMustMatch = false
+	IPs, err = resolver.ResolveIP(ctx, networkID, params, exampleDomain)
+	if err == nil {
+		errors.TraceNew("unexpected success")
+	}
+
+	err = checkResult(IPs)
+	if err != nil {
+		return errors.Trace(err)
+	}
+
+	if randomQNameCasingOkServer.getRequestCount() < 1 {
+		return errors.TraceNew("unexpected random QName casing server request count")
+	}
+
+	params.AttemptsPerServer = 1
+	params.AlternateDNSServer = ""
+	params.PreferAlternateDNSServer = false
+	params.RandomQNameCasingSeed = nil
+
 	// Test: EDNS(0)
 
 	resolver.cache.Flush()
@@ -741,15 +796,16 @@ const (
 var exampleTransform = transforms.Spec{[2]string{"^([a-f0-9]{4})0100", "\\$\\{1\\}0140"}}
 
 type testDNSServer struct {
-	respond         bool
-	validResponse   bool
-	expectTransform bool
-	addr            string
-	requestCount    int32
-	server          *dns.Server
+	respond                 bool
+	validResponse           bool
+	expectTransform         bool
+	expectRandomQNameCasing bool
+	addr                    string
+	requestCount            int32
+	server                  *dns.Server
 }
 
-func newTestDNSServer(respond, validResponse, expectTransform bool) (*testDNSServer, error) {
+func newTestDNSServer(respond, validResponse, expectTransform, expectRandomQNameCasing bool) (*testDNSServer, error) {
 
 	udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
 	if err != nil {
@@ -762,10 +818,11 @@ func newTestDNSServer(respond, validResponse, expectTransform bool) (*testDNSSer
 	}
 
 	s := &testDNSServer{
-		respond:         respond,
-		validResponse:   validResponse,
-		expectTransform: expectTransform,
-		addr:            udpConn.LocalAddr().String(),
+		respond:                 respond,
+		validResponse:           validResponse,
+		expectTransform:         expectTransform,
+		expectRandomQNameCasing: expectRandomQNameCasing,
+		addr:                    udpConn.LocalAddr().String(),
 	}
 
 	server := &dns.Server{
@@ -792,7 +849,9 @@ func (s *testDNSServer) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
 		return
 	}
 
-	if len(r.Question) != 1 || r.Question[0].Name != dns.Fqdn(exampleDomain) {
+	if len(r.Question) != 1 ||
+		(!s.expectRandomQNameCasing &&
+			r.Question[0].Name != dns.Fqdn(exampleDomain)) {
 		return
 	}
 

+ 26 - 0
psiphon/common/utils.go

@@ -31,7 +31,9 @@ import (
 	"math"
 	"net/url"
 	"os"
+	"strings"
 	"time"
+	"unicode"
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
@@ -292,3 +294,27 @@ func MaxDuration(durations ...time.Duration) time.Duration {
 	}
 	return max
 }
+
+// ToRandomCasing returns s with each Unicode letter randomly mapped to either
+// its upper or lower case.
+func ToRandomCasing(s string, seed *prng.Seed) string {
+
+	PRNG := prng.NewPRNGWithSeed(seed)
+
+	var result strings.Builder
+	result.Grow(len(s))
+
+	for _, r := range s {
+		if unicode.IsLetter(r) {
+			if PRNG.FlipCoin() {
+				result.WriteRune(unicode.ToLower(r))
+			} else {
+				result.WriteRune(unicode.ToUpper(r))
+			}
+		} else {
+			result.WriteRune(r)
+		}
+	}
+
+	return result.String()
+}

+ 29 - 0
psiphon/common/utils_test.go

@@ -28,6 +28,8 @@ import (
 	"strings"
 	"testing"
 	"time"
+
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 )
 
 func TestGetStringSlice(t *testing.T) {
@@ -164,3 +166,30 @@ func TestSleepWithContext(t *testing.T) {
 		t.Errorf("unexpected duration: %v", duration)
 	}
 }
+
+func TestToRandomCasing(t *testing.T) {
+	s := "test.to.random.casing.aaaa.bbbb.cccc.dd" // 32 Unicode letters
+
+	seed, err := prng.NewSeed()
+	if err != nil {
+		t.Errorf("NewPRNG failed: %s", err)
+	}
+
+	randomized := ToRandomCasing(s, seed)
+
+	// Note: There's a (1/2)^32 chance that the randomized string has the same
+	// casing as the input string.
+	if strings.Compare(s, randomized) == 0 {
+		t.Errorf("expected random casing")
+	}
+
+	if strings.Compare(strings.ToLower(s), strings.ToLower(randomized)) != 0 {
+		t.Errorf("expected strings to be identical minus casing")
+	}
+
+	replaySameSeed := ToRandomCasing(s, seed)
+
+	if strings.Compare(randomized, replaySameSeed) != 0 {
+		t.Errorf("expected randomized string with same seed to be identical")
+	}
+}

+ 20 - 0
psiphon/config.go

@@ -947,6 +947,8 @@ type Config struct {
 	DNSResolverProtocolTransformSpecs                transforms.Specs
 	DNSResolverProtocolTransformScopedSpecNames      transforms.ScopedSpecNames
 	DNSResolverProtocolTransformProbability          *float64
+	DNSResolverQNameRandomizeCasingProbability       *float64
+	DNSResolverQNameMustMatchProbability             *float64
 	DNSResolverIncludeEDNS0Probability               *float64
 	DNSResolverCacheExtensionInitialTTLMilliseconds  *int
 	DNSResolverCacheExtensionVerifiedTTLMilliseconds *int
@@ -2381,6 +2383,14 @@ func (config *Config) makeConfigParameters() map[string]interface{} {
 		applyParameters[parameters.DNSResolverProtocolTransformProbability] = *config.DNSResolverProtocolTransformProbability
 	}
 
+	if config.DNSResolverQNameRandomizeCasingProbability != nil {
+		applyParameters[parameters.DNSResolverQNameRandomizeCasingProbability] = *config.DNSResolverQNameRandomizeCasingProbability
+	}
+
+	if config.DNSResolverQNameMustMatchProbability != nil {
+		applyParameters[parameters.DNSResolverQNameMustMatchProbability] = *config.DNSResolverQNameMustMatchProbability
+	}
+
 	if config.DNSResolverIncludeEDNS0Probability != nil {
 		applyParameters[parameters.DNSResolverIncludeEDNS0Probability] = *config.DNSResolverIncludeEDNS0Probability
 	}
@@ -3289,6 +3299,16 @@ func (config *Config) setDialParametersHash() {
 		binary.Write(hash, binary.LittleEndian, *config.DNSResolverProtocolTransformProbability)
 	}
 
+	if config.DNSResolverQNameRandomizeCasingProbability != nil {
+		hash.Write([]byte("DNSResolverQNameRandomizeCasingProbability"))
+		binary.Write(hash, binary.LittleEndian, *config.DNSResolverQNameRandomizeCasingProbability)
+	}
+
+	if config.DNSResolverQNameMustMatchProbability != nil {
+		hash.Write([]byte("DNSResolverQNameMustMatchProbability"))
+		binary.Write(hash, binary.LittleEndian, *config.DNSResolverQNameMustMatchProbability)
+	}
+
 	if config.DNSResolverIncludeEDNS0Probability != nil {
 		hash.Write([]byte("DNSResolverIncludeEDNS0Probability"))
 		binary.Write(hash, binary.LittleEndian, *config.DNSResolverIncludeEDNS0Probability)

+ 8 - 0
psiphon/frontingDialParameters.go

@@ -518,6 +518,14 @@ func (meekDialParameters *FrontedMeekDialParameters) GetMetrics(overridePrefix s
 			logFields[prefix+"dns_transform"] = meekDialParameters.ResolveParameters.ProtocolTransformName
 		}
 
+		if meekDialParameters.ResolveParameters.RandomQNameCasingSeed != nil {
+			logFields[prefix+"dns_qname_random_casing"] = "1"
+		}
+
+		if meekDialParameters.ResolveParameters.ResponseQNameMustMatch {
+			logFields[prefix+"dns_qname_must_match"] = "1"
+		}
+
 		logFields[prefix+"dns_attempt"] = strconv.Itoa(
 			meekDialParameters.ResolveParameters.GetFirstAttemptWithAnswer())
 	}

+ 8 - 0
psiphon/inproxy.go

@@ -2085,6 +2085,14 @@ func (dialParams *InproxySTUNDialParameters) GetMetrics() common.LogFields {
 			logFields["inproxy_webrtc_dns_transform"] = dialParams.ResolveParameters.ProtocolTransformName
 		}
 
+		if dialParams.ResolveParameters.RandomQNameCasingSeed != nil {
+			logFields["inproxy_webrtc_dns_qname_random_casing"] = "1"
+		}
+
+		if dialParams.ResolveParameters.ResponseQNameMustMatch {
+			logFields["inproxy_webrtc_dns_qname_must_match"] = "1"
+		}
+
 		logFields["inproxy_webrtc_dns_attempt"] = strconv.Itoa(
 			dialParams.ResolveParameters.GetFirstAttemptWithAnswer())
 	}

+ 6 - 0
psiphon/server/api.go

@@ -1120,6 +1120,8 @@ var baseDialParams = []requestParamSpec{
 	{"dns_preresolved", isAnyString, requestParamOptional},
 	{"dns_preferred", isAnyString, requestParamOptional},
 	{"dns_transform", isAnyString, requestParamOptional},
+	{"dns_qname_random_casing", isBooleanFlag, requestParamOptional | requestParamLogFlagAsBool},
+	{"dns_qname_must_match", isBooleanFlag, requestParamOptional | requestParamLogFlagAsBool},
 	{"dns_attempt", isIntString, requestParamOptional | requestParamLogStringAsInt},
 	{"http_transform", isAnyString, requestParamOptional},
 	{"seed_transform", isAnyString, requestParamOptional},
@@ -1162,10 +1164,14 @@ var inproxyDialParams = []requestParamSpec{
 	{"inproxy_broker_dns_preresolved", isAnyString, requestParamOptional},
 	{"inproxy_broker_dns_preferred", isAnyString, requestParamOptional},
 	{"inproxy_broker_dns_transform", isAnyString, requestParamOptional},
+	{"inproxy_broker_dns_qname_random_casing", isBooleanFlag, requestParamOptional | requestParamLogFlagAsBool},
+	{"inproxy_broker_dns_qname_must_match", isBooleanFlag, requestParamOptional | requestParamLogFlagAsBool},
 	{"inproxy_broker_dns_attempt", isIntString, requestParamOptional | requestParamLogStringAsInt},
 	{"inproxy_webrtc_dns_preresolved", isAnyString, requestParamOptional},
 	{"inproxy_webrtc_dns_preferred", isAnyString, requestParamOptional},
 	{"inproxy_webrtc_dns_transform", isAnyString, requestParamOptional},
+	{"inproxy_broker_dns_qname_random_casing", isBooleanFlag, requestParamOptional | requestParamLogFlagAsBool},
+	{"inproxy_webrtc_dns_qname_must_match", isBooleanFlag, requestParamOptional | requestParamLogFlagAsBool},
 	{"inproxy_webrtc_dns_attempt", isIntString, requestParamOptional | requestParamLogStringAsInt},
 	{"inproxy_webrtc_stun_server", isAnyString, requestParamOptional},
 	{"inproxy_webrtc_stun_server_resolved_ip_address", isAnyString, requestParamOptional},

+ 8 - 0
psiphon/serverApi.go

@@ -1278,6 +1278,14 @@ func getBaseAPIParameters(
 				params["dns_transform"] = dialParams.ResolveParameters.ProtocolTransformName
 			}
 
+			if dialParams.ResolveParameters.RandomQNameCasingSeed != nil {
+				params["dns_qname_random_casing"] = "1"
+			}
+
+			if dialParams.ResolveParameters.ResponseQNameMustMatch {
+				params["dns_qname_must_match"] = "1"
+			}
+
 			params["dns_attempt"] = strconv.Itoa(
 				dialParams.ResolveParameters.GetFirstAttemptWithAnswer())
 		}