/*
* Copyright (c) 2015, Psiphon Inc.
* All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package psiphon
import (
"crypto/md5"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"reflect"
"regexp"
"strconv"
"strings"
"sync"
"unicode"
"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/resolver"
"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/transforms"
"golang.org/x/crypto/nacl/secretbox"
)
const (
TUNNEL_POOL_SIZE = 1
MAX_TUNNEL_POOL_SIZE = 32
// Psiphon data directory name, relative to config.DataRootDirectory.
// See config.GetPsiphonDataDirectory().
PsiphonDataDirectoryName = "ca.psiphon.PsiphonTunnel.tunnel-core"
// Filename constants, all relative to config.GetPsiphonDataDirectory().
HomepageFilename = "homepage"
NoticesFilename = "notices"
OldNoticesFilename = "notices.1"
UpgradeDownloadFilename = "upgrade"
)
// Config is the Psiphon configuration specified by the application. This
// configuration controls the behavior of the core tunnel functionality.
//
// To distinguish omitted timeout params from explicit 0 value timeout params,
// corresponding fields are int pointers. nil means no value was supplied and
// to use the default; a non-nil pointer to 0 means no timeout.
type Config struct {
// DataRootDirectory is the directory in which to store persistent files,
// which contain information such as server entries. By default, current
// working directory.
//
// Psiphon will assume full control of files under this directory. They may
// be deleted, moved or overwritten.
DataRootDirectory string
// UseNoticeFiles configures notice files for writing. If set, homepages
// will be written to a file created at config.GetHomePageFilename()
// and notices will be written to a file created at
// config.GetNoticesFilename().
//
// The homepage file may be read after the Tunnels notice with count of 1.
//
// The value of UseNoticeFiles sets the size and frequency at which the
// notices file, config.GetNoticesFilename(), will be rotated. See the
// comment for UseNoticeFiles for more details. One rotated older file,
// config.GetOldNoticesFilename(), is retained.
//
// The notice files may be may be read at any time; and should be opened
// read-only for reading. Diagnostic notices are omitted from the notice
// files.
//
// See comment for setNoticeFiles in notice.go for further details.
UseNoticeFiles *UseNoticeFiles
// PropagationChannelId is a string identifier which indicates how the
// Psiphon client was distributed. This parameter is required. This value
// is supplied by and depends on the Psiphon Network, and is typically
// embedded in the client binary.
PropagationChannelId string
// SponsorId is a string identifier which indicates who is sponsoring this
// Psiphon client. One purpose of this value is to determine the home
// pages for display. This parameter is required. This value is supplied
// by and depends on the Psiphon Network, and is typically embedded in the
// client binary.
SponsorId string
// ClientVersion is the client version number that the client reports to
// the server. The version number refers to the host client application,
// not the core tunnel library. One purpose of this value is to enable
// automatic updates. This value is supplied by and depends on the Psiphon
// Network, and is typically embedded in the client binary.
//
// Note that sending a ClientPlatform string which includes "windows"
// (case insensitive) and a ClientVersion of <= 44 will cause an error in
// processing the response to DoConnectedRequest calls.
ClientVersion string
// ClientPlatform is the client platform ("Windows", "Android", etc.) that
// the client reports to the server.
ClientPlatform string
// ClientFeatures is a list of feature names denoting enabled application
// features. Clients report enabled features to the server for stats
// purposes.
ClientFeatures []string
// EgressRegion is a ISO 3166-1 alpha-2 country code which indicates which
// country to egress from. For the default, "", the best performing server
// in any country is selected.
EgressRegion string
// SplitTunnelOwnRegion enables split tunnel mode for the client's own
// country. When enabled, TCP port forward destinations that resolve to
// the same GeoIP country as the client are connected to directly,
// untunneled.
SplitTunnelOwnRegion bool
// SplitTunnelRegions enables selected split tunnel mode in which the
// client specifies a list of ISO 3166-1 alpha-2 country codes for which
// traffic should be untunneled. TCP port forwards destined to any
// country specified in SplitTunnelRegions will be untunneled, regardless
// of whether SplitTunnelOwnRegion is on or off.
SplitTunnelRegions []string
// ListenInterface specifies which interface to listen on. If no
// interface is provided then listen on 127.0.0.1. If 'any' is provided
// then use 0.0.0.0. If there are multiple IP addresses on an interface
// use the first IPv4 address.
ListenInterface string
// DisableLocalSocksProxy disables running the local SOCKS proxy.
DisableLocalSocksProxy bool
// LocalSocksProxyPort specifies a port number for the local SOCKS proxy
// running at 127.0.0.1. For the default value, 0, the system selects a
// free port (a notice reporting the selected port is emitted).
LocalSocksProxyPort int
// LocalHttpProxyPort specifies a port number for the local HTTP proxy
// running at 127.0.0.1. For the default value, 0, the system selects a
// free port (a notice reporting the selected port is emitted).
LocalHttpProxyPort int
// DisableLocalHTTPProxy disables running the local HTTP proxy.
DisableLocalHTTPProxy bool
// NetworkLatencyMultiplier is a multiplier that is to be applied to
// default network event timeouts. Set this to tune performance for
// slow networks.
// When set, must be >= 1.0.
NetworkLatencyMultiplier float64
// LimitTunnelProtocols indicates which protocols to use. Valid values
// include: "SSH", "OSSH", "TLS-OSSH", "UNFRONTED-MEEK-OSSH",
// "UNFRONTED-MEEK-HTTPS-OSSH", "UNFRONTED-MEEK-SESSION-TICKET-OSSH",
// "FRONTED-MEEK-OSSH", "FRONTED-MEEK-HTTP-OSSH", "QUIC-OSSH",
// "FRONTED-MEEK-QUIC-OSSH", "TAPDANCE-OSSH", and "CONJURE-OSSH".
// For the default, an empty list, all protocols are used.
LimitTunnelProtocols []string
// InitialLimitTunnelProtocols is an optional initial phase of limited
// protocols for the first InitialLimitTunnelProtocolsCandidateCount
// candidates; after these candidates, LimitTunnelProtocols applies.
//
// For the default, an empty list, InitialLimitTunnelProtocols is off.
InitialLimitTunnelProtocols []string
// InitialLimitTunnelProtocolsCandidateCount is the number of candidates
// to which InitialLimitTunnelProtocols is applied instead of
// LimitTunnelProtocols.
//
// For the default, 0, InitialLimitTunnelProtocols is off.
InitialLimitTunnelProtocolsCandidateCount int
// LimitTLSProfiles indicates which TLS profiles to select from. Valid
// values are listed in protocols.SupportedTLSProfiles.
// For the default, an empty list, all profiles are candidates for
// selection.
LimitTLSProfiles []string
// LimitQUICVersions indicates which QUIC versions to select from. Valid
// values are listed in protocols.SupportedQUICVersions.
// For the default, an empty list, all versions are candidates for
// selection.
LimitQUICVersions []string
// EstablishTunnelTimeoutSeconds specifies a time limit after which to
// halt the core tunnel controller if no tunnel has been established. The
// default is parameters.EstablishTunnelTimeout.
EstablishTunnelTimeoutSeconds *int
// EstablishTunnelPausePeriodSeconds specifies the delay between attempts
// to establish tunnels. Briefly pausing allows for network conditions to
// improve and for asynchronous operations such as fetch remote server
// list to complete. If omitted, a default value is used. This value is
// typical overridden for testing.
EstablishTunnelPausePeriodSeconds *int
// EstablishTunnelPausePeriodSeconds specifies the grace period, or head
// start, provided to the affinity server candidate when establishing. The
// affinity server is the server used for the last established tunnel.
EstablishTunnelServerAffinityGracePeriodMilliseconds *int
// ConnectionWorkerPoolSize specifies how many connection attempts to
// attempt in parallel. If omitted of when 0, a default is used; this is
// recommended.
ConnectionWorkerPoolSize int
// TunnelPoolSize specifies how many tunnels to run in parallel. Port
// forwards are multiplexed over multiple tunnels. If omitted or when 0,
// the default is TUNNEL_POOL_SIZE, which is recommended. Any value over
// MAX_TUNNEL_POOL_SIZE is treated as MAX_TUNNEL_POOL_SIZE.
TunnelPoolSize int
// StaggerConnectionWorkersMilliseconds adds a specified delay before
// making each server candidate available to connection workers. This
// option is enabled when StaggerConnectionWorkersMilliseconds > 0.
StaggerConnectionWorkersMilliseconds int
// LimitIntensiveConnectionWorkers limits the number of concurrent
// connection workers attempting connections with resource intensive
// protocols. This option is enabled when LimitIntensiveConnectionWorkers
// > 0.
LimitIntensiveConnectionWorkers int
// LimitMeekBufferSizes selects smaller buffers for meek protocols.
LimitMeekBufferSizes bool
// LimitCPUThreads minimizes the number of CPU threads -- and associated
// overhead -- the are used.
LimitCPUThreads bool
// LimitRelayBufferSizes selects smaller buffers for port forward relaying.
LimitRelayBufferSizes bool
// IgnoreHandshakeStatsRegexps skips compiling and using stats regexes.
IgnoreHandshakeStatsRegexps bool
// UpstreamProxyURL is a URL specifying an upstream proxy to use for all
// outbound connections. The URL should include proxy type and
// authentication information, as required. See example URLs here:
// https://github.com/Psiphon-Labs/psiphon-tunnel-core/tree/master/psiphon/upstreamproxy
UpstreamProxyURL string
// CustomHeaders is a set of additional arbitrary HTTP headers that are
// added to all plaintext HTTP requests and requests made through an HTTP
// upstream proxy when specified by UpstreamProxyURL.
CustomHeaders http.Header
// MeekAdditionalHeaders is a set of additional arbitrary HTTP headers
// that are added to all meek HTTP requests. An additional header is
// ignored when the header name is already present in a meek request.
MeekAdditionalHeaders http.Header
// NetworkConnectivityChecker is an interface that enables tunnel-core to
// call into the host application to check for network connectivity. See:
// NetworkConnectivityChecker doc.
NetworkConnectivityChecker NetworkConnectivityChecker
// DeviceBinder is an interface that enables tunnel-core to call into the
// host application to bind sockets to specific devices. See: DeviceBinder
// doc.
//
// When DeviceBinder is set, the "VPN" feature name is automatically added
// when reporting ClientFeatures.
DeviceBinder DeviceBinder
// AllowDefaultDNSResolverWithBindToDevice indicates that it's safe to use
// the default resolver when DeviceBinder is configured, as the host OS
// will automatically exclude DNS requests from the VPN.
AllowDefaultDNSResolverWithBindToDevice bool
// IPv6Synthesizer is an interface that allows tunnel-core to call into
// the host application to synthesize IPv6 addresses. See: IPv6Synthesizer
// doc.
IPv6Synthesizer IPv6Synthesizer
// HasIPv6RouteGetter is an interface that allows tunnel-core to call into
// the host application to determine if the host has an IPv6 route. See:
// HasIPv6RouteGetter doc.
HasIPv6RouteGetter HasIPv6RouteGetter
// DNSServerGetter is an interface that enables tunnel-core to call into
// the host application to discover the native network DNS server
// settings. See: DNSServerGetter doc.
DNSServerGetter DNSServerGetter
// NetworkIDGetter in an interface that enables tunnel-core to call into
// the host application to get an identifier for the host's current active
// network. See: NetworkIDGetter doc.
NetworkIDGetter NetworkIDGetter
// NetworkID, when not blank, is used as the identifier for the host's
// current active network.
// NetworkID is ignored when NetworkIDGetter is set.
NetworkID string
// DisableTactics disables tactics operations including requests, payload
// handling, and application of parameters.
DisableTactics bool
// DisableReplay causes any persisted dial parameters to be ignored when
// they would otherwise be used for replay.
DisableReplay bool
// TargetServerEntry is an encoded server entry. When specified, this
// server entry is used exclusively and all other known servers are
// ignored; also, when set, ConnectionWorkerPoolSize is ignored and
// the pool size is 1.
TargetServerEntry string
// DisableApi disables Psiphon server API calls including handshake,
// connected, status, etc. This is used for special case temporary tunnels
// (Windows VPN mode).
DisableApi bool
// TargetApiProtocol specifies whether to force use of "ssh" or "web" API
// protocol. When blank, the default, the optimal API protocol is used.
// Note that this capability check is not applied before the
// "CandidateServers" count is emitted.
//
// This parameter is intended for testing and debugging only. Not all
// parameters are supported in the legacy "web" API protocol, including
// speed test samples.
TargetApiProtocol string
// RemoteServerListURLs is list of URLs which specify locations to fetch
// out-of-band server entries. This facility is used when a tunnel cannot
// be established to known servers. This value is supplied by and depends
// on the Psiphon Network, and is typically embedded in the client binary.
// All URLs must point to the same entity with the same ETag. At least one
// TransferURL must have OnlyAfterAttempts = 0.
RemoteServerListURLs parameters.TransferURLs
// RemoteServerListSignaturePublicKey specifies a public key that's used
// to authenticate the remote server list payload. This value is supplied
// by and depends on the Psiphon Network, and is typically embedded in the
// client binary.
RemoteServerListSignaturePublicKey string
// DisableRemoteServerListFetcher disables fetching remote server lists.
// This is used for special case temporary tunnels.
DisableRemoteServerListFetcher bool
// FetchRemoteServerListRetryPeriodMilliseconds specifies the delay before
// resuming a remote server list download after a failure. If omitted, a
// default value is used. This value is typical overridden for testing.
FetchRemoteServerListRetryPeriodMilliseconds *int
// ObfuscatedServerListRootURLs is a list of URLs which specify root
// locations from which to fetch obfuscated server list files. This value
// is supplied by and depends on the Psiphon Network, and is typically
// embedded in the client binary. All URLs must point to the same entity
// with the same ETag. At least one DownloadURL must have
// OnlyAfterAttempts = 0.
ObfuscatedServerListRootURLs parameters.TransferURLs
// EnableUpgradeDownload indicates whether to check for and download
// upgrades. When set, UpgradeDownloadURLs and
// UpgradeDownloadClientVersionHeader must also be set. ClientPlatform
// and ClientVersion should also be set.
EnableUpgradeDownload bool
// UpgradeDownloadURLs is list of URLs which specify locations from which
// to download a host client upgrade file, when one is available. The core
// tunnel controller provides a resumable download facility which
// downloads this resource and emits a notice when complete. This value is
// supplied by and depends on the Psiphon Network, and is typically
// embedded in the client binary. All URLs must point to the same entity
// with the same ETag. At least one DownloadURL must have
// OnlyAfterAttempts = 0.
UpgradeDownloadURLs parameters.TransferURLs
// UpgradeDownloadClientVersionHeader specifies the HTTP header name for
// the entity at UpgradeDownloadURLs which specifies the client version
// (an integer value). A HEAD request may be made to check the version
// number available at UpgradeDownloadURLs.
// UpgradeDownloadClientVersionHeader is required when UpgradeDownloadURLs
// is specified.
UpgradeDownloadClientVersionHeader string
// FetchUpgradeRetryPeriodMilliseconds specifies the delay before resuming
// a client upgrade download after a failure. If omitted, a default value
// is used. This value is typical overridden for testing.
FetchUpgradeRetryPeriodMilliseconds *int
// EnableFeedbackUpload indicates whether to enable uploading feedback
// data. When set, FeedbackUploadURLs and FeedbackEncryptionPublicKey
// must also be set.
EnableFeedbackUpload bool
// FeedbackUploadURLs is a list of SecureTransferURLs which specify
// locations where feedback data can be uploaded, pairing with each
// location a public key with which to encrypt the feedback data. This
// value is supplied by and depends on the Psiphon Network, and is
// typically embedded in the client binary. At least one TransferURL must
// have OnlyAfterAttempts = 0.
FeedbackUploadURLs parameters.TransferURLs
// FeedbackEncryptionPublicKey is a default base64-encoded, RSA public key
// value used to encrypt feedback data. Used when uploading feedback with a
// TransferURL which has no public key value configured, i.e.
// B64EncodedPublicKey = "".
FeedbackEncryptionPublicKey string
// TrustedCACertificatesFilename specifies a file containing trusted CA
// certs. When set, this toggles use of the trusted CA certs, specified in
// TrustedCACertificatesFilename, for tunneled TLS connections that expect
// server certificates signed with public certificate authorities
// (currently, only upgrade downloads). This option is used with stock Go
// TLS in cases where Go may fail to obtain a list of root CAs from the
// operating system.
TrustedCACertificatesFilename string
// DisableSystemRootCAs, when true, disables loading system root CAs when
// verifying TLS certificates for all remote server list downloads, upgrade
// downloads, and feedback uploads. Each of these transfers has additional
// security at the payload level. Verifying TLS certificates is preferred,
// as an additional security and circumvention layer; set
// DisableSystemRootCAs only in cases where system root CAs cannot be
// loaded; for example, if unsupported (iOS < 12) or insufficient memory
// (VPN extension on iOS < 15).
DisableSystemRootCAs bool
// DisablePeriodicSshKeepAlive indicates whether to send an SSH keepalive
// every 1-2 minutes, when the tunnel is idle. If the SSH keepalive times
// out, the tunnel is considered to have failed.
DisablePeriodicSshKeepAlive bool
// DeviceLocation is the optional, reported location the host device is
// running in. This input value should be a string representing location
// geohash. The device location is reported to the server in the connected
// request and recorded for Psiphon stats.
DeviceLocation string
// DeviceRegion is the optional, reported region the host device is
// running in. This input value should be a ISO 3166-1 alpha-2 country
// code. The device region is reported to the server in the connected
// request and recorded for Psiphon stats.
//
// When provided, this value may be used, pre-connection, to select
// performance or circumvention optimization strategies for the given
// region.
DeviceRegion string
// EmitDiagnosticNotices indicates whether to output notices containing
// detailed information about the Psiphon session. As these notices may
// contain sensitive information, they should not be insecurely distributed
// or displayed to users. Default is off.
EmitDiagnosticNotices bool
// EmitDiagnosticNetworkParameters indicates whether to include network
// parameters in diagnostic notices. As these parameters are sensitive
// circumvention network information, they should not be insecurely
// distributed or displayed to users. Default is off.
EmitDiagnosticNetworkParameters bool
// EmitBytesTransferred indicates whether to emit periodic notices showing
// bytes sent and received.
EmitBytesTransferred bool
// EmitSLOKs indicates whether to emit notices for each seeded SLOK. As
// this could reveal user browsing activity, it's intended for debugging
// and testing only.
EmitSLOKs bool
// EmitRefractionNetworkingLogs indicates whether to emit gotapdance log
// messages to stdout. Note that gotapdance log messages do not conform to
// the Notice format standard. Default is off.
EmitRefractionNetworkingLogs bool
// EmitServerAlerts indicates whether to emit notices for server alerts.
EmitServerAlerts bool
// EmitClientAddress indicates whether to emit the client's public network
// address, IP and port, as seen by the server.
EmitClientAddress bool
// RateLimits specify throttling configuration for the tunnel.
RateLimits common.RateLimits
// PacketTunnelTunDeviceFileDescriptor specifies a tun device file
// descriptor to use for running a packet tunnel. When this value is > 0,
// a packet tunnel is established through the server and packets are
// relayed via the tun device file descriptor. The file descriptor is
// duped in NewController. When PacketTunnelTunDeviceFileDescriptor is
// set, TunnelPoolSize must be 1.
PacketTunnelTunFileDescriptor int
// PacketTunnelTransparentDNSIPv4Address is the IPv4 address of the DNS
// server configured by a VPN using a packet tunnel. All DNS packets
// destined to this DNS server are transparently redirected to the
// Psiphon server DNS.
PacketTunnelTransparentDNSIPv4Address string
// PacketTunnelTransparentDNSIPv6Address is the IPv6 address of the DNS
// server configured by a VPN using a packet tunnel. All DNS packets
// destined to this DNS server are transparently redirected to the
// Psiphon server DNS.
PacketTunnelTransparentDNSIPv6Address string
// SessionID specifies a client session ID to use in the Psiphon API. The
// session ID should be a randomly generated value that is used only for a
// single session, which is defined as the period between a user starting
// a Psiphon client and stopping the client.
//
// A session ID must be 32 hex digits (lower case). When blank, a random
// session ID is automatically generated. Supply a session ID when a
// single client session will cross multiple Controller instances.
SessionID string
// Authorizations is a list of encoded, signed access control
// authorizations that the client has obtained and will present to the
// server.
Authorizations []string
// ServerEntrySignaturePublicKey is a base64-encoded, ed25519 public
// key value used to verify individual server entry signatures. This value
// is supplied by and depends on the Psiphon Network, and is typically
// embedded in the client binary.
ServerEntrySignaturePublicKey string
// ExchangeObfuscationKey is a base64-encoded, NaCl secretbox key used to
// obfuscate server info exchanges between clients.
// Required for the exchange functionality.
ExchangeObfuscationKey string
// MigrateHomepageNoticesFilename migrates a homepage file from the path
// previously configured with setNoticeFiles to the new path for homepage
// files under the data root directory. The file specified by this config
// value will be moved to config.GetHomePageFilename().
//
// Note: see comment for config.Commit() for a description of how file
// migrations are performed.
//
// If not set, no migration operation will be performed.
MigrateHomepageNoticesFilename string
// MigrateRotatingNoticesFilename migrates notice files from the path
// previously configured with setNoticeFiles to the new path for notice
// files under the data root directory.
//
// MigrateRotatingNoticesFilename will be moved to
// config.GetNoticesFilename().
//
// MigrateRotatingNoticesFilename.1 will be moved to
// config.GetOldNoticesFilename().
//
// Note: see comment for config.Commit() for a description of how file
// migrations are performed.
//
// If not set, no migration operation will be performed.
MigrateRotatingNoticesFilename string
// MigrateDataStoreDirectory indicates the location of the datastore
// directory, as previously configured with the deprecated
// DataStoreDirectory config field. Datastore files found in the specified
// directory will be moved under the data root directory.
//
// Note: see comment for config.Commit() for a description of how file
// migrations are performed.
MigrateDataStoreDirectory string
// MigrateRemoteServerListDownloadFilename indicates the location of
// remote server list download files. The remote server list files found at
// the specified path will be moved under the data root directory.
//
// Note: see comment for config.Commit() for a description of how file
// migrations are performed.
MigrateRemoteServerListDownloadFilename string
// MigrateObfuscatedServerListDownloadDirectory indicates the location of
// the obfuscated server list downloads directory, as previously configured
// with ObfuscatedServerListDownloadDirectory. Obfuscated server list
// download files found in the specified directory will be moved under the
// data root directory.
//
// Warning: if the directory is empty after obfuscated server
// list files are moved, then it will be deleted.
//
// Note: see comment for config.Commit() for a description of how file
// migrations are performed.
MigrateObfuscatedServerListDownloadDirectory string
// MigrateUpgradeDownloadFilename indicates the location of downloaded
// application upgrade files. Downloaded upgrade files found at the
// specified path will be moved under the data root directory.
//
// Note: see comment for config.Commit() for a description of how file
// migrations are performed.
MigrateUpgradeDownloadFilename string
//
// The following parameters are deprecated.
//
// DataStoreDirectory is the directory in which to store the persistent
// database, which contains information such as server entries. By
// default, current working directory.
//
// Deprecated:
// Use MigrateDataStoreDirectory. When MigrateDataStoreDirectory
// is set, this parameter is ignored.
//
// DataStoreDirectory has been subsumed by the new data root directory,
// which is configured with DataRootDirectory. If set, datastore files
// found in the specified directory will be moved under the data root
// directory.
DataStoreDirectory string
// RemoteServerListDownloadFilename specifies a target filename for
// storing the remote server list download. Data is stored in co-located
// files (RemoteServerListDownloadFilename.part*) to allow for resumable
// downloading.
//
// Deprecated:
// Use MigrateRemoteServerListDownloadFilename. When
// MigrateRemoteServerListDownloadFilename is set, this parameter is
// ignored.
//
// If set, remote server list download files found at the specified path
// will be moved under the data root directory.
RemoteServerListDownloadFilename string
// ObfuscatedServerListDownloadDirectory specifies a target directory for
// storing the obfuscated remote server list downloads. Data is stored in
// co-located files (.part*) to allow for resumable
// downloading.
//
// Deprecated:
// Use MigrateObfuscatedServerListDownloadDirectory. When
// MigrateObfuscatedServerListDownloadDirectory is set, this parameter is
// ignored.
//
// If set, obfuscated server list download files found at the specified path
// will be moved under the data root directory.
ObfuscatedServerListDownloadDirectory string
// UpgradeDownloadFilename is the local target filename for an upgrade
// download. This parameter is required when UpgradeDownloadURLs (or
// UpgradeDownloadUrl) is specified. Data is stored in co-located files
// (UpgradeDownloadFilename.part*) to allow for resumable downloading.
//
// Deprecated:
// Use MigrateUpgradeDownloadFilename. When MigrateUpgradeDownloadFilename
// is set, this parameter is ignored.
//
// If set, upgrade download files found at the specified path will be moved
// under the data root directory.
UpgradeDownloadFilename string
// TunnelProtocol indicates which protocol to use. For the default, "",
// all protocols are used.
//
// Deprecated: Use LimitTunnelProtocols. When LimitTunnelProtocols is not
// nil, this parameter is ignored.
TunnelProtocol string
// Deprecated: Use CustomHeaders. When CustomHeaders is not nil, this
// parameter is ignored.
UpstreamProxyCustomHeaders http.Header
// RemoteServerListUrl is a URL which specifies a location to fetch out-
// of-band server entries. This facility is used when a tunnel cannot be
// established to known servers. This value is supplied by and depends on
// the Psiphon Network, and is typically embedded in the client binary.
//
// Deprecated: Use RemoteServerListURLs. When RemoteServerListURLs is not
// nil, this parameter is ignored.
RemoteServerListUrl string
// ObfuscatedServerListRootURL is a URL which specifies the root location
// from which to fetch obfuscated server list files. This value is
// supplied by and depends on the Psiphon Network, and is typically
// embedded in the client binary.
//
// Deprecated: Use ObfuscatedServerListRootURLs. When
// ObfuscatedServerListRootURLs is not nil, this parameter is ignored.
ObfuscatedServerListRootURL string
// UpgradeDownloadUrl specifies a URL from which to download a host client
// upgrade file, when one is available. The core tunnel controller
// provides a resumable download facility which downloads this resource
// and emits a notice when complete. This value is supplied by and depends
// on the Psiphon Network, and is typically embedded in the client binary.
//
// Deprecated: Use UpgradeDownloadURLs. When UpgradeDownloadURLs is not
// nil, this parameter is ignored.
UpgradeDownloadUrl string
//
// The following parameters are for testing purposes.
//
// TransformHostNameProbability is for testing purposes.
TransformHostNameProbability *float64
// FragmentorProbability and associated Fragmentor fields are for testing
// purposes.
FragmentorProbability *float64
FragmentorLimitProtocols []string
FragmentorMinTotalBytes *int
FragmentorMaxTotalBytes *int
FragmentorMinWriteBytes *int
FragmentorMaxWriteBytes *int
FragmentorMinDelayMicroseconds *int
FragmentorMaxDelayMicroseconds *int
// MeekTrafficShapingProbability and associated fields are for testing
// purposes.
MeekTrafficShapingProbability *float64
MeekTrafficShapingLimitProtocols []string
MeekMinTLSPadding *int
MeekMaxTLSPadding *int
MeekMinLimitRequestPayloadLength *int
MeekMaxLimitRequestPayloadLength *int
MeekRedialTLSProbability *float64
MeekAlternateCookieNameProbability *float64
MeekAlternateContentTypeProbability *float64
// ObfuscatedSSHAlgorithms and associated ObfuscatedSSH fields are for
// testing purposes. If specified, ObfuscatedSSHAlgorithms must have 4 SSH
// KEX elements in order: the kex algorithm, cipher, MAC, and server host
// key algorithm.
ObfuscatedSSHAlgorithms []string
ObfuscatedSSHMinPadding *int
ObfuscatedSSHMaxPadding *int
// LivenessTestMinUpstreamBytes and other LivenessTest fields are for
// testing purposes.
LivenessTestMinUpstreamBytes *int
LivenessTestMaxUpstreamBytes *int
LivenessTestMinDownstreamBytes *int
LivenessTestMaxDownstreamBytes *int
// ReplayCandidateCount and other Replay fields are for testing purposes.
ReplayCandidateCount *int
ReplayDialParametersTTLSeconds *int
ReplayTargetUpstreamBytes *int
ReplayTargetDownstreamBytes *int
ReplayTargetTunnelDurationSeconds *int
ReplayLaterRoundMoveToFrontProbability *float64
ReplayRetainFailedProbability *float64
ReplayIgnoreChangedConfigState *bool
// NetworkLatencyMultiplierMin and other NetworkLatencyMultiplier fields are
// for testing purposes.
NetworkLatencyMultiplierMin float64
NetworkLatencyMultiplierMax float64
NetworkLatencyMultiplierLambda float64
// UseOnlyCustomTLSProfiles and other TLS configuration fields are for
// testing purposes.
UseOnlyCustomTLSProfiles *bool
CustomTLSProfiles protocol.CustomTLSProfiles
SelectRandomizedTLSProfileProbability *float64
NoDefaultTLSSessionIDProbability *float64
DisableFrontingProviderTLSProfiles protocol.LabeledTLSProfiles
// ClientBurstUpstreamTargetBytes and other burst metric fields are for
// testing purposes.
ClientBurstUpstreamTargetBytes *int
ClientBurstUpstreamDeadlineMilliseconds *int
ClientBurstDownstreamTargetBytes *int
ClientBurstDownstreamDeadlineMilliseconds *int
// ApplicationParameters is for testing purposes.
ApplicationParameters parameters.KeyValues
// CustomHostNameRegexes and other custom host name fields are for testing
// purposes.
CustomHostNameRegexes []string
CustomHostNameProbability *float64
CustomHostNameLimitProtocols []string
// ConjureCachedRegistrationTTLSeconds and other Conjure fields are for
// testing purposes.
ConjureCachedRegistrationTTLSeconds *int
ConjureAPIRegistrarBidirectionalURL string
ConjureAPIRegistrarFrontingSpecs parameters.FrontingSpecs
ConjureAPIRegistrarMinDelayMilliseconds *int
ConjureAPIRegistrarMaxDelayMilliseconds *int
ConjureDecoyRegistrarProbability *float64
ConjureDecoyRegistrarWidth *int
ConjureDecoyRegistrarMinDelayMilliseconds *int
ConjureDecoyRegistrarMaxDelayMilliseconds *int
ConjureEnableIPv6Dials *bool
ConjureEnablePortRandomization *bool
ConjureEnableRegistrationOverrides *bool
ConjureLimitTransports protocol.ConjureTransports
ConjureSTUNServerAddresses []string
ConjureDTLSEmptyInitialPacketProbability *float64
// HoldOffTunnelMinDurationMilliseconds and other HoldOffTunnel fields are
// for testing purposes.
HoldOffTunnelMinDurationMilliseconds *int
HoldOffTunnelMaxDurationMilliseconds *int
HoldOffTunnelProtocols []string
HoldOffTunnelFrontingProviderIDs []string
HoldOffTunnelProbability *float64
// RestrictFrontingProviderIDs and other RestrictFrontingProviderIDs fields
// are for testing purposes.
RestrictFrontingProviderIDs []string
RestrictFrontingProviderIDsClientProbability *float64
// HoldOffDirectTunnelMinDurationMilliseconds and other HoldOffDirect
// fields are for testing purposes.
HoldOffDirectTunnelMinDurationMilliseconds *int
HoldOffDirectTunnelMaxDurationMilliseconds *int
HoldOffDirectTunnelProviderRegions map[string][]string
HoldOffDirectTunnelProbability *float64
// RestrictDirectProviderRegions and other RestrictDirect fields are for
// testing purposes.
RestrictDirectProviderRegions map[string][]string
RestrictDirectProviderIDsClientProbability *float64
// UpstreamProxyAllowAllServerEntrySources is for testing purposes.
UpstreamProxyAllowAllServerEntrySources *bool
// LimitTunnelDialPortNumbers is for testing purposes.
LimitTunnelDialPortNumbers parameters.TunnelProtocolPortLists
// QUICDisablePathMTUDiscoveryProbability is for testing purposes.
QUICDisablePathMTUDiscoveryProbability *float64
// DNSResolverAttemptsPerServer and other DNSResolver fields are for
// testing purposes.
DNSResolverAttemptsPerServer *int
DNSResolverAttemptsPerPreferredServer *int
DNSResolverRequestTimeoutMilliseconds *int
DNSResolverAwaitTimeoutMilliseconds *int
DNSResolverPreresolvedIPAddressCIDRs parameters.LabeledCIDRs
DNSResolverPreresolvedIPAddressProbability *float64
DNSResolverAlternateServers []string
DNSResolverPreferredAlternateServers []string
DNSResolverPreferAlternateServerProbability *float64
DNSResolverProtocolTransformSpecs transforms.Specs
DNSResolverProtocolTransformScopedSpecNames transforms.ScopedSpecNames
DNSResolverProtocolTransformProbability *float64
DNSResolverIncludeEDNS0Probability *float64
DNSResolverCacheExtensionInitialTTLMilliseconds *int
DNSResolverCacheExtensionVerifiedTTLMilliseconds *int
DirectHTTPProtocolTransformSpecs transforms.Specs
DirectHTTPProtocolTransformScopedSpecNames transforms.ScopedSpecNames
DirectHTTPProtocolTransformProbability *float64
FrontedHTTPProtocolTransformSpecs transforms.Specs
FrontedHTTPProtocolTransformScopedSpecNames transforms.ScopedSpecNames
FrontedHTTPProtocolTransformProbability *float64
OSSHObfuscatorSeedTransformSpecs transforms.Specs
OSSHObfuscatorSeedTransformScopedSpecNames transforms.ScopedSpecNames
OSSHObfuscatorSeedTransformProbability *float64
ObfuscatedQUICNonceTransformSpecs transforms.Specs
ObfuscatedQUICNonceTransformScopedSpecNames transforms.ScopedSpecNames
ObfuscatedQUICNonceTransformProbability *float64
// OSSHPrefix parameters are for testing purposes only.
OSSHPrefixSpecs transforms.Specs
OSSHPrefixScopedSpecNames transforms.ScopedSpecNames
OSSHPrefixProbability *float64
OSSHPrefixSplitMinDelayMilliseconds *int
OSSHPrefixSplitMaxDelayMilliseconds *int
OSSHPrefixEnableFragmentor *bool
// TLSTunnelTrafficShapingProbability and associated fields are for testing.
TLSTunnelTrafficShapingProbability *float64
TLSTunnelMinTLSPadding *int
TLSTunnelMaxTLSPadding *int
// TLSFragmentClientHello fields are for testing purposes only.
TLSFragmentClientHelloProbability *float64
TLSFragmentClientHelloLimitProtocols []string
// AdditionalParameters is used for testing.
AdditionalParameters string
// SteeringIP fields are for testing purposes only.
SteeringIPCacheTTLSeconds *int
SteeringIPCacheMaxEntries *int
SteeringIPProbability *float64
// params is the active parameters.Parameters with defaults, config values,
// and, optionally, tactics applied.
//
// New tactics must be applied by calling Config.SetParameters; calling
// params.Set directly will fail to add config values.
params *parameters.Parameters
dialParametersHash []byte
dynamicConfigMutex sync.Mutex
sponsorID string
authorizations []string
deviceBinder DeviceBinder
networkIDGetter NetworkIDGetter
clientFeatures []string
resolverMutex sync.Mutex
resolver *resolver.Resolver
committed bool
loadTimestamp string
}
// Config field which specifies if notice files should be used and at which
// frequency and size they should be rotated.
//
// If either RotatingFileSize or RotatingSyncFrequency are <= 0, default values
// are used.
//
// See comment for setNoticeFiles in notice.go for further details.
type UseNoticeFiles struct {
RotatingFileSize int
RotatingSyncFrequency int
}
// LoadConfig parses a JSON format Psiphon config JSON string and returns a
// Config struct populated with config values.
//
// The Config struct may then be programmatically populated with additional
// values, including callbacks such as DeviceBinder.
//
// Before using the Config, Commit must be called, which will perform further
// validation and initialize internal data structures.
func LoadConfig(configJson []byte) (*Config, error) {
var config Config
err := json.Unmarshal(configJson, &config)
if err != nil {
return nil, errors.Trace(err)
}
config.loadTimestamp = common.TruncateTimestampToHour(
common.GetCurrentTimestamp())
return &config, nil
}
// IsCommitted checks if Commit was called.
func (config *Config) IsCommitted() bool {
return config.committed
}
// Commit validates Config fields finalizes initialization.
//
// Config fields should not be set after calling Config, as any changes may
// not be reflected in internal data structures.
//
// If migrateFromLegacyFields is set to true, then an attempt to migrate from
// legacy fields is made.
//
// Migration from legacy fields:
// Config fields of the naming Migrate* (e.g. MigrateDataStoreDirectory) specify
// a file migration operation which should be performed. These fields correspond
// to deprecated fields, which previously could be used to specify where Psiphon
// stored different sets of persistent files (e.g. MigrateDataStoreDirectory
// corresponds to the deprecated field DataStoreDirectory).
//
// Psiphon now stores all persistent data under the configurable
// DataRootDirectory (see Config.DataRootDirectory). The deprecated fields, and
// corresponding Migrate* fields, are now used to specify the file or directory
// path where, or under which, persistent files and directories created by
// previous versions of Psiphon exist, so they can be moved under the
// DataRootDirectory.
//
// For each migration operation:
// - In the case of directories that could have defaulted to the current working
// directory, persistent files and directories created by Psiphon are
// precisely targeted to avoid moving files which were not created by Psiphon.
// - If no file is found at the specified path, or an error is encountered while
// migrating the file, then an error is logged and execution continues
// normally.
//
// A sentinel file which signals that file migration has been completed, and
// should not be attempted again, is created under DataRootDirectory after one
// full pass through Commit(), regardless of whether file migration succeeds or
// fails. It is better to not endlessly retry file migrations on each Commit()
// because file system errors are expected to be rare and persistent files will
// be re-populated over time.
func (config *Config) Commit(migrateFromLegacyFields bool) error {
// Apply any additional parameters first
additionalParametersInfoMsgs, err := config.applyAdditionalParameters()
if err != nil {
return errors.TraceMsg(err, "failed to apply additional parameters")
}
// Do SetEmitDiagnosticNotices first, to ensure config file errors are
// emitted.
if config.EmitDiagnosticNotices {
SetEmitDiagnosticNotices(
true, config.EmitDiagnosticNetworkParameters)
}
// Migrate and set notice files before any operations that may emit an
// error. This is to ensure config file errors are written to file when
// notice files are configured with config.UseNoticeFiles.
//
// Note:
// Errors encountered while configuring the data directory cannot be written
// to notice files. This is because notices files are created within the
// data directory.
if config.DataRootDirectory == "" {
wd, err := os.Getwd()
if err != nil {
return errors.Trace(common.RedactFilePathsError(err))
}
config.DataRootDirectory = wd
}
// Create root directory
dataDirectoryPath := config.GetPsiphonDataDirectory()
if !common.FileExists(dataDirectoryPath) {
err := os.Mkdir(dataDirectoryPath, os.ModePerm)
if err != nil {
return errors.Tracef(
"failed to create datastore directory with error: %s",
common.RedactFilePathsError(err, dataDirectoryPath))
}
}
// Check if the migration from legacy config fields has already been
// completed. See the Migrate* config fields for more details.
migrationCompleteFilePath := filepath.Join(config.GetPsiphonDataDirectory(), "migration_complete")
needMigration := !common.FileExists(migrationCompleteFilePath)
// Collect notices to emit them after notice files are set
var noticeMigrationAlertMsgs []string
var noticeMigrationInfoMsgs []string
// Migrate notices first to ensure notice files are used for notices if
// UseNoticeFiles is set.
homepageFilePath := config.GetHomePageFilename()
noticesFilePath := config.GetNoticesFilename()
if migrateFromLegacyFields {
if needMigration {
// Move notice files that exist at legacy file paths under the data root
// directory.
noticeMigrationInfoMsgs = append(noticeMigrationInfoMsgs, "Config migration: need migration")
noticeMigrations := migrationsFromLegacyNoticeFilePaths(config)
successfulMigrations := 0
for _, migration := range noticeMigrations {
err := DoFileMigration(migration)
if err != nil {
alertMsg := fmt.Sprintf("Config migration: %s", errors.Trace(err))
noticeMigrationAlertMsgs = append(noticeMigrationAlertMsgs, alertMsg)
} else {
successfulMigrations += 1
}
}
infoMsg := fmt.Sprintf("Config migration: %d/%d notice files successfully migrated", successfulMigrations, len(noticeMigrations))
noticeMigrationInfoMsgs = append(noticeMigrationInfoMsgs, infoMsg)
} else {
noticeMigrationInfoMsgs = append(noticeMigrationInfoMsgs, "Config migration: migration already completed")
}
}
if config.UseNoticeFiles != nil {
setNoticeFiles(
homepageFilePath,
noticesFilePath,
config.UseNoticeFiles.RotatingFileSize,
config.UseNoticeFiles.RotatingSyncFrequency)
}
// Emit notices now that notice files are set if configured
for _, msg := range additionalParametersInfoMsgs {
NoticeInfo(msg)
}
for _, msg := range noticeMigrationAlertMsgs {
NoticeWarning(msg)
}
for _, msg := range noticeMigrationInfoMsgs {
NoticeInfo(msg)
}
// Promote legacy fields.
if config.CustomHeaders == nil {
config.CustomHeaders = config.UpstreamProxyCustomHeaders
config.UpstreamProxyCustomHeaders = nil
}
if config.RemoteServerListUrl != "" && config.RemoteServerListURLs == nil {
config.RemoteServerListURLs = promoteLegacyTransferURL(config.RemoteServerListUrl)
}
if config.ObfuscatedServerListRootURL != "" && config.ObfuscatedServerListRootURLs == nil {
config.ObfuscatedServerListRootURLs = promoteLegacyTransferURL(config.ObfuscatedServerListRootURL)
}
if config.UpgradeDownloadUrl != "" && config.UpgradeDownloadURLs == nil {
config.UpgradeDownloadURLs = promoteLegacyTransferURL(config.UpgradeDownloadUrl)
}
if config.TunnelProtocol != "" && len(config.LimitTunnelProtocols) == 0 {
config.LimitTunnelProtocols = []string{config.TunnelProtocol}
}
if config.DataStoreDirectory != "" && config.MigrateDataStoreDirectory == "" {
config.MigrateDataStoreDirectory = config.DataStoreDirectory
}
if config.RemoteServerListDownloadFilename != "" && config.MigrateRemoteServerListDownloadFilename == "" {
config.MigrateRemoteServerListDownloadFilename = config.RemoteServerListDownloadFilename
}
if config.ObfuscatedServerListDownloadDirectory != "" && config.MigrateObfuscatedServerListDownloadDirectory == "" {
config.MigrateObfuscatedServerListDownloadDirectory = config.ObfuscatedServerListDownloadDirectory
}
if config.UpgradeDownloadFilename != "" && config.MigrateUpgradeDownloadFilename == "" {
config.MigrateUpgradeDownloadFilename = config.UpgradeDownloadFilename
}
// Supply default values.
// Create datastore directory.
dataStoreDirectoryPath := config.GetDataStoreDirectory()
if !common.FileExists(dataStoreDirectoryPath) {
err := os.Mkdir(dataStoreDirectoryPath, os.ModePerm)
if err != nil {
return errors.Tracef(
"failed to create datastore directory with error: %s",
common.RedactFilePathsError(err, dataStoreDirectoryPath))
}
}
// Create OSL directory.
oslDirectoryPath := config.GetObfuscatedServerListDownloadDirectory()
if !common.FileExists(oslDirectoryPath) {
err := os.Mkdir(oslDirectoryPath, os.ModePerm)
if err != nil {
return errors.Tracef(
"failed to create osl directory with error: %s",
common.RedactFilePathsError(err, oslDirectoryPath))
}
}
if config.ClientVersion == "" {
config.ClientVersion = "0"
}
if config.TunnelPoolSize == 0 {
config.TunnelPoolSize = TUNNEL_POOL_SIZE
}
// Validate config fields.
if !common.FileExists(config.DataRootDirectory) {
return errors.TraceNew("DataRootDirectory does not exist")
}
if config.PropagationChannelId == "" {
return errors.TraceNew("propagation channel ID is missing from the configuration file")
}
if config.SponsorId == "" {
return errors.TraceNew("sponsor ID is missing from the configuration file")
}
_, err = strconv.Atoi(config.ClientVersion)
if err != nil {
return errors.Tracef("invalid client version: %s", err)
}
if !common.Contains(
[]string{"", protocol.PSIPHON_SSH_API_PROTOCOL, protocol.PSIPHON_WEB_API_PROTOCOL},
config.TargetApiProtocol) {
return errors.TraceNew("invalid TargetApiProtocol")
}
if !config.DisableRemoteServerListFetcher {
if config.RemoteServerListURLs != nil {
if config.RemoteServerListSignaturePublicKey == "" {
return errors.TraceNew("missing RemoteServerListSignaturePublicKey")
}
}
if config.ObfuscatedServerListRootURLs != nil {
if config.RemoteServerListSignaturePublicKey == "" {
return errors.TraceNew("missing RemoteServerListSignaturePublicKey")
}
}
}
if config.EnableUpgradeDownload {
if len(config.UpgradeDownloadURLs) == 0 {
return errors.TraceNew("missing UpgradeDownloadURLs")
}
if config.UpgradeDownloadClientVersionHeader == "" {
return errors.TraceNew("missing UpgradeDownloadClientVersionHeader")
}
}
if config.EnableFeedbackUpload {
if len(config.FeedbackUploadURLs) == 0 {
return errors.TraceNew("missing FeedbackUploadURLs")
}
if config.FeedbackEncryptionPublicKey == "" {
return errors.TraceNew("missing FeedbackEncryptionPublicKey")
}
}
// This constraint is expected by logic in Controller.runTunnels().
if config.PacketTunnelTunFileDescriptor > 0 && config.TunnelPoolSize != 1 {
return errors.TraceNew("packet tunnel mode requires TunnelPoolSize to be 1")
}
// SessionID must be PSIPHON_API_CLIENT_SESSION_ID_LENGTH lowercase hex-encoded bytes.
if config.SessionID == "" {
sessionID, err := MakeSessionId()
if err != nil {
return errors.Trace(err)
}
config.SessionID = sessionID
}
if len(config.SessionID) != 2*protocol.PSIPHON_API_CLIENT_SESSION_ID_LENGTH ||
-1 != strings.IndexFunc(config.SessionID, func(c rune) bool {
return !unicode.Is(unicode.ASCII_Hex_Digit, c) || unicode.IsUpper(c)
}) {
return errors.TraceNew("invalid SessionID")
}
config.params, err = parameters.NewParameters(
func(err error) {
NoticeWarning("Parameters getValue failed: %s", err)
})
if err != nil {
return errors.Trace(err)
}
if config.ObfuscatedSSHAlgorithms != nil &&
len(config.ObfuscatedSSHAlgorithms) != 4 {
// TODO: validate each algorithm?
return errors.TraceNew("invalid ObfuscatedSSHAlgorithms")
}
// parametersParameters.Set will validate the config fields applied to
// parametersParameters.
err = config.SetParameters("", false, nil)
if err != nil {
return errors.Trace(err)
}
// Calculate and set the dial parameters hash. After this point, related
// config fields must not change.
config.setDialParametersHash()
// Set defaults for dynamic config fields.
config.SetDynamicConfig(config.SponsorId, config.Authorizations)
// Initialize config.deviceBinder and config.config.networkIDGetter. These
// wrap config.DeviceBinder and config.NetworkIDGetter/NetworkID with
// loggers.
//
// New variables are set to avoid mutating input config fields.
// Internally, code must use config.deviceBinder and
// config.networkIDGetter and not the input/exported fields.
if config.DeviceBinder != nil {
config.deviceBinder = newLoggingDeviceBinder(config.DeviceBinder)
}
networkIDGetter := config.NetworkIDGetter
if networkIDGetter == nil {
// Limitation: unlike NetworkIDGetter, which calls back to platform APIs
// this method of network identification is not dynamic and will not reflect
// network changes that occur while running.
if config.NetworkID != "" {
networkIDGetter = newStaticNetworkGetter(config.NetworkID)
} else {
networkIDGetter = newStaticNetworkGetter("UNKNOWN")
}
}
config.networkIDGetter = newLoggingNetworkIDGetter(networkIDGetter)
// Initialize config.clientFeatures, which adds feature names on top of
// those specified by the host application in config.ClientFeatures.
config.clientFeatures = config.ClientFeatures
feature := "VPN"
if config.DeviceBinder != nil && !common.Contains(config.clientFeatures, feature) {
config.clientFeatures = append(config.clientFeatures, feature)
}
// Migrate from old config fields. This results in files being moved under
// a config specified data root directory.
if migrateFromLegacyFields && needMigration {
// If unset, set MigrateDataStoreDirectory to the previous default value for
// DataStoreDirectory to ensure that datastore files are migrated.
if config.MigrateDataStoreDirectory == "" {
wd, err := os.Getwd()
if err != nil {
return errors.Trace(err)
}
NoticeInfo("MigrateDataStoreDirectory unset, using working directory")
config.MigrateDataStoreDirectory = wd
}
// Move files that exist at legacy file paths under the data root
// directory.
migrations, err := migrationsFromLegacyFilePaths(config)
if err != nil {
return errors.Trace(err)
}
// Do migrations
successfulMigrations := 0
for _, migration := range migrations {
err := DoFileMigration(migration)
if err != nil {
NoticeWarning("Config migration: %s", errors.Trace(err))
} else {
successfulMigrations += 1
}
}
NoticeInfo(fmt.Sprintf(
"Config migration: %d/%d legacy files successfully migrated",
successfulMigrations, len(migrations)))
// Remove OSL directory if empty
if config.MigrateObfuscatedServerListDownloadDirectory != "" {
files, err := ioutil.ReadDir(config.MigrateObfuscatedServerListDownloadDirectory)
if err != nil {
NoticeWarning(
"Error reading OSL directory: %s",
errors.Trace(common.RedactFilePathsError(err, config.MigrateObfuscatedServerListDownloadDirectory)))
} else if len(files) == 0 {
err := os.Remove(config.MigrateObfuscatedServerListDownloadDirectory)
if err != nil {
NoticeWarning(
"Error deleting empty OSL directory: %s",
errors.Trace(common.RedactFilePathsError(err, config.MigrateObfuscatedServerListDownloadDirectory)))
}
}
}
f, err := os.Create(migrationCompleteFilePath)
if err != nil {
NoticeWarning(
"Config migration: failed to create migration completed file with error %s",
errors.Trace(common.RedactFilePathsError(err, migrationCompleteFilePath)))
} else {
NoticeInfo("Config migration: completed")
f.Close()
}
}
config.committed = true
return nil
}
// GetParameters returns the current parameters.Parameters.
func (config *Config) GetParameters() *parameters.Parameters {
return config.params
}
// SetParameters resets the parameters.Parameters to the default values,
// applies any config file values, and then applies the input parameters (from
// tactics, etc.)
//
// Set skipOnError to false when initially applying only config values, as
// this will validate the values and should fail. Set skipOnError to true when
// applying tactics to ignore invalid or unknown parameter values from tactics.
//
// In the case of applying tactics, do not call Config.parameters.Set
// directly as this will not first apply config values.
//
// If there is an error, the existing Config.parameters are left
// entirely unmodified.
func (config *Config) SetParameters(tag string, skipOnError bool, applyParameters map[string]interface{}) error {
setParameters := []map[string]interface{}{config.makeConfigParameters()}
if applyParameters != nil {
setParameters = append(setParameters, applyParameters)
}
counts, err := config.params.Set(tag, skipOnError, setParameters...)
if err != nil {
return errors.Trace(err)
}
NoticeInfo("applied %v parameters with tag '%s'", counts, tag)
// Emit certain individual parameter values for quick reference in diagnostics.
p := config.params.Get()
NoticeInfo(
"NetworkLatencyMultiplier Min/Max/Lambda: %f/%f/%f",
p.Float(parameters.NetworkLatencyMultiplierMin),
p.Float(parameters.NetworkLatencyMultiplierMax),
p.Float(parameters.NetworkLatencyMultiplierLambda))
// Application Parameters are feature flags/config info, delivered as Client
// Parameters via tactics/etc., to be communicated to the outer application.
// Emit these now, as notices.
if p.WeightedCoinFlip(parameters.ApplicationParametersProbability) {
NoticeApplicationParameters(p.KeyValues(parameters.ApplicationParameters))
}
return nil
}
// SetResolver sets the current resolver.
func (config *Config) SetResolver(resolver *resolver.Resolver) {
config.resolverMutex.Lock()
defer config.resolverMutex.Unlock()
config.resolver = resolver
}
// GetResolver returns the current resolver. May return nil.
func (config *Config) GetResolver() *resolver.Resolver {
config.resolverMutex.Lock()
defer config.resolverMutex.Unlock()
return config.resolver
}
// SetDynamicConfig sets the current client sponsor ID and authorizations.
// Invalid values for sponsor ID are ignored. The caller must not modify the
// input authorizations slice.
func (config *Config) SetDynamicConfig(sponsorID string, authorizations []string) {
config.dynamicConfigMutex.Lock()
defer config.dynamicConfigMutex.Unlock()
if sponsorID != "" {
config.sponsorID = sponsorID
}
config.authorizations = authorizations
}
// GetSponsorID returns the current client sponsor ID.
func (config *Config) GetSponsorID() string {
config.dynamicConfigMutex.Lock()
defer config.dynamicConfigMutex.Unlock()
return config.sponsorID
}
// IsSplitTunnelEnabled indicates if split tunnel mode is enabled, either for
// the client's own country, a specified list of countries, or both.
func (config *Config) IsSplitTunnelEnabled() bool {
return config.SplitTunnelOwnRegion || len(config.SplitTunnelRegions) > 0
}
// GetAuthorizations returns the current client authorizations.
// The caller must not modify the returned slice.
func (config *Config) GetAuthorizations() []string {
config.dynamicConfigMutex.Lock()
defer config.dynamicConfigMutex.Unlock()
return config.authorizations
}
// GetPsiphonDataDirectory returns the directory under which all persistent
// files should be stored. This directory is created under
// config.DataRootDirectory. The motivation for an additional directory is that
// config.DataRootDirectory defaults to the current working directory, which may
// include non-tunnel-core files that should be excluded from directory-spanning
// operations (e.g. excluding all tunnel-core files from backup).
func (config *Config) GetPsiphonDataDirectory() string {
return filepath.Join(config.DataRootDirectory, PsiphonDataDirectoryName)
}
// GetHomePageFilename the path where the homepage notices file will be created.
func (config *Config) GetHomePageFilename() string {
return filepath.Join(config.GetPsiphonDataDirectory(), HomepageFilename)
}
// GetNoticesFilename returns the path where the notices file will be created.
// When the file is rotated it will be moved to config.GetOldNoticesFilename().
func (config *Config) GetNoticesFilename() string {
return filepath.Join(config.GetPsiphonDataDirectory(), NoticesFilename)
}
// GetOldNoticeFilename returns the path where the rotated notices file will be
// created.
func (config *Config) GetOldNoticesFilename() string {
return filepath.Join(config.GetPsiphonDataDirectory(), OldNoticesFilename)
}
// GetDataStoreDirectory returns the directory in which the persistent database
// will be stored. Created in Config.Commit(). The persistent database contains
// information such as server entries.
func (config *Config) GetDataStoreDirectory() string {
return filepath.Join(config.GetPsiphonDataDirectory(), "datastore")
}
// GetObfuscatedServerListDownloadDirectory returns the directory in which
// obfuscated remote server list downloads will be stored. Created in
// Config.Commit().
func (config *Config) GetObfuscatedServerListDownloadDirectory() string {
return filepath.Join(config.GetPsiphonDataDirectory(), "osl")
}
// GetRemoteServerListDownloadFilename returns the filename where the remote
// server list download will be stored. Data is stored in co-located files
// (RemoteServerListDownloadFilename.part*) to allow for resumable downloading.
func (config *Config) GetRemoteServerListDownloadFilename() string {
return filepath.Join(config.GetPsiphonDataDirectory(), "remote_server_list")
}
// GetUpgradeDownloadFilename specifies the filename where upgrade downloads
// will be stored. This filename is valid when UpgradeDownloadURLs
// (or UpgradeDownloadUrl) is specified. Data is stored in co-located files
// (UpgradeDownloadFilename.part*) to allow for resumable downloading.
func (config *Config) GetUpgradeDownloadFilename() string {
return filepath.Join(config.GetPsiphonDataDirectory(), UpgradeDownloadFilename)
}
// UseUpstreamProxy indicates if an upstream proxy has been
// configured.
func (config *Config) UseUpstreamProxy() bool {
return config.UpstreamProxyURL != ""
}
// GetNetworkID returns the current network ID. When NetworkIDGetter
// is set, this calls into the host application; otherwise, a default
// value is returned.
func (config *Config) GetNetworkID() string {
return config.networkIDGetter.GetNetworkID()
}
func (config *Config) makeConfigParameters() map[string]interface{} {
// Build set of config values to apply to parameters.
//
// Note: names of some config fields such as
// StaggerConnectionWorkersMilliseconds and LimitMeekBufferSizes have
// changed in the parameters. The existing config fields are retained for
// backwards compatibility.
applyParameters := make(map[string]interface{})
// To support platform clients that configure NetworkLatencyMultiplier, set
// the NetworkLatencyMultiplierMin/NetworkLatencyMultiplierMax range to the
// specified value. Also set the older NetworkLatencyMultiplier tactic, since
// that will be used in the case of replaying with dial parameters persisted
// by an older client version.
if config.NetworkLatencyMultiplier > 0.0 {
applyParameters[parameters.NetworkLatencyMultiplier] = config.NetworkLatencyMultiplier
applyParameters[parameters.NetworkLatencyMultiplierMin] = config.NetworkLatencyMultiplier
applyParameters[parameters.NetworkLatencyMultiplierMax] = config.NetworkLatencyMultiplier
}
if config.NetworkLatencyMultiplierMin > 0.0 {
applyParameters[parameters.NetworkLatencyMultiplierMin] = config.NetworkLatencyMultiplierMin
}
if config.NetworkLatencyMultiplierMax > 0.0 {
applyParameters[parameters.NetworkLatencyMultiplierMax] = config.NetworkLatencyMultiplierMax
}
if config.NetworkLatencyMultiplierLambda > 0.0 {
applyParameters[parameters.NetworkLatencyMultiplierLambda] = config.NetworkLatencyMultiplierLambda
}
if len(config.LimitTunnelProtocols) > 0 {
applyParameters[parameters.LimitTunnelProtocols] = protocol.TunnelProtocols(config.LimitTunnelProtocols)
}
if len(config.InitialLimitTunnelProtocols) > 0 && config.InitialLimitTunnelProtocolsCandidateCount > 0 {
applyParameters[parameters.InitialLimitTunnelProtocols] = protocol.TunnelProtocols(config.InitialLimitTunnelProtocols)
applyParameters[parameters.InitialLimitTunnelProtocolsCandidateCount] = config.InitialLimitTunnelProtocolsCandidateCount
}
if len(config.LimitTLSProfiles) > 0 {
applyParameters[parameters.LimitTLSProfiles] = protocol.TunnelProtocols(config.LimitTLSProfiles)
}
if len(config.LimitQUICVersions) > 0 {
applyParameters[parameters.LimitQUICVersions] = protocol.QUICVersions(config.LimitQUICVersions)
}
if config.EstablishTunnelTimeoutSeconds != nil {
applyParameters[parameters.EstablishTunnelTimeout] = fmt.Sprintf("%ds", *config.EstablishTunnelTimeoutSeconds)
}
if config.EstablishTunnelServerAffinityGracePeriodMilliseconds != nil {
applyParameters[parameters.EstablishTunnelServerAffinityGracePeriod] = fmt.Sprintf("%dms", *config.EstablishTunnelServerAffinityGracePeriodMilliseconds)
}
if config.EstablishTunnelPausePeriodSeconds != nil {
applyParameters[parameters.EstablishTunnelPausePeriod] = fmt.Sprintf("%ds", *config.EstablishTunnelPausePeriodSeconds)
}
if config.ConnectionWorkerPoolSize != 0 {
applyParameters[parameters.ConnectionWorkerPoolSize] = config.ConnectionWorkerPoolSize
}
if config.TunnelPoolSize != 0 {
applyParameters[parameters.TunnelPoolSize] = config.TunnelPoolSize
}
if config.StaggerConnectionWorkersMilliseconds > 0 {
applyParameters[parameters.StaggerConnectionWorkersPeriod] = fmt.Sprintf("%dms", config.StaggerConnectionWorkersMilliseconds)
}
if config.LimitIntensiveConnectionWorkers > 0 {
applyParameters[parameters.LimitIntensiveConnectionWorkers] = config.LimitIntensiveConnectionWorkers
}
applyParameters[parameters.MeekLimitBufferSizes] = config.LimitMeekBufferSizes
applyParameters[parameters.IgnoreHandshakeStatsRegexps] = config.IgnoreHandshakeStatsRegexps
if config.EstablishTunnelTimeoutSeconds != nil {
applyParameters[parameters.EstablishTunnelTimeout] = fmt.Sprintf("%ds", *config.EstablishTunnelTimeoutSeconds)
}
if config.FetchRemoteServerListRetryPeriodMilliseconds != nil {
applyParameters[parameters.FetchRemoteServerListRetryPeriod] = fmt.Sprintf("%dms", *config.FetchRemoteServerListRetryPeriodMilliseconds)
}
if config.FetchUpgradeRetryPeriodMilliseconds != nil {
applyParameters[parameters.FetchUpgradeRetryPeriod] = fmt.Sprintf("%dms", *config.FetchUpgradeRetryPeriodMilliseconds)
}
if !config.DisableRemoteServerListFetcher {
if config.RemoteServerListURLs != nil {
applyParameters[parameters.RemoteServerListSignaturePublicKey] = config.RemoteServerListSignaturePublicKey
applyParameters[parameters.RemoteServerListURLs] = config.RemoteServerListURLs
}
if config.ObfuscatedServerListRootURLs != nil {
applyParameters[parameters.RemoteServerListSignaturePublicKey] = config.RemoteServerListSignaturePublicKey
applyParameters[parameters.ObfuscatedServerListRootURLs] = config.ObfuscatedServerListRootURLs
}
}
if config.EnableUpgradeDownload {
applyParameters[parameters.UpgradeDownloadURLs] = config.UpgradeDownloadURLs
applyParameters[parameters.UpgradeDownloadClientVersionHeader] = config.UpgradeDownloadClientVersionHeader
}
if config.EnableFeedbackUpload {
applyParameters[parameters.FeedbackUploadURLs] = config.FeedbackUploadURLs
applyParameters[parameters.FeedbackEncryptionPublicKey] = config.FeedbackEncryptionPublicKey
}
applyParameters[parameters.TunnelRateLimits] = config.RateLimits
if config.TransformHostNameProbability != nil {
applyParameters[parameters.TransformHostNameProbability] = *config.TransformHostNameProbability
}
if config.FragmentorProbability != nil {
applyParameters[parameters.FragmentorProbability] = *config.FragmentorProbability
}
if len(config.FragmentorLimitProtocols) > 0 {
applyParameters[parameters.FragmentorLimitProtocols] = protocol.TunnelProtocols(config.FragmentorLimitProtocols)
}
if config.FragmentorMinTotalBytes != nil {
applyParameters[parameters.FragmentorMinTotalBytes] = *config.FragmentorMinTotalBytes
}
if config.FragmentorMaxTotalBytes != nil {
applyParameters[parameters.FragmentorMaxTotalBytes] = *config.FragmentorMaxTotalBytes
}
if config.FragmentorMinWriteBytes != nil {
applyParameters[parameters.FragmentorMinWriteBytes] = *config.FragmentorMinWriteBytes
}
if config.FragmentorMaxWriteBytes != nil {
applyParameters[parameters.FragmentorMaxWriteBytes] = *config.FragmentorMaxWriteBytes
}
if config.FragmentorMinDelayMicroseconds != nil {
applyParameters[parameters.FragmentorMinDelay] = fmt.Sprintf("%dus", *config.FragmentorMinDelayMicroseconds)
}
if config.FragmentorMaxDelayMicroseconds != nil {
applyParameters[parameters.FragmentorMaxDelay] = fmt.Sprintf("%dus", *config.FragmentorMaxDelayMicroseconds)
}
if config.MeekTrafficShapingProbability != nil {
applyParameters[parameters.MeekTrafficShapingProbability] = *config.MeekTrafficShapingProbability
}
if len(config.MeekTrafficShapingLimitProtocols) > 0 {
applyParameters[parameters.MeekTrafficShapingLimitProtocols] = protocol.TunnelProtocols(config.MeekTrafficShapingLimitProtocols)
}
if config.MeekMinTLSPadding != nil {
applyParameters[parameters.MeekMinTLSPadding] = *config.MeekMinTLSPadding
}
if config.MeekMaxTLSPadding != nil {
applyParameters[parameters.MeekMaxTLSPadding] = *config.MeekMaxTLSPadding
}
if config.MeekMinLimitRequestPayloadLength != nil {
applyParameters[parameters.MeekMinLimitRequestPayloadLength] = *config.MeekMinLimitRequestPayloadLength
}
if config.MeekMaxLimitRequestPayloadLength != nil {
applyParameters[parameters.MeekMaxLimitRequestPayloadLength] = *config.MeekMaxLimitRequestPayloadLength
}
if config.MeekRedialTLSProbability != nil {
applyParameters[parameters.MeekRedialTLSProbability] = *config.MeekRedialTLSProbability
}
if config.MeekAlternateCookieNameProbability != nil {
applyParameters[parameters.MeekAlternateCookieNameProbability] = *config.MeekAlternateCookieNameProbability
}
if config.MeekAlternateContentTypeProbability != nil {
applyParameters[parameters.MeekAlternateContentTypeProbability] = *config.MeekAlternateContentTypeProbability
}
if config.ObfuscatedSSHMinPadding != nil {
applyParameters[parameters.ObfuscatedSSHMinPadding] = *config.ObfuscatedSSHMinPadding
}
if config.ObfuscatedSSHMaxPadding != nil {
applyParameters[parameters.ObfuscatedSSHMaxPadding] = *config.ObfuscatedSSHMaxPadding
}
if config.LivenessTestMinUpstreamBytes != nil {
applyParameters[parameters.LivenessTestMinUpstreamBytes] = *config.LivenessTestMinUpstreamBytes
}
if config.LivenessTestMaxUpstreamBytes != nil {
applyParameters[parameters.LivenessTestMaxUpstreamBytes] = *config.LivenessTestMaxUpstreamBytes
}
if config.LivenessTestMinDownstreamBytes != nil {
applyParameters[parameters.LivenessTestMinDownstreamBytes] = *config.LivenessTestMinDownstreamBytes
}
if config.LivenessTestMaxDownstreamBytes != nil {
applyParameters[parameters.LivenessTestMaxDownstreamBytes] = *config.LivenessTestMaxDownstreamBytes
}
if config.ReplayCandidateCount != nil {
applyParameters[parameters.ReplayCandidateCount] = *config.ReplayCandidateCount
}
if config.ReplayDialParametersTTLSeconds != nil {
applyParameters[parameters.ReplayDialParametersTTL] = fmt.Sprintf("%ds", *config.ReplayDialParametersTTLSeconds)
}
if config.ReplayTargetUpstreamBytes != nil {
applyParameters[parameters.ReplayTargetUpstreamBytes] = *config.ReplayTargetUpstreamBytes
}
if config.ReplayTargetDownstreamBytes != nil {
applyParameters[parameters.ReplayTargetDownstreamBytes] = *config.ReplayTargetDownstreamBytes
}
if config.ReplayTargetTunnelDurationSeconds != nil {
applyParameters[parameters.ReplayTargetTunnelDuration] = fmt.Sprintf("%ds", *config.ReplayTargetTunnelDurationSeconds)
}
if config.ReplayLaterRoundMoveToFrontProbability != nil {
applyParameters[parameters.ReplayLaterRoundMoveToFrontProbability] = *config.ReplayLaterRoundMoveToFrontProbability
}
if config.ReplayRetainFailedProbability != nil {
applyParameters[parameters.ReplayRetainFailedProbability] = *config.ReplayRetainFailedProbability
}
if config.ReplayIgnoreChangedConfigState != nil {
applyParameters[parameters.ReplayIgnoreChangedConfigState] = *config.ReplayIgnoreChangedConfigState
}
if config.UseOnlyCustomTLSProfiles != nil {
applyParameters[parameters.UseOnlyCustomTLSProfiles] = *config.UseOnlyCustomTLSProfiles
}
if len(config.CustomTLSProfiles) > 0 {
applyParameters[parameters.CustomTLSProfiles] = config.CustomTLSProfiles
}
if config.SelectRandomizedTLSProfileProbability != nil {
applyParameters[parameters.SelectRandomizedTLSProfileProbability] = *config.SelectRandomizedTLSProfileProbability
}
if config.NoDefaultTLSSessionIDProbability != nil {
applyParameters[parameters.NoDefaultTLSSessionIDProbability] = *config.NoDefaultTLSSessionIDProbability
}
if len(config.DisableFrontingProviderTLSProfiles) > 0 {
applyParameters[parameters.DisableFrontingProviderTLSProfiles] = config.DisableFrontingProviderTLSProfiles
}
if config.ClientBurstUpstreamTargetBytes != nil {
applyParameters[parameters.ClientBurstUpstreamTargetBytes] = *config.ClientBurstUpstreamTargetBytes
}
if config.ClientBurstUpstreamDeadlineMilliseconds != nil {
applyParameters[parameters.ClientBurstUpstreamDeadline] = fmt.Sprintf("%dms", *config.ClientBurstUpstreamDeadlineMilliseconds)
}
if config.ClientBurstDownstreamTargetBytes != nil {
applyParameters[parameters.ClientBurstDownstreamTargetBytes] = *config.ClientBurstDownstreamTargetBytes
}
if config.ClientBurstDownstreamDeadlineMilliseconds != nil {
applyParameters[parameters.ClientBurstDownstreamDeadline] = fmt.Sprintf("%dms", *config.ClientBurstDownstreamDeadlineMilliseconds)
}
if config.ApplicationParameters != nil {
applyParameters[parameters.ApplicationParameters] = config.ApplicationParameters
}
if config.CustomHostNameRegexes != nil {
applyParameters[parameters.CustomHostNameRegexes] = parameters.RegexStrings(config.CustomHostNameRegexes)
}
if config.CustomHostNameProbability != nil {
applyParameters[parameters.CustomHostNameProbability] = *config.CustomHostNameProbability
}
if config.CustomHostNameLimitProtocols != nil {
applyParameters[parameters.CustomHostNameLimitProtocols] = protocol.TunnelProtocols(config.CustomHostNameLimitProtocols)
}
if config.ConjureCachedRegistrationTTLSeconds != nil {
applyParameters[parameters.ConjureCachedRegistrationTTL] = fmt.Sprintf("%ds", *config.ConjureCachedRegistrationTTLSeconds)
}
if config.ConjureAPIRegistrarBidirectionalURL != "" {
applyParameters[parameters.ConjureAPIRegistrarBidirectionalURL] = config.ConjureAPIRegistrarBidirectionalURL
}
if len(config.ConjureAPIRegistrarFrontingSpecs) > 0 {
applyParameters[parameters.ConjureAPIRegistrarFrontingSpecs] = config.ConjureAPIRegistrarFrontingSpecs
}
if config.ConjureAPIRegistrarMinDelayMilliseconds != nil {
applyParameters[parameters.ConjureAPIRegistrarMinDelay] = fmt.Sprintf("%dms", *config.ConjureAPIRegistrarMinDelayMilliseconds)
}
if config.ConjureAPIRegistrarMaxDelayMilliseconds != nil {
applyParameters[parameters.ConjureAPIRegistrarMaxDelay] = fmt.Sprintf("%dms", *config.ConjureAPIRegistrarMaxDelayMilliseconds)
}
if config.ConjureDecoyRegistrarProbability != nil {
applyParameters[parameters.ConjureDecoyRegistrarProbability] = *config.ConjureDecoyRegistrarProbability
}
if config.ConjureDecoyRegistrarWidth != nil {
applyParameters[parameters.ConjureDecoyRegistrarWidth] = *config.ConjureDecoyRegistrarWidth
}
if config.ConjureDecoyRegistrarMinDelayMilliseconds != nil {
applyParameters[parameters.ConjureDecoyRegistrarMinDelay] = fmt.Sprintf("%dms", *config.ConjureDecoyRegistrarMinDelayMilliseconds)
}
if config.ConjureDecoyRegistrarMaxDelayMilliseconds != nil {
applyParameters[parameters.ConjureDecoyRegistrarMaxDelay] = fmt.Sprintf("%dms", *config.ConjureDecoyRegistrarMaxDelayMilliseconds)
}
if config.ConjureEnableIPv6Dials != nil {
applyParameters[parameters.ConjureEnableIPv6Dials] = *config.ConjureEnableIPv6Dials
}
if config.ConjureEnablePortRandomization != nil {
applyParameters[parameters.ConjureEnablePortRandomization] = *config.ConjureEnablePortRandomization
}
if config.ConjureEnableRegistrationOverrides != nil {
applyParameters[parameters.ConjureEnableRegistrationOverrides] = *config.ConjureEnableRegistrationOverrides
}
if config.ConjureLimitTransports != nil {
applyParameters[parameters.ConjureLimitTransports] = config.ConjureLimitTransports
}
if config.ConjureSTUNServerAddresses != nil {
applyParameters[parameters.ConjureSTUNServerAddresses] = config.ConjureSTUNServerAddresses
}
if config.ConjureDTLSEmptyInitialPacketProbability != nil {
applyParameters[parameters.ConjureDTLSEmptyInitialPacketProbability] = *config.ConjureDTLSEmptyInitialPacketProbability
}
if config.HoldOffTunnelMinDurationMilliseconds != nil {
applyParameters[parameters.HoldOffTunnelMinDuration] = fmt.Sprintf("%dms", *config.HoldOffTunnelMinDurationMilliseconds)
}
if config.HoldOffTunnelMaxDurationMilliseconds != nil {
applyParameters[parameters.HoldOffTunnelMaxDuration] = fmt.Sprintf("%dms", *config.HoldOffTunnelMaxDurationMilliseconds)
}
if len(config.HoldOffTunnelProtocols) > 0 {
applyParameters[parameters.HoldOffTunnelProtocols] = protocol.TunnelProtocols(config.HoldOffTunnelProtocols)
}
if len(config.HoldOffTunnelFrontingProviderIDs) > 0 {
applyParameters[parameters.HoldOffTunnelFrontingProviderIDs] = config.HoldOffTunnelFrontingProviderIDs
}
if config.HoldOffTunnelProbability != nil {
applyParameters[parameters.HoldOffTunnelProbability] = *config.HoldOffTunnelProbability
}
if config.HoldOffDirectTunnelMinDurationMilliseconds != nil {
applyParameters[parameters.HoldOffDirectTunnelMinDuration] = fmt.Sprintf("%dms", *config.HoldOffDirectTunnelMinDurationMilliseconds)
}
if config.HoldOffDirectTunnelMaxDurationMilliseconds != nil {
applyParameters[parameters.HoldOffDirectTunnelMaxDuration] = fmt.Sprintf("%dms", *config.HoldOffDirectTunnelMaxDurationMilliseconds)
}
if len(config.HoldOffDirectTunnelProviderRegions) > 0 {
applyParameters[parameters.HoldOffDirectTunnelProviderRegions] = parameters.KeyStrings(config.HoldOffDirectTunnelProviderRegions)
}
if config.HoldOffDirectTunnelProbability != nil {
applyParameters[parameters.HoldOffDirectTunnelProbability] = *config.HoldOffDirectTunnelProbability
}
if len(config.RestrictDirectProviderRegions) > 0 {
applyParameters[parameters.RestrictDirectProviderRegions] = parameters.KeyStrings(config.RestrictDirectProviderRegions)
}
if config.RestrictDirectProviderIDsClientProbability != nil {
applyParameters[parameters.RestrictDirectProviderIDsClientProbability] = *config.RestrictDirectProviderIDsClientProbability
}
if len(config.RestrictFrontingProviderIDs) > 0 {
applyParameters[parameters.RestrictFrontingProviderIDs] = config.RestrictFrontingProviderIDs
}
if config.RestrictFrontingProviderIDsClientProbability != nil {
applyParameters[parameters.RestrictFrontingProviderIDsClientProbability] = *config.RestrictFrontingProviderIDsClientProbability
}
if config.UpstreamProxyAllowAllServerEntrySources != nil {
applyParameters[parameters.UpstreamProxyAllowAllServerEntrySources] = *config.UpstreamProxyAllowAllServerEntrySources
}
if len(config.LimitTunnelDialPortNumbers) > 0 {
applyParameters[parameters.LimitTunnelDialPortNumbers] = config.LimitTunnelDialPortNumbers
}
if config.QUICDisablePathMTUDiscoveryProbability != nil {
applyParameters[parameters.QUICDisableClientPathMTUDiscoveryProbability] = *config.QUICDisablePathMTUDiscoveryProbability
}
if config.DNSResolverAttemptsPerServer != nil {
applyParameters[parameters.DNSResolverAttemptsPerServer] = *config.DNSResolverAttemptsPerServer
}
if config.DNSResolverAttemptsPerPreferredServer != nil {
applyParameters[parameters.DNSResolverAttemptsPerPreferredServer] = *config.DNSResolverAttemptsPerPreferredServer
}
if config.DNSResolverRequestTimeoutMilliseconds != nil {
applyParameters[parameters.DNSResolverRequestTimeout] = fmt.Sprintf("%dms", *config.DNSResolverRequestTimeoutMilliseconds)
}
if config.DNSResolverAwaitTimeoutMilliseconds != nil {
applyParameters[parameters.DNSResolverAwaitTimeout] = fmt.Sprintf("%dms", *config.DNSResolverAwaitTimeoutMilliseconds)
}
if config.DNSResolverPreresolvedIPAddressProbability != nil {
applyParameters[parameters.DNSResolverPreresolvedIPAddressProbability] = *config.DNSResolverPreresolvedIPAddressProbability
}
if config.DNSResolverPreresolvedIPAddressCIDRs != nil {
applyParameters[parameters.DNSResolverPreresolvedIPAddressCIDRs] = config.DNSResolverPreresolvedIPAddressCIDRs
}
if config.DNSResolverAlternateServers != nil {
applyParameters[parameters.DNSResolverAlternateServers] = config.DNSResolverAlternateServers
}
if config.DNSResolverPreferredAlternateServers != nil {
applyParameters[parameters.DNSResolverPreferredAlternateServers] = config.DNSResolverPreferredAlternateServers
}
if config.DNSResolverPreferAlternateServerProbability != nil {
applyParameters[parameters.DNSResolverPreferAlternateServerProbability] = *config.DNSResolverPreferAlternateServerProbability
}
if config.DNSResolverProtocolTransformSpecs != nil {
applyParameters[parameters.DNSResolverProtocolTransformSpecs] = config.DNSResolverProtocolTransformSpecs
}
if config.DNSResolverProtocolTransformScopedSpecNames != nil {
applyParameters[parameters.DNSResolverProtocolTransformScopedSpecNames] = config.DNSResolverProtocolTransformScopedSpecNames
}
if config.DNSResolverProtocolTransformProbability != nil {
applyParameters[parameters.DNSResolverProtocolTransformProbability] = *config.DNSResolverProtocolTransformProbability
}
if config.DNSResolverIncludeEDNS0Probability != nil {
applyParameters[parameters.DNSResolverIncludeEDNS0Probability] = *config.DNSResolverIncludeEDNS0Probability
}
if config.DNSResolverCacheExtensionInitialTTLMilliseconds != nil {
applyParameters[parameters.DNSResolverCacheExtensionInitialTTL] = fmt.Sprintf("%dms", *config.DNSResolverCacheExtensionInitialTTLMilliseconds)
}
if config.DNSResolverCacheExtensionVerifiedTTLMilliseconds != nil {
applyParameters[parameters.DNSResolverCacheExtensionVerifiedTTL] = fmt.Sprintf("%dms", *config.DNSResolverCacheExtensionVerifiedTTLMilliseconds)
}
if config.DirectHTTPProtocolTransformSpecs != nil {
applyParameters[parameters.DirectHTTPProtocolTransformSpecs] = config.DirectHTTPProtocolTransformSpecs
}
if config.DirectHTTPProtocolTransformScopedSpecNames != nil {
applyParameters[parameters.DirectHTTPProtocolTransformScopedSpecNames] = config.DirectHTTPProtocolTransformScopedSpecNames
}
if config.DirectHTTPProtocolTransformProbability != nil {
applyParameters[parameters.DirectHTTPProtocolTransformProbability] = *config.DirectHTTPProtocolTransformProbability
}
if config.FrontedHTTPProtocolTransformSpecs != nil {
applyParameters[parameters.FrontedHTTPProtocolTransformSpecs] = config.FrontedHTTPProtocolTransformSpecs
}
if config.FrontedHTTPProtocolTransformScopedSpecNames != nil {
applyParameters[parameters.FrontedHTTPProtocolTransformScopedSpecNames] = config.FrontedHTTPProtocolTransformScopedSpecNames
}
if config.FrontedHTTPProtocolTransformProbability != nil {
applyParameters[parameters.FrontedHTTPProtocolTransformProbability] = *config.FrontedHTTPProtocolTransformProbability
}
if config.OSSHObfuscatorSeedTransformSpecs != nil {
applyParameters[parameters.OSSHObfuscatorSeedTransformSpecs] = config.OSSHObfuscatorSeedTransformSpecs
}
if config.OSSHObfuscatorSeedTransformScopedSpecNames != nil {
applyParameters[parameters.OSSHObfuscatorSeedTransformScopedSpecNames] = config.OSSHObfuscatorSeedTransformScopedSpecNames
}
if config.OSSHObfuscatorSeedTransformProbability != nil {
applyParameters[parameters.OSSHObfuscatorSeedTransformProbability] = *config.OSSHObfuscatorSeedTransformProbability
}
if config.ObfuscatedQUICNonceTransformSpecs != nil {
applyParameters[parameters.ObfuscatedQUICNonceTransformSpecs] = config.ObfuscatedQUICNonceTransformSpecs
}
if config.ObfuscatedQUICNonceTransformScopedSpecNames != nil {
applyParameters[parameters.ObfuscatedQUICNonceTransformScopedSpecNames] = config.ObfuscatedQUICNonceTransformScopedSpecNames
}
if config.ObfuscatedQUICNonceTransformProbability != nil {
applyParameters[parameters.ObfuscatedQUICNonceTransformProbability] = *config.ObfuscatedQUICNonceTransformProbability
}
if config.OSSHPrefixSpecs != nil {
applyParameters[parameters.OSSHPrefixSpecs] = config.OSSHPrefixSpecs
}
if config.OSSHPrefixScopedSpecNames != nil {
applyParameters[parameters.OSSHPrefixScopedSpecNames] = config.OSSHPrefixScopedSpecNames
}
if config.OSSHPrefixProbability != nil {
applyParameters[parameters.OSSHPrefixProbability] = *config.OSSHPrefixProbability
}
if config.OSSHPrefixSplitMinDelayMilliseconds != nil {
applyParameters[parameters.OSSHPrefixSplitMinDelay] = fmt.Sprintf("%dms", *config.OSSHPrefixSplitMinDelayMilliseconds)
}
if config.OSSHPrefixSplitMaxDelayMilliseconds != nil {
applyParameters[parameters.OSSHPrefixSplitMaxDelay] = fmt.Sprintf("%dms", *config.OSSHPrefixSplitMaxDelayMilliseconds)
}
if config.OSSHPrefixEnableFragmentor != nil {
applyParameters[parameters.OSSHPrefixEnableFragmentor] = *config.OSSHPrefixEnableFragmentor
}
if config.TLSTunnelTrafficShapingProbability != nil {
applyParameters[parameters.TLSTunnelTrafficShapingProbability] = *config.TLSTunnelTrafficShapingProbability
}
if config.TLSTunnelMinTLSPadding != nil {
applyParameters[parameters.TLSTunnelMinTLSPadding] = *config.TLSTunnelMinTLSPadding
}
if config.TLSTunnelMaxTLSPadding != nil {
applyParameters[parameters.TLSTunnelMaxTLSPadding] = *config.TLSTunnelMaxTLSPadding
}
if config.TLSFragmentClientHelloProbability != nil {
applyParameters[parameters.TLSFragmentClientHelloProbability] = *config.TLSFragmentClientHelloProbability
}
if len(config.TLSFragmentClientHelloLimitProtocols) > 0 {
applyParameters[parameters.TLSFragmentClientHelloLimitProtocols] = protocol.TunnelProtocols(config.TLSFragmentClientHelloLimitProtocols)
}
if config.SteeringIPCacheTTLSeconds != nil {
applyParameters[parameters.SteeringIPCacheTTL] = fmt.Sprintf("%ds", *config.SteeringIPCacheTTLSeconds)
}
if config.SteeringIPCacheMaxEntries != nil {
applyParameters[parameters.SteeringIPCacheMaxEntries] = *config.SteeringIPCacheMaxEntries
}
if config.SteeringIPProbability != nil {
applyParameters[parameters.SteeringIPProbability] = *config.SteeringIPProbability
}
// When adding new config dial parameters that may override tactics, also
// update setDialParametersHash.
return applyParameters
}
func (config *Config) setDialParametersHash() {
// Calculate and store a hash of the config values that may impact
// dial parameters. This hash is used as part of the dial parameters
// replay mechanism to detect when persisted dial parameters should
// be discarded due to conflicting config changes.
//
// With a couple of minor exceptions, configuring dial parameters via the
// config is intended for testing only, and so these parameters are expected
// to be present in test runs only. It remains an important case to discard
// replay dial parameters when test config parameters are varied.
//
// Hashing the parameter names detects some ambiguous hash cases, such as two
// consecutive int64 parameters, one omitted and one not, that are flipped.
// The serialization is not completely unambiguous, and the format is
// currently limited by legacy cases (not invalidating replay dial parameters
// for production clients is more important than invalidating for test runs).
// We cannot hash the entire config JSON as it contains non-dial parameter
// fields which may frequently change across runs.
//
// MD5 hash is used solely as a data checksum and not for any security
// purpose.
hash := md5.New()
if len(config.LimitTunnelProtocols) > 0 {
hash.Write([]byte("LimitTunnelProtocols"))
for _, protocol := range config.LimitTunnelProtocols {
hash.Write([]byte(protocol))
}
}
if len(config.InitialLimitTunnelProtocols) > 0 && config.InitialLimitTunnelProtocolsCandidateCount > 0 {
hash.Write([]byte("InitialLimitTunnelProtocols"))
for _, protocol := range config.InitialLimitTunnelProtocols {
hash.Write([]byte(protocol))
}
binary.Write(hash, binary.LittleEndian, int64(config.InitialLimitTunnelProtocolsCandidateCount))
}
if len(config.LimitTLSProfiles) > 0 {
hash.Write([]byte("LimitTLSProfiles"))
for _, profile := range config.LimitTLSProfiles {
hash.Write([]byte(profile))
}
}
if len(config.LimitQUICVersions) > 0 {
hash.Write([]byte("LimitQUICVersions"))
for _, version := range config.LimitQUICVersions {
hash.Write([]byte(version))
}
}
// Whether a custom User-Agent is specified is a binary flag: when not set,
// the replay dial parameters value applies. When set, external
// considerations apply.
if _, ok := config.CustomHeaders["User-Agent"]; ok {
hash.Write([]byte("CustomHeaders User-Agent"))
hash.Write([]byte{1})
}
if config.UpstreamProxyURL != "" {
hash.Write([]byte("UpstreamProxyURL"))
hash.Write([]byte(config.UpstreamProxyURL))
}
if config.TransformHostNameProbability != nil {
hash.Write([]byte("TransformHostNameProbability"))
binary.Write(hash, binary.LittleEndian, *config.TransformHostNameProbability)
}
if config.FragmentorProbability != nil {
hash.Write([]byte("FragmentorProbability"))
binary.Write(hash, binary.LittleEndian, *config.FragmentorProbability)
}
if len(config.FragmentorLimitProtocols) > 0 {
hash.Write([]byte("FragmentorLimitProtocols"))
for _, protocol := range config.FragmentorLimitProtocols {
hash.Write([]byte(protocol))
}
}
if config.FragmentorMinTotalBytes != nil {
hash.Write([]byte("FragmentorMinTotalBytes"))
binary.Write(hash, binary.LittleEndian, int64(*config.FragmentorMinTotalBytes))
}
if config.FragmentorMaxTotalBytes != nil {
hash.Write([]byte("FragmentorMaxTotalBytes"))
binary.Write(hash, binary.LittleEndian, int64(*config.FragmentorMaxTotalBytes))
}
if config.FragmentorMinWriteBytes != nil {
hash.Write([]byte("FragmentorMinWriteBytes"))
binary.Write(hash, binary.LittleEndian, int64(*config.FragmentorMinWriteBytes))
}
if config.FragmentorMaxWriteBytes != nil {
hash.Write([]byte("FragmentorMaxWriteBytes"))
binary.Write(hash, binary.LittleEndian, int64(*config.FragmentorMaxWriteBytes))
}
if config.FragmentorMinDelayMicroseconds != nil {
hash.Write([]byte("FragmentorMinDelayMicroseconds"))
binary.Write(hash, binary.LittleEndian, int64(*config.FragmentorMinDelayMicroseconds))
}
if config.FragmentorMaxDelayMicroseconds != nil {
hash.Write([]byte("FragmentorMaxDelayMicroseconds"))
binary.Write(hash, binary.LittleEndian, int64(*config.FragmentorMaxDelayMicroseconds))
}
if config.MeekTrafficShapingProbability != nil {
hash.Write([]byte("MeekTrafficShapingProbability"))
binary.Write(hash, binary.LittleEndian, *config.MeekTrafficShapingProbability)
}
if len(config.MeekTrafficShapingLimitProtocols) > 0 {
hash.Write([]byte("MeekTrafficShapingLimitProtocols"))
for _, protocol := range config.MeekTrafficShapingLimitProtocols {
hash.Write([]byte(protocol))
}
}
if config.MeekMinLimitRequestPayloadLength != nil {
hash.Write([]byte("MeekMinLimitRequestPayloadLength"))
binary.Write(hash, binary.LittleEndian, int64(*config.MeekMinLimitRequestPayloadLength))
}
if config.MeekMaxLimitRequestPayloadLength != nil {
hash.Write([]byte("MeekMaxLimitRequestPayloadLength"))
binary.Write(hash, binary.LittleEndian, int64(*config.MeekMaxLimitRequestPayloadLength))
}
if config.MeekRedialTLSProbability != nil {
hash.Write([]byte("MeekRedialTLSProbability"))
binary.Write(hash, binary.LittleEndian, *config.MeekRedialTLSProbability)
}
if config.ObfuscatedSSHMinPadding != nil {
hash.Write([]byte("ObfuscatedSSHMinPadding"))
binary.Write(hash, binary.LittleEndian, int64(*config.ObfuscatedSSHMinPadding))
}
if config.ObfuscatedSSHMaxPadding != nil {
hash.Write([]byte("ObfuscatedSSHMaxPadding"))
binary.Write(hash, binary.LittleEndian, int64(*config.ObfuscatedSSHMaxPadding))
}
if config.LivenessTestMinUpstreamBytes != nil {
hash.Write([]byte("LivenessTestMinUpstreamBytes"))
binary.Write(hash, binary.LittleEndian, int64(*config.LivenessTestMinUpstreamBytes))
}
if config.LivenessTestMaxUpstreamBytes != nil {
hash.Write([]byte("LivenessTestMaxUpstreamBytes"))
binary.Write(hash, binary.LittleEndian, int64(*config.LivenessTestMaxUpstreamBytes))
}
if config.LivenessTestMinDownstreamBytes != nil {
hash.Write([]byte("LivenessTestMinDownstreamBytes"))
binary.Write(hash, binary.LittleEndian, int64(*config.LivenessTestMinDownstreamBytes))
}
if config.LivenessTestMaxDownstreamBytes != nil {
hash.Write([]byte("LivenessTestMaxDownstreamBytes"))
binary.Write(hash, binary.LittleEndian, int64(*config.LivenessTestMaxDownstreamBytes))
}
// Legacy case: these parameters are included in the hash unconditionally,
// and so will impact almost all production clients. These parameter names
// are not hashed since that would invalidate all replay dial parameters for
// existing clients whose hashes predate the inclusion of parameter names.
binary.Write(hash, binary.LittleEndian, config.NetworkLatencyMultiplierMin)
binary.Write(hash, binary.LittleEndian, config.NetworkLatencyMultiplierMax)
binary.Write(hash, binary.LittleEndian, config.NetworkLatencyMultiplierLambda)
if config.UseOnlyCustomTLSProfiles != nil {
hash.Write([]byte("UseOnlyCustomTLSProfiles"))
binary.Write(hash, binary.LittleEndian, *config.UseOnlyCustomTLSProfiles)
}
if len(config.CustomTLSProfiles) > 0 {
hash.Write([]byte("CustomTLSProfiles"))
for _, customTLSProfile := range config.CustomTLSProfiles {
encodedCustomTLSProofile, _ := json.Marshal(customTLSProfile)
hash.Write(encodedCustomTLSProofile)
}
}
if config.SelectRandomizedTLSProfileProbability != nil {
hash.Write([]byte("SelectRandomizedTLSProfileProbability"))
binary.Write(hash, binary.LittleEndian, *config.SelectRandomizedTLSProfileProbability)
}
if config.NoDefaultTLSSessionIDProbability != nil {
hash.Write([]byte("NoDefaultTLSSessionIDProbability"))
binary.Write(hash, binary.LittleEndian, *config.NoDefaultTLSSessionIDProbability)
}
if len(config.DisableFrontingProviderTLSProfiles) > 0 {
hash.Write([]byte("DisableFrontingProviderTLSProfiles"))
encodedDisableFrontingProviderTLSProfiles, _ :=
json.Marshal(config.DisableFrontingProviderTLSProfiles)
hash.Write(encodedDisableFrontingProviderTLSProfiles)
}
if len(config.CustomHostNameRegexes) > 0 {
hash.Write([]byte("CustomHostNameRegexes"))
for _, customHostNameRegex := range config.CustomHostNameRegexes {
hash.Write([]byte(customHostNameRegex))
}
}
if config.CustomHostNameProbability != nil {
hash.Write([]byte("CustomHostNameProbability"))
binary.Write(hash, binary.LittleEndian, *config.CustomHostNameProbability)
}
if len(config.CustomHostNameLimitProtocols) > 0 {
hash.Write([]byte("CustomHostNameLimitProtocols"))
for _, protocol := range config.CustomHostNameLimitProtocols {
hash.Write([]byte(protocol))
}
}
if config.ConjureCachedRegistrationTTLSeconds != nil {
hash.Write([]byte("ConjureCachedRegistrationTTLSeconds"))
binary.Write(hash, binary.LittleEndian, int64(*config.ConjureCachedRegistrationTTLSeconds))
}
if config.ConjureAPIRegistrarBidirectionalURL != "" {
hash.Write([]byte("ConjureAPIRegistrarBidirectionalURL"))
hash.Write([]byte(config.ConjureAPIRegistrarBidirectionalURL))
}
if len(config.ConjureAPIRegistrarFrontingSpecs) > 0 {
hash.Write([]byte("ConjureAPIRegistrarFrontingSpecs"))
for _, frontingSpec := range config.ConjureAPIRegistrarFrontingSpecs {
encodedFrontSpec, _ := json.Marshal(frontingSpec)
hash.Write(encodedFrontSpec)
}
}
if config.ConjureAPIRegistrarMinDelayMilliseconds != nil {
hash.Write([]byte("ConjureAPIRegistrarMinDelayMilliseconds"))
binary.Write(hash, binary.LittleEndian, int64(*config.ConjureAPIRegistrarMinDelayMilliseconds))
}
if config.ConjureAPIRegistrarMaxDelayMilliseconds != nil {
hash.Write([]byte("ConjureAPIRegistrarMaxDelayMilliseconds"))
binary.Write(hash, binary.LittleEndian, int64(*config.ConjureAPIRegistrarMaxDelayMilliseconds))
}
if config.ConjureDecoyRegistrarWidth != nil {
hash.Write([]byte("ConjureDecoyRegistrarWidth"))
binary.Write(hash, binary.LittleEndian, int64(*config.ConjureDecoyRegistrarWidth))
}
if config.ConjureDecoyRegistrarMinDelayMilliseconds != nil {
hash.Write([]byte("ConjureDecoyRegistrarMinDelayMilliseconds"))
binary.Write(hash, binary.LittleEndian, int64(*config.ConjureDecoyRegistrarMinDelayMilliseconds))
}
if config.ConjureDecoyRegistrarMaxDelayMilliseconds != nil {
hash.Write([]byte("ConjureDecoyRegistrarMaxDelayMilliseconds"))
binary.Write(hash, binary.LittleEndian, int64(*config.ConjureDecoyRegistrarMaxDelayMilliseconds))
}
if config.ConjureLimitTransports != nil {
hash.Write([]byte("ConjureLimitTransports"))
for _, transport := range config.ConjureLimitTransports {
hash.Write([]byte(transport))
}
}
if config.ConjureSTUNServerAddresses != nil {
hash.Write([]byte("ConjureSTUNServerAddresses"))
for _, address := range config.ConjureSTUNServerAddresses {
hash.Write([]byte(address))
}
}
if config.HoldOffTunnelMinDurationMilliseconds != nil {
hash.Write([]byte("HoldOffTunnelMinDurationMilliseconds"))
binary.Write(hash, binary.LittleEndian, int64(*config.HoldOffTunnelMinDurationMilliseconds))
}
if config.HoldOffTunnelMaxDurationMilliseconds != nil {
hash.Write([]byte("HoldOffTunnelMaxDurationMilliseconds"))
binary.Write(hash, binary.LittleEndian, int64(*config.HoldOffTunnelMaxDurationMilliseconds))
}
if len(config.HoldOffTunnelProtocols) > 0 {
hash.Write([]byte("HoldOffTunnelProtocols"))
for _, protocol := range config.HoldOffTunnelProtocols {
hash.Write([]byte(protocol))
}
}
if len(config.HoldOffTunnelFrontingProviderIDs) > 0 {
hash.Write([]byte("HoldOffTunnelFrontingProviderIDs"))
for _, providerID := range config.HoldOffTunnelFrontingProviderIDs {
hash.Write([]byte(providerID))
}
}
if config.HoldOffDirectTunnelProbability != nil {
hash.Write([]byte("HoldOffDirectTunnelProbability"))
binary.Write(hash, binary.LittleEndian, *config.HoldOffDirectTunnelProbability)
}
if config.HoldOffDirectTunnelMinDurationMilliseconds != nil {
hash.Write([]byte("HoldOffDirectTunnelMinDurationMilliseconds"))
binary.Write(hash, binary.LittleEndian, int64(*config.HoldOffDirectTunnelMinDurationMilliseconds))
}
if config.HoldOffDirectTunnelMaxDurationMilliseconds != nil {
hash.Write([]byte("HoldOffDirectTunnelMaxDurationMilliseconds"))
binary.Write(hash, binary.LittleEndian, int64(*config.HoldOffDirectTunnelMaxDurationMilliseconds))
}
if len(config.HoldOffDirectTunnelProviderRegions) > 0 {
hash.Write([]byte("HoldOffDirectTunnelProviderRegions"))
for providerID, regions := range config.HoldOffDirectTunnelProviderRegions {
hash.Write([]byte(providerID))
for _, region := range regions {
hash.Write([]byte(region))
}
}
}
if config.HoldOffTunnelProbability != nil {
hash.Write([]byte("HoldOffTunnelProbability"))
binary.Write(hash, binary.LittleEndian, *config.HoldOffTunnelProbability)
}
if len(config.RestrictDirectProviderRegions) > 0 {
hash.Write([]byte("RestrictDirectProviderRegions"))
for providerID, regions := range config.RestrictDirectProviderRegions {
hash.Write([]byte(providerID))
for _, region := range regions {
hash.Write([]byte(region))
}
}
}
if config.RestrictDirectProviderIDsClientProbability != nil {
hash.Write([]byte("RestrictDirectProviderIDsClientProbability"))
binary.Write(hash, binary.LittleEndian, *config.RestrictDirectProviderIDsClientProbability)
}
if len(config.RestrictFrontingProviderIDs) > 0 {
hash.Write([]byte("RestrictFrontingProviderIDs"))
for _, providerID := range config.RestrictFrontingProviderIDs {
hash.Write([]byte(providerID))
}
}
if config.RestrictFrontingProviderIDsClientProbability != nil {
hash.Write([]byte("RestrictFrontingProviderIDsClientProbability"))
binary.Write(hash, binary.LittleEndian, *config.RestrictFrontingProviderIDsClientProbability)
}
if config.UpstreamProxyAllowAllServerEntrySources != nil {
hash.Write([]byte("UpstreamProxyAllowAllServerEntrySources"))
binary.Write(hash, binary.LittleEndian, *config.UpstreamProxyAllowAllServerEntrySources)
}
if len(config.LimitTunnelDialPortNumbers) > 0 {
hash.Write([]byte("LimitTunnelDialPortNumbers"))
encodedLimitTunnelDialPortNumbers, _ :=
json.Marshal(config.LimitTunnelDialPortNumbers)
hash.Write(encodedLimitTunnelDialPortNumbers)
}
if config.QUICDisablePathMTUDiscoveryProbability != nil {
hash.Write([]byte("QUICDisablePathMTUDiscoveryProbability"))
binary.Write(hash, binary.LittleEndian, *config.QUICDisablePathMTUDiscoveryProbability)
}
if config.DNSResolverAttemptsPerServer != nil {
hash.Write([]byte("DNSResolverAttemptsPerServer"))
binary.Write(hash, binary.LittleEndian, int64(*config.DNSResolverAttemptsPerServer))
}
if config.DNSResolverRequestTimeoutMilliseconds != nil {
hash.Write([]byte("DNSResolverRequestTimeoutMilliseconds"))
binary.Write(hash, binary.LittleEndian, int64(*config.DNSResolverRequestTimeoutMilliseconds))
}
if config.DNSResolverAwaitTimeoutMilliseconds != nil {
hash.Write([]byte("DNSResolverAwaitTimeoutMilliseconds"))
binary.Write(hash, binary.LittleEndian, int64(*config.DNSResolverAwaitTimeoutMilliseconds))
}
if config.DNSResolverPreresolvedIPAddressCIDRs != nil {
hash.Write([]byte("DNSResolverPreresolvedIPAddressCIDRs"))
encodedDNSResolverPreresolvedIPAddressCIDRs, _ :=
json.Marshal(config.DNSResolverPreresolvedIPAddressCIDRs)
hash.Write(encodedDNSResolverPreresolvedIPAddressCIDRs)
}
if config.DNSResolverPreresolvedIPAddressProbability != nil {
hash.Write([]byte("DNSResolverPreresolvedIPAddressProbability"))
binary.Write(hash, binary.LittleEndian, *config.DNSResolverPreresolvedIPAddressProbability)
}
if config.DNSResolverAlternateServers != nil {
hash.Write([]byte("DNSResolverAlternateServers"))
for _, server := range config.DNSResolverAlternateServers {
hash.Write([]byte(server))
}
}
if config.DNSResolverPreferAlternateServerProbability != nil {
hash.Write([]byte("DNSResolverPreferAlternateServerProbability"))
binary.Write(hash, binary.LittleEndian, *config.DNSResolverPreferAlternateServerProbability)
}
if config.DNSResolverProtocolTransformSpecs != nil {
hash.Write([]byte("DNSResolverProtocolTransformSpecs"))
encodedDNSResolverProtocolTransformSpecs, _ :=
json.Marshal(config.DNSResolverProtocolTransformSpecs)
hash.Write(encodedDNSResolverProtocolTransformSpecs)
}
if config.DNSResolverProtocolTransformScopedSpecNames != nil {
hash.Write([]byte("DNSResolverProtocolTransformScopedSpecNames"))
encodedDNSResolverProtocolTransformScopedSpecNames, _ :=
json.Marshal(config.DNSResolverProtocolTransformScopedSpecNames)
hash.Write(encodedDNSResolverProtocolTransformScopedSpecNames)
}
if config.DNSResolverProtocolTransformProbability != nil {
hash.Write([]byte("DNSResolverProtocolTransformProbability"))
binary.Write(hash, binary.LittleEndian, *config.DNSResolverProtocolTransformProbability)
}
if config.DNSResolverIncludeEDNS0Probability != nil {
hash.Write([]byte("DNSResolverIncludeEDNS0Probability"))
binary.Write(hash, binary.LittleEndian, *config.DNSResolverIncludeEDNS0Probability)
}
if config.DNSResolverCacheExtensionInitialTTLMilliseconds != nil {
hash.Write([]byte("DNSResolverCacheExtensionInitialTTLMilliseconds"))
binary.Write(hash, binary.LittleEndian, int64(*config.DNSResolverCacheExtensionInitialTTLMilliseconds))
}
if config.DNSResolverCacheExtensionVerifiedTTLMilliseconds != nil {
hash.Write([]byte("DNSResolverCacheExtensionVerifiedTTLMilliseconds"))
binary.Write(hash, binary.LittleEndian, int64(*config.DNSResolverCacheExtensionVerifiedTTLMilliseconds))
}
if config.DirectHTTPProtocolTransformSpecs != nil {
hash.Write([]byte("DirectHTTPProtocolTransformSpecs"))
encodedDirectHTTPProtocolTransformSpecs, _ :=
json.Marshal(config.DirectHTTPProtocolTransformSpecs)
hash.Write(encodedDirectHTTPProtocolTransformSpecs)
}
if config.DirectHTTPProtocolTransformScopedSpecNames != nil {
hash.Write([]byte("DirectHTTPProtocolTransformScopedSpecNames"))
encodedDirectHTTPProtocolTransformScopedSpecNames, _ :=
json.Marshal(config.DirectHTTPProtocolTransformScopedSpecNames)
hash.Write(encodedDirectHTTPProtocolTransformScopedSpecNames)
}
if config.DirectHTTPProtocolTransformProbability != nil {
hash.Write([]byte("DirectHTTPProtocolTransformProbability"))
binary.Write(hash, binary.LittleEndian, *config.DirectHTTPProtocolTransformProbability)
}
if config.FrontedHTTPProtocolTransformSpecs != nil {
hash.Write([]byte("FrontedHTTPProtocolTransformSpecs"))
encodedFrontedHTTPProtocolTransformSpecs, _ :=
json.Marshal(config.FrontedHTTPProtocolTransformSpecs)
hash.Write(encodedFrontedHTTPProtocolTransformSpecs)
}
if config.FrontedHTTPProtocolTransformScopedSpecNames != nil {
hash.Write([]byte("FrontedHTTPProtocolTransformScopedSpecNames"))
encodedFrontedHTTPProtocolTransformScopedSpecNames, _ :=
json.Marshal(config.FrontedHTTPProtocolTransformScopedSpecNames)
hash.Write(encodedFrontedHTTPProtocolTransformScopedSpecNames)
}
if config.FrontedHTTPProtocolTransformProbability != nil {
hash.Write([]byte("FrontedHTTPProtocolTransformProbability"))
binary.Write(hash, binary.LittleEndian, *config.FrontedHTTPProtocolTransformProbability)
}
if config.OSSHObfuscatorSeedTransformSpecs != nil {
hash.Write([]byte("OSSHObfuscatorSeedTransformSpecs"))
encodedOSSHObfuscatorSeedTransformSpecs, _ :=
json.Marshal(config.OSSHObfuscatorSeedTransformSpecs)
hash.Write(encodedOSSHObfuscatorSeedTransformSpecs)
}
if config.OSSHObfuscatorSeedTransformScopedSpecNames != nil {
hash.Write([]byte("OSSHObfuscatorSeedTransformScopedSpecNames"))
encodedOSSHObfuscatorSeedTransformScopedSpecNames, _ :=
json.Marshal(config.OSSHObfuscatorSeedTransformScopedSpecNames)
hash.Write(encodedOSSHObfuscatorSeedTransformScopedSpecNames)
}
if config.OSSHObfuscatorSeedTransformProbability != nil {
hash.Write([]byte("OSSHObfuscatorSeedTransformProbability"))
binary.Write(hash, binary.LittleEndian, *config.OSSHObfuscatorSeedTransformProbability)
}
if config.ObfuscatedQUICNonceTransformSpecs != nil {
hash.Write([]byte("ObfuscatedQUICNonceTransformSpecs"))
encodedObfuscatedQUICNonceTransformSpecs, _ :=
json.Marshal(config.ObfuscatedQUICNonceTransformSpecs)
hash.Write(encodedObfuscatedQUICNonceTransformSpecs)
}
if config.ObfuscatedQUICNonceTransformScopedSpecNames != nil {
hash.Write([]byte("ObfuscatedQUICNonceTransformScopedSpecNames"))
encodedObfuscatedQUICNonceTransformScopedSpecNames, _ :=
json.Marshal(config.ObfuscatedQUICNonceTransformScopedSpecNames)
hash.Write(encodedObfuscatedQUICNonceTransformScopedSpecNames)
}
if config.ObfuscatedQUICNonceTransformProbability != nil {
hash.Write([]byte("ObfuscatedQUICNonceTransformProbability"))
binary.Write(hash, binary.LittleEndian, *config.ObfuscatedQUICNonceTransformProbability)
}
if config.OSSHPrefixSpecs != nil {
hash.Write([]byte("OSSHPrefixSpecs"))
encodedOSSHPrefixSpecs, _ := json.Marshal(config.OSSHPrefixSpecs)
hash.Write(encodedOSSHPrefixSpecs)
}
if config.OSSHPrefixScopedSpecNames != nil {
hash.Write([]byte("OSSHPrefixScopedSpecNames"))
encodedOSSHPrefixScopedSpecNames, _ := json.Marshal(config.OSSHPrefixScopedSpecNames)
hash.Write(encodedOSSHPrefixScopedSpecNames)
}
if config.OSSHPrefixProbability != nil {
hash.Write([]byte("OSSHPrefixProbability"))
binary.Write(hash, binary.LittleEndian, *config.OSSHPrefixProbability)
}
if config.OSSHPrefixSplitMinDelayMilliseconds != nil {
hash.Write([]byte("OSSHPrefixSplitMinDelayMilliseconds"))
binary.Write(hash, binary.LittleEndian, int64(*config.OSSHPrefixSplitMinDelayMilliseconds))
}
if config.OSSHPrefixSplitMaxDelayMilliseconds != nil {
hash.Write([]byte("OSSHPrefixSplitMaxDelayMilliseconds"))
binary.Write(hash, binary.LittleEndian, int64(*config.OSSHPrefixSplitMaxDelayMilliseconds))
}
if config.OSSHPrefixEnableFragmentor != nil {
hash.Write([]byte("OSSHPrefixEnableFragmentor"))
binary.Write(hash, binary.LittleEndian, *config.OSSHPrefixEnableFragmentor)
}
if config.TLSTunnelTrafficShapingProbability != nil {
hash.Write([]byte("TLSTunnelTrafficShapingProbability"))
binary.Write(hash, binary.LittleEndian, *config.TLSTunnelTrafficShapingProbability)
}
if config.TLSTunnelMinTLSPadding != nil {
hash.Write([]byte("TLSTunnelMinTLSPadding"))
binary.Write(hash, binary.LittleEndian, int64(*config.TLSTunnelMinTLSPadding))
}
if config.TLSTunnelMaxTLSPadding != nil {
hash.Write([]byte("TLSTunnelMaxTLSPadding"))
binary.Write(hash, binary.LittleEndian, int64(*config.TLSTunnelMaxTLSPadding))
}
if config.TLSFragmentClientHelloProbability != nil {
hash.Write([]byte("TLSFragmentClientHelloProbability"))
binary.Write(hash, binary.LittleEndian, *config.TLSFragmentClientHelloProbability)
}
if len(config.TLSFragmentClientHelloLimitProtocols) > 0 {
hash.Write([]byte("TLSFragmentClientHelloLimitProtocols"))
for _, protocol := range config.TLSFragmentClientHelloLimitProtocols {
hash.Write([]byte(protocol))
}
}
// Steering IPs are ephemeral and not replayed, so steering IP parameters
// are excluded here.
config.dialParametersHash = hash.Sum(nil)
}
// applyAdditionalParameters decodes and applies any additional parameters
// stored in config.AdditionalParameter to the Config and returns an array
// of notices which should be logged at the info level. If there is no error,
// then config.AdditionalParameter is set to "" to conserve memory and further
// calls will do nothing. This function should only be called once.
//
// If there is an error, the existing Config is left entirely unmodified.
func (config *Config) applyAdditionalParameters() ([]string, error) {
if config.AdditionalParameters == "" {
return nil, nil
}
b, err := base64.StdEncoding.DecodeString(config.AdditionalParameters)
if err != nil {
return nil, errors.Trace(err)
}
if len(b) < 32 {
return nil, errors.Tracef("invalid length, len(b) == %d", len(b))
}
var key [32]byte
copy(key[:], b[:32])
decrypted, ok := secretbox.Open(nil, b[32:], &[24]byte{}, &key)
if !ok {
return nil, errors.TraceNew("secretbox.Open failed")
}
var additionalParameters Config
err = json.Unmarshal(decrypted, &additionalParameters)
if err != nil {
return nil, errors.Trace(err)
}
src := reflect.ValueOf(&additionalParameters).Elem()
dest := reflect.ValueOf(config).Elem()
var infoNotices []string
for i := 0; i < src.NumField(); i++ {
if !src.Field(i).IsZero() {
dest.Field(i).Set(src.Field(i))
infoNotice := fmt.Sprintf("%s overridden by AdditionalParameters", dest.Type().Field(i).Name)
infoNotices = append(infoNotices, infoNotice)
}
}
// Reset field to conserve memory since this is a one-time operation.
config.AdditionalParameters = ""
return infoNotices, nil
}
func promoteLegacyTransferURL(URL string) parameters.TransferURLs {
transferURLs := make(parameters.TransferURLs, 1)
transferURLs[0] = ¶meters.TransferURL{
URL: base64.StdEncoding.EncodeToString([]byte(URL)),
SkipVerify: false,
OnlyAfterAttempts: 0,
}
return transferURLs
}
type loggingDeviceBinder struct {
d DeviceBinder
}
func newLoggingDeviceBinder(d DeviceBinder) *loggingDeviceBinder {
return &loggingDeviceBinder{d: d}
}
func (d *loggingDeviceBinder) BindToDevice(fileDescriptor int) (string, error) {
deviceInfo, err := d.d.BindToDevice(fileDescriptor)
if err == nil && deviceInfo != "" {
NoticeBindToDevice(deviceInfo)
}
return deviceInfo, err
}
type staticNetworkGetter struct {
networkID string
}
func newStaticNetworkGetter(networkID string) *staticNetworkGetter {
return &staticNetworkGetter{networkID: networkID}
}
func (n *staticNetworkGetter) GetNetworkID() string {
return n.networkID
}
type loggingNetworkIDGetter struct {
n NetworkIDGetter
}
func newLoggingNetworkIDGetter(n NetworkIDGetter) *loggingNetworkIDGetter {
return &loggingNetworkIDGetter{n: n}
}
func (n *loggingNetworkIDGetter) GetNetworkID() string {
networkID := n.n.GetNetworkID()
// All PII must appear after the initial "-"
// See: https://godoc.org/github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon#NetworkIDGetter
logNetworkID := networkID
index := strings.Index(logNetworkID, "-")
if index != -1 {
logNetworkID = logNetworkID[:index]
}
if len(logNetworkID)+1 < len(networkID) {
// Indicate when additional network info was present after the first "-".
logNetworkID += "+[redacted]"
}
NoticeNetworkID(logNetworkID)
return networkID
}
// migrationsFromLegacyNoticeFilePaths returns the file migrations which must be
// performed to move notice files from legacy file paths, which were configured
// with the legacy config fields HomepageNoticesFilename and
// RotatingNoticesFilename, to the new file paths used by Psiphon which exist
// under the data root directory.
func migrationsFromLegacyNoticeFilePaths(config *Config) []FileMigration {
var noticeMigrations []FileMigration
if config.MigrateHomepageNoticesFilename != "" {
noticeMigrations = append(noticeMigrations, FileMigration{
Name: "hompage",
OldPath: config.MigrateHomepageNoticesFilename,
NewPath: config.GetHomePageFilename(),
})
}
if config.MigrateRotatingNoticesFilename != "" {
migrations := []FileMigration{
{
Name: "notices",
OldPath: config.MigrateRotatingNoticesFilename,
NewPath: config.GetNoticesFilename(),
IsDir: false,
},
{
Name: "notices.1",
OldPath: config.MigrateRotatingNoticesFilename + ".1",
NewPath: config.GetNoticesFilename() + ".1",
},
}
noticeMigrations = append(noticeMigrations, migrations...)
}
return noticeMigrations
}
// migrationsFromLegacyFilePaths returns the file migrations which must be
// performed to move files from legacy file paths, which were configured with
// legacy config fields, to the new file paths used by Psiphon which exist
// under the data root directory.
// Note: an attempt is made to redact any file paths from the returned error.
func migrationsFromLegacyFilePaths(config *Config) ([]FileMigration, error) {
migrations := []FileMigration{
{
Name: "psiphon.boltdb",
OldPath: filepath.Join(config.MigrateDataStoreDirectory, "psiphon.boltdb"),
NewPath: filepath.Join(config.GetDataStoreDirectory(), "psiphon.boltdb"),
},
{
Name: "psiphon.boltdb.lock",
OldPath: filepath.Join(config.MigrateDataStoreDirectory, "psiphon.boltdb.lock"),
NewPath: filepath.Join(config.GetDataStoreDirectory(), "psiphon.boltdb.lock"),
},
}
if config.MigrateRemoteServerListDownloadFilename != "" {
// Migrate remote server list files
rslMigrations := []FileMigration{
{
Name: "remote_server_list",
OldPath: config.MigrateRemoteServerListDownloadFilename,
NewPath: config.GetRemoteServerListDownloadFilename(),
},
{
Name: "remote_server_list.part",
OldPath: config.MigrateRemoteServerListDownloadFilename + ".part",
NewPath: config.GetRemoteServerListDownloadFilename() + ".part",
},
{
Name: "remote_server_list.part.etag",
OldPath: config.MigrateRemoteServerListDownloadFilename + ".part.etag",
NewPath: config.GetRemoteServerListDownloadFilename() + ".part.etag",
},
}
migrations = append(migrations, rslMigrations...)
}
if config.MigrateObfuscatedServerListDownloadDirectory != "" {
// Migrate OSL registry file and downloads
oslFileRegex, err := regexp.Compile(`^osl-.+$`)
if err != nil {
return nil, errors.TraceMsg(err, "failed to compile regex for osl files")
}
files, err := ioutil.ReadDir(config.MigrateObfuscatedServerListDownloadDirectory)
if err != nil {
NoticeWarning(
"Migration: failed to read OSL download directory with error %s",
common.RedactFilePathsError(err, config.MigrateObfuscatedServerListDownloadDirectory))
} else {
for _, file := range files {
if oslFileRegex.MatchString(file.Name()) {
fileMigration := FileMigration{
Name: "osl",
OldPath: filepath.Join(config.MigrateObfuscatedServerListDownloadDirectory, file.Name()),
NewPath: filepath.Join(config.GetObfuscatedServerListDownloadDirectory(), file.Name()),
}
migrations = append(migrations, fileMigration)
}
}
}
}
if config.MigrateUpgradeDownloadFilename != "" {
// Migrate downloaded upgrade files
oldUpgradeDownloadFilename := filepath.Base(config.MigrateUpgradeDownloadFilename)
// Create regex for:
//
// .
// ..part
// ..part.etag
upgradeDownloadFileRegex, err := regexp.Compile(`^` + oldUpgradeDownloadFilename + `(\.\d+(\.part(\.etag)?)?)?$`)
if err != nil {
return nil, errors.TraceMsg(err, "failed to compile regex for upgrade files")
}
upgradeDownloadDir := filepath.Dir(config.MigrateUpgradeDownloadFilename)
files, err := ioutil.ReadDir(upgradeDownloadDir)
if err != nil {
NoticeWarning(
"Migration: failed to read upgrade download directory with error %s",
common.RedactFilePathsError(err, upgradeDownloadDir))
} else {
for _, file := range files {
if upgradeDownloadFileRegex.MatchString(file.Name()) {
oldFileSuffix := strings.TrimPrefix(file.Name(), oldUpgradeDownloadFilename)
fileMigration := FileMigration{
Name: "upgrade",
OldPath: filepath.Join(upgradeDownloadDir, file.Name()),
NewPath: config.GetUpgradeDownloadFilename() + oldFileSuffix,
}
migrations = append(migrations, fileMigration)
}
}
}
}
return migrations, nil
}