/*
* 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"
"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"
)
const (
TUNNEL_POOL_SIZE = 1
// 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
// TunnelWholeDevice is a flag that is passed through to the handshake
// request for stats purposes. Set to 1 when the host application is
// tunneling the whole device, 0 otherwise.
TunnelWholeDevice int
// 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
// 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", "UNFRONTED-MEEK-OSSH", "UNFRONTED-MEEK-HTTPS-OSSH",
// "UNFRONTED-MEEK-SESSION-TICKET-OSSH", "FRONTED-MEEK-OSSH",
// "FRONTED-MEEK-HTTP-OSSH", "QUIC-OSSH", "MARIONETTE-OSSH", and
// "TAPDANCE-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.
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
// 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
// NetworkConnectivityChecker is an interface that enables tunnel-core to
// call into the host application to check for network connectivity. See:
// NetworkConnectivityChecker doc.
//
// This parameter is only applicable to library deployments.
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.
//
// This parameter is only applicable to library deployments.
DeviceBinder DeviceBinder
// IPv6Synthesizer is an interface that allows tunnel-core to call into
// the host application to synthesize IPv6 addresses. See: IPv6Synthesizer
// doc.
//
// This parameter is only applicable to library deployments.
IPv6Synthesizer IPv6Synthesizer
// 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.
//
// This parameter is only applicable to library deployments.
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.
//
// This parameter is only applicable to library deployments.
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
// SplitTunnelRoutesURLFormat is a URL which specifies the location of a
// routes file to use for split tunnel mode. The URL must include a
// placeholder for the client region to be supplied. Split tunnel mode
// uses the routes file to classify port forward destinations as foreign
// or domestic and does not tunnel domestic destinations. Split tunnel
// mode is on when all the SplitTunnel parameters are supplied. This value
// is supplied by and depends on the Psiphon Network, and is typically
// embedded in the client binary.
SplitTunnelRoutesURLFormat string
// SplitTunnelRoutesSignaturePublicKey specifies a public key that's used
// to authenticate the split tunnel routes payload. This value is supplied
// by and depends on the Psiphon Network, and is typically embedded in the
// client binary.
SplitTunnelRoutesSignaturePublicKey string
// SplitTunnelDNSServer specifies a DNS server to use when resolving port
// forward target domain names to IP addresses for classification. The DNS
// server must support TCP requests.
SplitTunnelDNSServer string
// 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
// 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
// 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
// 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
// EmitTapdanceLogs 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.
EmitTapdanceLogs bool
// EmitServerAlerts indicates whether to emit notices for server alerts.
EmitServerAlerts 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
// 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
// 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
// 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
// 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
// ApplicationParameters is for testing purposes.
ApplicationParameters parameters.KeyValues
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// clientParameters is the active ClientParameters with defaults, config
// values, and, optionally, tactics applied.
//
// New tactics must be applied by calling Config.SetClientParameters;
// calling clientParameters.Set directly will fail to add config values.
clientParameters *parameters.ClientParameters
dialParametersHash []byte
dynamicConfigMutex sync.Mutex
sponsorID string
authorizations []string
deviceBinder DeviceBinder
networkIDGetter NetworkIDGetter
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 {
// 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(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 %s with error: %s", dataDirectoryPath, err.Error())
}
}
// 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)
for _, migration := range noticeMigrations {
err := common.DoFileMigration(migration)
if err != nil {
alertMsg := fmt.Sprintf("Config migration: %s", errors.Trace(err))
noticeMigrationAlertMsgs = append(noticeMigrationAlertMsgs, alertMsg)
} else {
infoMsg := fmt.Sprintf("Config migration: moved %s to %s", migration.OldPath, migration.NewPath)
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 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 %s with error: %s", dataStoreDirectoryPath, err.Error())
}
}
// 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 %s with error: %s", oslDirectoryPath, err.Error())
}
}
// Create tapdance directory
tapdanceDirectoryPath := config.GetTapdanceDirectory()
if !common.FileExists(tapdanceDirectoryPath) {
err := os.Mkdir(tapdanceDirectoryPath, os.ModePerm)
if err != nil {
return errors.Tracef("failed to create tapdance directory %s with error: %s", tapdanceDirectoryPath, err.Error())
}
}
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.Tracef("DataRootDirectory does not exist: %s", config.DataRootDirectory)
}
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.SplitTunnelRoutesURLFormat != "" {
if config.SplitTunnelRoutesSignaturePublicKey == "" {
return errors.TraceNew("missing SplitTunnelRoutesSignaturePublicKey")
}
if config.SplitTunnelDNSServer == "" {
return errors.TraceNew("missing SplitTunnelDNSServer")
}
}
if config.UpgradeDownloadURLs != nil {
if config.UpgradeDownloadClientVersionHeader == "" {
return errors.TraceNew("missing UpgradeDownloadClientVersionHeader")
}
}
if config.FeedbackUploadURLs != nil {
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.clientParameters, err = parameters.NewClientParameters(
func(err error) {
NoticeWarning("ClientParameters 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")
}
// clientParameters.Set will validate the config fields applied to parameters.
err = config.SetClientParameters("", 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)
// 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 %s", wd)
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
for _, migration := range migrations {
err := common.DoFileMigration(migration)
if err != nil {
NoticeWarning("Config migration: %s", errors.Trace(err))
} else {
NoticeInfo("Config migration: moved %s to %s", migration.OldPath, migration.NewPath)
}
}
// Remove OSL directory if empty
if config.MigrateObfuscatedServerListDownloadDirectory != "" {
files, err := ioutil.ReadDir(config.MigrateObfuscatedServerListDownloadDirectory)
if err != nil {
NoticeWarning("Error reading OSL directory %s: %s", config.MigrateObfuscatedServerListDownloadDirectory, errors.Trace(err))
} else if len(files) == 0 {
err := os.Remove(config.MigrateObfuscatedServerListDownloadDirectory)
if err != nil {
NoticeWarning("Error deleting empty OSL directory %s: %s", config.MigrateObfuscatedServerListDownloadDirectory, errors.Trace(err))
}
}
}
f, err := os.Create(migrationCompleteFilePath)
if err != nil {
NoticeWarning("Config migration: failed to create %s with error %s", migrationCompleteFilePath, errors.Trace(err))
} else {
NoticeInfo("Config migration: completed")
f.Close()
}
}
config.committed = true
return nil
}
// GetClientParameters returns the current client parameters.
func (config *Config) GetClientParameters() *parameters.ClientParameters {
return config.clientParameters
}
// SetClientParameters resets Config.clientParameters 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.clientParameters.Set
// directly as this will not first apply config values.
//
// If there is an error, the existing Config.clientParameters are left
// entirely unmodified.
func (config *Config) SetClientParameters(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.clientParameters.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.clientParameters.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
}
// 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
}
// 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)
}
// GetTapdanceDirectory returns the directory under which tapdance will create
// and manage files.
func (config *Config) GetTapdanceDirectory() string {
return filepath.Join(config.GetPsiphonDataDirectory(), "tapdance")
}
// 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.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
}
}
applyParameters[parameters.SplitTunnelRoutesURLFormat] = config.SplitTunnelRoutesURLFormat
applyParameters[parameters.SplitTunnelRoutesSignaturePublicKey] = config.SplitTunnelRoutesSignaturePublicKey
applyParameters[parameters.SplitTunnelDNSServer] = config.SplitTunnelDNSServer
if config.UpgradeDownloadURLs != nil {
applyParameters[parameters.UpgradeDownloadClientVersionHeader] = config.UpgradeDownloadClientVersionHeader
applyParameters[parameters.UpgradeDownloadURLs] = config.UpgradeDownloadURLs
}
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.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.UseOnlyCustomTLSProfiles != nil {
applyParameters[parameters.UseOnlyCustomTLSProfiles] = *config.UseOnlyCustomTLSProfiles
}
if config.CustomTLSProfiles != nil {
applyParameters[parameters.CustomTLSProfiles] = config.CustomTLSProfiles
}
if config.SelectRandomizedTLSProfileProbability != nil {
applyParameters[parameters.SelectRandomizedTLSProfileProbability] = *config.SelectRandomizedTLSProfileProbability
}
if config.NoDefaultTLSSessionIDProbability != nil {
applyParameters[parameters.NoDefaultTLSSessionIDProbability] = *config.NoDefaultTLSSessionIDProbability
}
if config.ApplicationParameters != nil {
applyParameters[parameters.ApplicationParameters] = config.ApplicationParameters
}
if len(config.FeedbackUploadURLs) > 0 {
applyParameters[parameters.FeedbackUploadURLs] = config.FeedbackUploadURLs
}
if config.FeedbackEncryptionPublicKey != "" {
applyParameters[parameters.FeedbackEncryptionPublicKey] = config.FeedbackEncryptionPublicKey
}
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.
//
// MD5 hash is used solely as a data checksum and not for any security
// purpose; serialization is not strictly unambiguous.
hash := md5.New()
if len(config.LimitTunnelProtocols) > 0 {
for _, protocol := range config.LimitTunnelProtocols {
hash.Write([]byte(protocol))
}
}
if len(config.InitialLimitTunnelProtocols) > 0 && config.InitialLimitTunnelProtocolsCandidateCount > 0 {
for _, protocol := range config.InitialLimitTunnelProtocols {
hash.Write([]byte(protocol))
}
binary.Write(hash, binary.LittleEndian, int64(config.InitialLimitTunnelProtocolsCandidateCount))
}
if len(config.LimitTLSProfiles) > 0 {
for _, profile := range config.LimitTLSProfiles {
hash.Write([]byte(profile))
}
}
if len(config.LimitQUICVersions) > 0 {
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{1})
}
if config.UpstreamProxyURL != "" {
hash.Write([]byte(config.UpstreamProxyURL))
}
if config.TransformHostNameProbability != nil {
binary.Write(hash, binary.LittleEndian, *config.TransformHostNameProbability)
}
if config.FragmentorProbability != nil {
binary.Write(hash, binary.LittleEndian, *config.FragmentorProbability)
}
if len(config.FragmentorLimitProtocols) > 0 {
for _, protocol := range config.FragmentorLimitProtocols {
hash.Write([]byte(protocol))
}
}
if config.FragmentorMinTotalBytes != nil {
binary.Write(hash, binary.LittleEndian, int64(*config.FragmentorMinTotalBytes))
}
if config.FragmentorMaxTotalBytes != nil {
binary.Write(hash, binary.LittleEndian, int64(*config.FragmentorMaxTotalBytes))
}
if config.FragmentorMinWriteBytes != nil {
binary.Write(hash, binary.LittleEndian, int64(*config.FragmentorMinWriteBytes))
}
if config.FragmentorMaxWriteBytes != nil {
binary.Write(hash, binary.LittleEndian, int64(*config.FragmentorMaxWriteBytes))
}
if config.FragmentorMinDelayMicroseconds != nil {
binary.Write(hash, binary.LittleEndian, int64(*config.FragmentorMinDelayMicroseconds))
}
if config.FragmentorMaxDelayMicroseconds != nil {
binary.Write(hash, binary.LittleEndian, int64(*config.FragmentorMaxDelayMicroseconds))
}
if config.MeekTrafficShapingProbability != nil {
binary.Write(hash, binary.LittleEndian, int64(*config.MeekTrafficShapingProbability))
}
if len(config.MeekTrafficShapingLimitProtocols) > 0 {
for _, protocol := range config.MeekTrafficShapingLimitProtocols {
hash.Write([]byte(protocol))
}
}
if config.MeekMinLimitRequestPayloadLength != nil {
binary.Write(hash, binary.LittleEndian, int64(*config.MeekMinLimitRequestPayloadLength))
}
if config.MeekMaxLimitRequestPayloadLength != nil {
binary.Write(hash, binary.LittleEndian, int64(*config.MeekMaxLimitRequestPayloadLength))
}
if config.MeekRedialTLSProbability != nil {
binary.Write(hash, binary.LittleEndian, *config.MeekRedialTLSProbability)
}
if config.ObfuscatedSSHMinPadding != nil {
binary.Write(hash, binary.LittleEndian, int64(*config.ObfuscatedSSHMinPadding))
}
if config.ObfuscatedSSHMaxPadding != nil {
binary.Write(hash, binary.LittleEndian, int64(*config.ObfuscatedSSHMaxPadding))
}
if config.LivenessTestMinUpstreamBytes != nil {
binary.Write(hash, binary.LittleEndian, int64(*config.LivenessTestMinUpstreamBytes))
}
if config.LivenessTestMaxUpstreamBytes != nil {
binary.Write(hash, binary.LittleEndian, int64(*config.LivenessTestMaxUpstreamBytes))
}
if config.LivenessTestMinDownstreamBytes != nil {
binary.Write(hash, binary.LittleEndian, int64(*config.LivenessTestMinDownstreamBytes))
}
if config.LivenessTestMaxDownstreamBytes != nil {
binary.Write(hash, binary.LittleEndian, int64(*config.LivenessTestMaxDownstreamBytes))
}
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 {
binary.Write(hash, binary.LittleEndian, *config.UseOnlyCustomTLSProfiles)
}
for _, customTLSProfile := range config.CustomTLSProfiles {
// Assumes consistent definition for a given profile name
hash.Write([]byte(customTLSProfile.Name))
}
if config.SelectRandomizedTLSProfileProbability != nil {
binary.Write(hash, binary.LittleEndian, *config.SelectRandomizedTLSProfileProbability)
}
if config.NoDefaultTLSSessionIDProbability != nil {
binary.Write(hash, binary.LittleEndian, *config.NoDefaultTLSSessionIDProbability)
}
config.dialParametersHash = hash.Sum(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) []common.FileMigration {
var noticeMigrations []common.FileMigration
if config.MigrateHomepageNoticesFilename != "" {
noticeMigrations = append(noticeMigrations, common.FileMigration{
OldPath: config.MigrateHomepageNoticesFilename,
NewPath: config.GetHomePageFilename(),
})
}
if config.MigrateRotatingNoticesFilename != "" {
migrations := []common.FileMigration{
{
OldPath: config.MigrateRotatingNoticesFilename,
NewPath: config.GetNoticesFilename(),
IsDir: false,
},
{
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.
func migrationsFromLegacyFilePaths(config *Config) ([]common.FileMigration, error) {
migrations := []common.FileMigration{
{
OldPath: filepath.Join(config.MigrateDataStoreDirectory, "psiphon.boltdb"),
NewPath: filepath.Join(config.GetDataStoreDirectory(), "psiphon.boltdb"),
},
{
OldPath: filepath.Join(config.MigrateDataStoreDirectory, "psiphon.boltdb.lock"),
NewPath: filepath.Join(config.GetDataStoreDirectory(), "psiphon.boltdb.lock"),
},
{
OldPath: filepath.Join(config.MigrateDataStoreDirectory, "tapdance"),
NewPath: filepath.Join(config.GetTapdanceDirectory(), "tapdance"),
IsDir: true,
},
}
if config.MigrateRemoteServerListDownloadFilename != "" {
// Migrate remote server list files
rslMigrations := []common.FileMigration{
{
OldPath: config.MigrateRemoteServerListDownloadFilename,
NewPath: config.GetRemoteServerListDownloadFilename(),
},
{
OldPath: config.MigrateRemoteServerListDownloadFilename + ".part",
NewPath: config.GetRemoteServerListDownloadFilename() + ".part",
},
{
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 directory %s with error %s", config.MigrateObfuscatedServerListDownloadDirectory, err)
} else {
for _, file := range files {
if oslFileRegex.MatchString(file.Name()) {
fileMigration := common.FileMigration{
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 directory %s with error %s", upgradeDownloadDir, err)
} else {
for _, file := range files {
if upgradeDownloadFileRegex.MatchString(file.Name()) {
oldFileSuffix := strings.TrimPrefix(file.Name(), oldUpgradeDownloadFilename)
fileMigration := common.FileMigration{
OldPath: filepath.Join(upgradeDownloadDir, file.Name()),
NewPath: config.GetUpgradeDownloadFilename() + oldFileSuffix,
}
migrations = append(migrations, fileMigration)
}
}
}
}
return migrations, nil
}