| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537 |
- package psiphon
- import (
- "context"
- "net"
- "net/http"
- "strconv"
- "sync/atomic"
- "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/fragmentor"
- "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
- "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
- "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
- "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/resolver"
- utls "github.com/Psiphon-Labs/utls"
- "golang.org/x/net/bpf"
- )
- // FrontedMeekDialParameters represents a selected fronting transport and all
- // the related protocol attributes, many chosen at random, for a fronted dial
- // attempt.
- //
- // FrontedMeekDialParameters is used:
- // - to configure dialers
- // - as a persistent record to store successful dial parameters for replay
- // - to report dial stats in notices and Psiphon API calls.
- //
- // FrontedMeekDialParameters is similar to tunnel DialParameters, but is
- // specific to fronted meek. It should be used for all fronted meek dials,
- // apart from the tunnel DialParameters cases.
- //
- // prepareDialConfigs must be called on any unmarshaled
- // FrontedMeekDialParameters. For example, when unmarshaled from a replay
- // record.
- //
- // resolvedIPAddress is set asynchronously, as it is not known until the dial
- // process has begun. The atomic.Value will contain a string, initialized to
- // "", and set to the resolved IP address once that part of the dial process
- // has completed.
- //
- // FrontedMeekDialParameters is not safe for concurrent use.
- type FrontedMeekDialParameters struct {
- NetworkLatencyMultiplier float64
- FrontingTransport string
- DialAddress string
- FrontingProviderID string
- FrontingDialAddress string
- SNIServerName string
- TransformedHostName bool
- VerifyServerName string
- VerifyPins []string
- HostHeader string
- resolvedIPAddress atomic.Value `json:"-"`
- TLSProfile string
- TLSVersion string
- RandomizedTLSProfileSeed *prng.Seed
- NoDefaultTLSSessionID bool
- TLSFragmentClientHello bool
- SelectedUserAgent bool
- UserAgent string
- BPFProgramName string
- BPFProgramInstructions []bpf.RawInstruction
- FragmentorSeed *prng.Seed
- ResolveParameters *resolver.ResolveParameters
- dialConfig *DialConfig `json:"-"`
- meekConfig *MeekConfig `json:"-"`
- }
- // makeFrontedMeekDialParameters creates a new FrontedMeekDialParameters for
- // configuring a fronted HTTP client, including selecting a fronting transport,
- // and all the various protocol attributes.
- //
- // payloadSecure must only be set if all HTTP plaintext payloads sent through
- // the returned net/http.Client will be wrapped in their own transport security
- // layer, which permits skipping of server certificate verification.
- func makeFrontedMeekDialParameters(
- config *Config,
- p parameters.ParametersAccessor,
- tunnel *Tunnel,
- frontingSpecs parameters.FrontingSpecs,
- selectedFrontingProviderID func(string),
- useDeviceBinder,
- skipVerify,
- disableSystemRootCAs,
- payloadSecure bool,
- tlsCache utls.ClientSessionCache) (*FrontedMeekDialParameters, error) {
- // This function duplicates some code from MakeDialParameters. To simplify
- // the logic, the Replay<Component> tactic flags for individual dial
- // components are ignored.
- //
- // TODO: merge common functionality?
- if !payloadSecure && (skipVerify || disableSystemRootCAs) {
- return nil, errors.TraceNew("cannot skip certificate verification if payload insecure")
- }
- frontedMeekDialParams := FrontedMeekDialParameters{}
- // Network latency multiplier
- frontedMeekDialParams.NetworkLatencyMultiplier = prng.ExpFloat64Range(
- p.Float(parameters.NetworkLatencyMultiplierMin),
- p.Float(parameters.NetworkLatencyMultiplierMax),
- p.Float(parameters.NetworkLatencyMultiplierLambda))
- // Select fronting configuration
- var err error
- frontedMeekDialParams.FrontingProviderID,
- frontedMeekDialParams.FrontingTransport,
- frontedMeekDialParams.FrontingDialAddress,
- frontedMeekDialParams.SNIServerName,
- frontedMeekDialParams.VerifyServerName,
- frontedMeekDialParams.VerifyPins,
- frontedMeekDialParams.HostHeader,
- err = frontingSpecs.SelectParameters()
- if err != nil {
- return nil, errors.Trace(err)
- }
- // At this time, the transport is limited to fronted HTTPS.
- //
- // As a future enhancement, allow HTTP in certain cases (e.g. the in-proxy
- // broker case), skip selecting TLS tactics and select HTTP tactics such as
- // HTTPTransformerParameters; and allow QUIC and select QUIC tactics.
- if frontedMeekDialParams.FrontingTransport != protocol.FRONTING_TRANSPORT_HTTPS {
- return nil, errors.TraceNew("unsupported fronting transport")
- }
- if selectedFrontingProviderID != nil {
- selectedFrontingProviderID(frontedMeekDialParams.FrontingProviderID)
- }
- // FrontingSpec.Addresses may include a port; default to 443 if none.
- if _, _, err := net.SplitHostPort(frontedMeekDialParams.FrontingDialAddress); err == nil {
- frontedMeekDialParams.DialAddress = frontedMeekDialParams.FrontingDialAddress
- } else {
- frontedMeekDialParams.DialAddress = net.JoinHostPort(frontedMeekDialParams.FrontingDialAddress, "443")
- }
- // Determine and use the equivalent tunnel protocol for tactics
- // selections. For example, for the broker transport FRONTED-HTTPS, use
- // the tactics for FRONTED-MEEK-OSSH.
- equivalentTunnelProtocol, err := protocol.EquivilentTunnelProtocol(frontedMeekDialParams.FrontingTransport)
- if err != nil {
- return nil, errors.Trace(err)
- }
- // SNI configuration
- //
- // For a FrontingSpec, an SNI value of "" indicates to disable/omit SNI, so
- // never transform in that case.
- if frontedMeekDialParams.SNIServerName != "" {
- if p.WeightedCoinFlip(parameters.TransformHostNameProbability) {
- frontedMeekDialParams.SNIServerName = selectHostName(equivalentTunnelProtocol, p)
- frontedMeekDialParams.TransformedHostName = true
- }
- }
- // TLS configuration
- //
- // In the in-proxy case, the requireTLS13 flag is set to true, and
- // requireTLS12SessionTickets to false, in order to use only modern TLS
- // fingerprints which should support HTTP/2 in the ALPN.
- //
- // TODO: TLS padding
- requireTLS12SessionTickets :=
- !protocol.TunnelProtocolUsesInproxy(equivalentTunnelProtocol) &&
- protocol.TunnelProtocolRequiresTLS12SessionTickets(
- equivalentTunnelProtocol)
- requireTLS13Support :=
- protocol.TunnelProtocolUsesInproxy(equivalentTunnelProtocol) ||
- protocol.TunnelProtocolRequiresTLS13Support(equivalentTunnelProtocol)
- isFronted := true
- frontedMeekDialParams.TLSProfile,
- frontedMeekDialParams.TLSVersion,
- frontedMeekDialParams.RandomizedTLSProfileSeed,
- err = SelectTLSProfile(requireTLS12SessionTickets, requireTLS13Support, isFronted, frontedMeekDialParams.FrontingProviderID, p)
- if err != nil {
- return nil, errors.Trace(err)
- }
- if frontedMeekDialParams.TLSProfile == "" && (requireTLS12SessionTickets || requireTLS13Support) {
- return nil, errors.TraceNew("required TLS profile not found")
- }
- frontedMeekDialParams.NoDefaultTLSSessionID = p.WeightedCoinFlip(
- parameters.NoDefaultTLSSessionIDProbability)
- if frontedMeekDialParams.SNIServerName != "" && net.ParseIP(frontedMeekDialParams.SNIServerName) == nil {
- tlsFragmentorLimitProtocols := p.TunnelProtocols(parameters.TLSFragmentClientHelloLimitProtocols)
- if len(tlsFragmentorLimitProtocols) == 0 || common.Contains(tlsFragmentorLimitProtocols, equivalentTunnelProtocol) {
- frontedMeekDialParams.TLSFragmentClientHello = p.WeightedCoinFlip(parameters.TLSFragmentClientHelloProbability)
- }
- }
- // User Agent configuration
- dialCustomHeaders := makeDialCustomHeaders(config, p)
- frontedMeekDialParams.SelectedUserAgent, frontedMeekDialParams.UserAgent = selectUserAgentIfUnset(p, dialCustomHeaders)
- // Resolver configuration
- //
- // The custom resolver is wired up only when there is a domain to be
- // resolved; GetMetrics will log resolver metrics when the resolver is set.
- if net.ParseIP(frontedMeekDialParams.DialAddress) == nil {
- resolver := config.GetResolver()
- if resolver == nil {
- return nil, errors.TraceNew("missing resolver")
- }
- frontedMeekDialParams.ResolveParameters, err = resolver.MakeResolveParameters(
- p, frontedMeekDialParams.FrontingProviderID, frontedMeekDialParams.DialAddress)
- if err != nil {
- return nil, errors.Trace(err)
- }
- }
- if tunnel == nil {
- // BPF configuration
- if ClientBPFEnabled() &&
- protocol.TunnelProtocolMayUseClientBPF(equivalentTunnelProtocol) {
- if p.WeightedCoinFlip(parameters.BPFClientTCPProbability) {
- frontedMeekDialParams.BPFProgramName = ""
- frontedMeekDialParams.BPFProgramInstructions = nil
- ok, name, rawInstructions := p.BPFProgram(parameters.BPFClientTCPProgram)
- if ok {
- frontedMeekDialParams.BPFProgramName = name
- frontedMeekDialParams.BPFProgramInstructions = rawInstructions
- }
- }
- }
- // Fragmentor configuration
- frontedMeekDialParams.FragmentorSeed, err = prng.NewSeed()
- if err != nil {
- return nil, errors.Trace(err)
- }
- }
- // Initialize Dial/MeekConfigs to be passed to the corresponding dialers.
- err = frontedMeekDialParams.prepareDialConfigs(
- config, p, tunnel, dialCustomHeaders, useDeviceBinder, skipVerify,
- disableSystemRootCAs, payloadSecure, tlsCache)
- if err != nil {
- return nil, errors.Trace(err)
- }
- return &frontedMeekDialParams, nil
- }
- // prepareDialConfigs is called for both new and replayed dial parameters.
- func (f *FrontedMeekDialParameters) prepareDialConfigs(
- config *Config,
- p parameters.ParametersAccessor,
- tunnel *Tunnel,
- dialCustomHeaders http.Header,
- useDeviceBinder,
- skipVerify,
- disableSystemRootCAs,
- payloadSecure bool,
- tlsCache utls.ClientSessionCache) error {
- if !payloadSecure && (skipVerify || disableSystemRootCAs) {
- return errors.TraceNew("cannot skip certificate verification if payload insecure")
- }
- equivilentTunnelProtocol, err := protocol.EquivilentTunnelProtocol(f.FrontingTransport)
- if err != nil {
- return errors.Trace(err)
- }
- // Custom headers and User Agent
- if dialCustomHeaders == nil {
- dialCustomHeaders = makeDialCustomHeaders(config, p)
- }
- if f.SelectedUserAgent {
- dialCustomHeaders.Set("User-Agent", f.UserAgent)
- }
- // Fragmentor
- fragmentorConfig := fragmentor.NewUpstreamConfig(
- p, equivilentTunnelProtocol, f.FragmentorSeed)
- // Resolver
- //
- // DialConfig.ResolveIP is required and called even when the destination
- // is an IP address.
- resolver := config.GetResolver()
- if resolver == nil {
- return errors.TraceNew("missing resolver")
- }
- // DialConfig
- f.resolvedIPAddress.Store("")
- var resolveIP func(context.Context, string) ([]net.IP, error)
- if tunnel != nil {
- tunneledDialer := func(_, addr string) (net.Conn, error) {
- // Set alwaysTunneled to ensure the http.Client traffic is always tunneled,
- // even when split tunnel mode is enabled.
- conn, _, err := tunnel.DialTCPChannel(addr, true, nil)
- return conn, errors.Trace(err)
- }
- f.dialConfig = &DialConfig{
- DiagnosticID: f.FrontingProviderID,
- TrustedCACertificatesFilename: config.TrustedCACertificatesFilename,
- CustomDialer: func(_ context.Context, _, addr string) (net.Conn, error) {
- return tunneledDialer("", addr)
- },
- }
- } else {
- resolveIP = func(ctx context.Context, hostname string) ([]net.IP, error) {
- IPs, err := UntunneledResolveIP(
- ctx, config, resolver, hostname, f.FrontingProviderID)
- if err != nil {
- return nil, errors.Trace(err)
- }
- return IPs, nil
- }
- var deviceBinder DeviceBinder
- if useDeviceBinder {
- deviceBinder = config.DeviceBinder
- }
- f.dialConfig = &DialConfig{
- DiagnosticID: f.FrontingProviderID,
- UpstreamProxyURL: config.UpstreamProxyURL,
- CustomHeaders: dialCustomHeaders,
- BPFProgramInstructions: f.BPFProgramInstructions,
- TrustedCACertificatesFilename: config.TrustedCACertificatesFilename,
- FragmentorConfig: fragmentorConfig,
- DeviceBinder: deviceBinder,
- IPv6Synthesizer: config.IPv6Synthesizer,
- ResolveIP: resolveIP,
- ResolvedIPCallback: func(IPAddress string) {
- f.resolvedIPAddress.Store(IPAddress)
- },
- }
- }
- // MeekDialConfig
- // Note: if MeekModeRelay or MeekModeObfuscatedRoundTrip are supported in the
- // future, set MeekObfuscatorPaddingSeed.
- var meekMode MeekMode = MeekModePlaintextRoundTrip
- if payloadSecure {
- meekMode = MeekModeWrappedPlaintextRoundTrip
- }
- addPsiphonFrontingHeader := false
- if f.FrontingProviderID != "" {
- addPsiphonFrontingHeader = common.Contains(
- p.LabeledTunnelProtocols(
- parameters.AddFrontingProviderPsiphonFrontingHeader,
- f.FrontingProviderID),
- equivilentTunnelProtocol)
- }
- f.meekConfig = &MeekConfig{
- DiagnosticID: f.FrontingProviderID,
- Parameters: config.GetParameters(),
- Mode: meekMode,
- DialAddress: f.DialAddress,
- TLSProfile: f.TLSProfile,
- TLSFragmentClientHello: f.TLSFragmentClientHello,
- NoDefaultTLSSessionID: f.NoDefaultTLSSessionID,
- RandomizedTLSProfileSeed: f.RandomizedTLSProfileSeed,
- SNIServerName: f.SNIServerName,
- HostHeader: f.HostHeader,
- TransformedHostName: f.TransformedHostName,
- AddPsiphonFrontingHeader: addPsiphonFrontingHeader,
- VerifyServerName: f.VerifyServerName,
- VerifyPins: f.VerifyPins,
- ClientTunnelProtocol: equivilentTunnelProtocol,
- NetworkLatencyMultiplier: f.NetworkLatencyMultiplier,
- AdditionalHeaders: config.MeekAdditionalHeaders,
- // CustomTLSDial will use the resolved IP address as the session key.
- TLSClientSessionCache: common.WrapUtlsClientSessionCache(tlsCache, common.TLS_NULL_SESSION_KEY),
- }
- if !skipVerify {
- f.meekConfig.DisableSystemRootCAs = disableSystemRootCAs
- if !f.meekConfig.DisableSystemRootCAs {
- f.meekConfig.VerifyServerName = f.VerifyServerName
- f.meekConfig.VerifyPins = f.VerifyPins
- }
- }
- switch f.FrontingTransport {
- case protocol.FRONTING_TRANSPORT_HTTPS:
- f.meekConfig.UseHTTPS = true
- case protocol.FRONTING_TRANSPORT_QUIC:
- // TODO: configure QUIC tactics
- f.meekConfig.UseQUIC = true
- }
- return nil
- }
- // GetMetrics returns log fields detailing the fronted meek dial parameters.
- // All log field names are prefixed with overridePrefix, when specified, which
- // also overrides any default prefixes.
- func (meekDialParameters *FrontedMeekDialParameters) GetMetrics(overridePrefix string) common.LogFields {
- prefix := ""
- meekPrefix := "meek_"
- if overridePrefix != "" {
- prefix = overridePrefix
- meekPrefix = overridePrefix
- }
- logFields := make(common.LogFields)
- logFields[prefix+"fronting_provider_id"] = meekDialParameters.FrontingProviderID
- if meekDialParameters.DialAddress != "" {
- logFields[meekPrefix+"dial_address"] = meekDialParameters.DialAddress
- }
- meekResolvedIPAddress := meekDialParameters.resolvedIPAddress.Load().(string)
- if meekResolvedIPAddress != "" {
- logFields[meekPrefix+"resolved_ip_address"] = meekResolvedIPAddress
- }
- if meekDialParameters.SNIServerName != "" {
- logFields[meekPrefix+"sni_server_name"] = meekDialParameters.SNIServerName
- }
- if meekDialParameters.HostHeader != "" {
- logFields[meekPrefix+"host_header"] = meekDialParameters.HostHeader
- }
- transformedHostName := "0"
- if meekDialParameters.TransformedHostName {
- transformedHostName = "1"
- }
- logFields[meekPrefix+"transformed_host_name"] = transformedHostName
- if meekDialParameters.SelectedUserAgent {
- logFields[prefix+"user_agent"] = meekDialParameters.UserAgent
- }
- if meekDialParameters.FrontingTransport == protocol.FRONTING_TRANSPORT_HTTPS {
- if meekDialParameters.TLSProfile != "" {
- logFields[prefix+"tls_profile"] = meekDialParameters.TLSProfile
- }
- if meekDialParameters.TLSVersion != "" {
- logFields[prefix+"tls_version"] =
- getTLSVersionForMetrics(meekDialParameters.TLSVersion, meekDialParameters.NoDefaultTLSSessionID)
- }
- tlsFragmented := "0"
- if meekDialParameters.TLSFragmentClientHello {
- tlsFragmented = "1"
- }
- logFields[prefix+"tls_fragmented"] = tlsFragmented
- }
- if meekDialParameters.BPFProgramName != "" {
- logFields[prefix+"client_bpf"] = meekDialParameters.BPFProgramName
- }
- if meekDialParameters.ResolveParameters != nil {
- // See comment for dialParams.ResolveParameters handling in
- // getBaseAPIParameters.
- if meekDialParameters.ResolveParameters.PreresolvedIPAddress != "" {
- dialDomain, _, _ := net.SplitHostPort(meekDialParameters.meekConfig.DialAddress)
- if meekDialParameters.ResolveParameters.PreresolvedDomain == dialDomain {
- logFields[prefix+"dns_preresolved"] = meekDialParameters.ResolveParameters.PreresolvedIPAddress
- }
- }
- if meekDialParameters.ResolveParameters.PreferAlternateDNSServer {
- logFields[prefix+"dns_preferred"] = meekDialParameters.ResolveParameters.AlternateDNSServer
- }
- if meekDialParameters.ResolveParameters.ProtocolTransformName != "" {
- logFields[prefix+"dns_transform"] = meekDialParameters.ResolveParameters.ProtocolTransformName
- }
- if meekDialParameters.ResolveParameters.RandomQNameCasingSeed != nil {
- logFields[prefix+"dns_qname_random_casing"] = "1"
- }
- if meekDialParameters.ResolveParameters.ResponseQNameMustMatch {
- logFields[prefix+"dns_qname_must_match"] = "1"
- }
- logFields[prefix+"dns_qname_mismatches"] = strconv.Itoa(
- meekDialParameters.ResolveParameters.GetQNameMismatches())
- logFields[prefix+"dns_attempt"] = strconv.Itoa(
- meekDialParameters.ResolveParameters.GetFirstAttemptWithAnswer())
- }
- // TODO: get fragmentor metrics, if any, from MeekConn.
- return logFields
- }
|