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

Merge pull request #146 from rod-hynes/master

Merge in several changes
Rod Hynes 10 лет назад
Родитель
Сommit
398532bcb4

+ 2 - 0
AndroidLibrary/psi/psi.go

@@ -68,6 +68,8 @@ func Start(
 			provider.Notice(string(notice))
 		}))
 
+	psiphon.EmitNoticeBuildInfo()
+
 	// TODO: should following errors be Notices?
 
 	err = psiphon.InitDataStore(config)

+ 2 - 0
ConsoleClient/psiphonClient.go

@@ -61,6 +61,8 @@ func main() {
 	}
 	psiphon.SetNoticeOutput(noticeWriter)
 
+	psiphon.EmitNoticeBuildInfo()
+
 	// Handle required config file parameter
 
 	if configFilename == "" {

+ 2 - 1
psiphon/TCPConn.go

@@ -70,7 +70,8 @@ func makeTCPDialer(config *DialConfig) func(network, addr string) (net.Conn, err
 				if err == nil {
 					config.ResolvedIPCallback(host)
 				}
-			}		}
+			}
+		}
 		return conn, nil
 	}
 }

+ 7 - 18
psiphon/buildinfo.go

@@ -44,22 +44,11 @@ var goVersion string
 // -X github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon.gomobileVersion=`gomobile version | perl -ne '/gomobile version (.*?) / && print $1'`
 var gomobileVersion string
 
-func NoticeBuildInfo() {
-
-	if len(strings.TrimSpace(gomobileVersion)) > 0 {
-		NoticeInfo(
-			"Built: %#v from %#v at rev %#v using go: %#v and gomobile: %#v",
-			strings.TrimSpace(buildDate),
-			strings.TrimSpace(buildRepo),
-			strings.TrimSpace(buildRev),
-			strings.TrimSpace(goVersion),
-			strings.TrimSpace(gomobileVersion))
-	} else {
-		NoticeInfo(
-			"Built: %#v from %#v at rev %#v using go: %#v",
-			strings.TrimSpace(buildDate),
-			strings.TrimSpace(buildRepo),
-			strings.TrimSpace(buildRev),
-			strings.TrimSpace(goVersion))
-	}
+func EmitNoticeBuildInfo() {
+	NoticeBuildInfo(
+		strings.TrimSpace(buildDate),
+		strings.TrimSpace(buildRepo),
+		strings.TrimSpace(buildRev),
+		strings.TrimSpace(goVersion),
+		strings.TrimSpace(gomobileVersion))
 }

+ 12 - 2
psiphon/config.go

@@ -150,8 +150,8 @@ type Config struct {
 
 	// TunnelProtocol indicates which protocol to use. Valid values include:
 	// "SSH", "OSSH", "UNFRONTED-MEEK-OSSH", "UNFRONTED-MEEK-HTTPS-OSSH",
-	// "FRONTED-MEEK-OSSH". For the default, "", the best performing protocol
-	// is used.
+	// "FRONTED-MEEK-OSSH", "FRONTED-MEEK-HTTP-OSSH". For the default, "",
+	// the best performing protocol is used.
 	TunnelProtocol string
 
 	// EstablishTunnelTimeoutSeconds specifies a time limit after which to halt
@@ -289,6 +289,12 @@ type Config struct {
 	// 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
+	// network information, they should not be insecurely distributed or displayed
+	// to users.
+	EmitDiagnosticNotices bool
 }
 
 // LoadConfig parses and validates a JSON format Psiphon config JSON
@@ -353,5 +359,9 @@ func LoadConfig(configJson []byte) (*Config, error) {
 		return nil, ContextError(errors.New("DnsServerGetter interface must be set at runtime"))
 	}
 
+	if config.EmitDiagnosticNotices {
+		setEmitDiagnosticNotices(true)
+	}
+
 	return &config, nil
 }

+ 0 - 1
psiphon/controller.go

@@ -138,7 +138,6 @@ func NewController(config *Config) (controller *Controller, err error) {
 // - a local SOCKS proxy that port forwards through the pool of tunnels
 // - a local HTTP proxy that port forwards through the pool of tunnels
 func (controller *Controller) Run(shutdownBroadcast <-chan struct{}) {
-	NoticeBuildInfo()
 	ReportAvailableRegions()
 
 	// Start components

+ 8 - 0
psiphon/controller_test.go

@@ -46,6 +46,14 @@ func TestControllerRunFrontedMeek(t *testing.T) {
 	controllerRun(t, TUNNEL_PROTOCOL_FRONTED_MEEK)
 }
 
+func TestControllerRunFrontedMeekHTTP(t *testing.T) {
+	controllerRun(t, TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP)
+}
+
+func TestControllerRunUnfrontedMeekHTTPS(t *testing.T) {
+	controllerRun(t, TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS)
+}
+
 func controllerRun(t *testing.T, protocol string) {
 
 	configFileContents, err := ioutil.ReadFile("controller_test.config")

+ 93 - 64
psiphon/meekConn.go

@@ -72,6 +72,7 @@ const (
 // through a CDN.
 type MeekConn struct {
 	frontingAddress      string
+	useHTTPS             bool
 	url                  *url.URL
 	cookie               *http.Cookie
 	pendingConns         *Conns
@@ -103,10 +104,10 @@ type transporter interface {
 // is spawned which will eventually start HTTP polling.
 // When frontingAddress is not "", fronting is used. This option assumes caller has
 // already checked server entry capabilities.
-// Fronting always uses HTTPS. Otherwise, HTTPS is optional.
 func DialMeek(
 	serverEntry *ServerEntry, sessionId string,
-	useHttps bool, frontingAddress string,
+	useHTTPS, useSNI bool,
+	frontingAddress, frontingHost string,
 	config *DialConfig) (meek *MeekConn, err error) {
 
 	// Configure transport
@@ -121,78 +122,105 @@ func DialMeek(
 	*meekConfig = *config
 	meekConfig.PendingConns = pendingConns
 
-	// host is both what is dialed and what ends up in the HTTP Host header
-	host := fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.MeekServerPort)
+	var host string
 	var dialer Dialer
 	var proxyUrl func(*http.Request) (*url.URL, error)
 
-	if useHttps || frontingAddress != "" {
-		// Custom TLS dialer:
-		//
-		//  1. ignores the HTTP request address and uses the fronting domain
-		//  2. disables SNI -- SNI breaks fronting when used with CDNs that support SNI on the server side.
-		//  3. skips verifying the server cert.
-		//
-		// Reasoning for #3:
-		//
-		// With a TLS MiM attack in place, and server certs verified, we'll fail to connect because the client
-		// will refuse to connect. That's not a successful outcome.
-		//
-		// With a MiM attack in place, and server certs not verified, we'll fail to connect if the MiM is actively
-		// targeting Psiphon and classifying the HTTP traffic by Host header or payload signature.
-		//
-		// However, in the case of a passive MiM that's just recording traffic or an active MiM that's targeting
-		// something other than Psiphon, the client will connect. This is a successful outcome.
-		//
-		// What is exposed to the MiM? The Host header does not contain a Psiphon server IP address, just an
-		// unrelated, randomly generated domain name which cannot be used to block direct connections. The
-		// Psiphon server IP is sent over meek, but it's in the encrypted cookie.
-		//
-		// The payload (user traffic) gets its confidentiality and integrity from the underlying SSH protocol.
-		// So, nothing is leaked to the MiM apart from signatures which could be used to classify the traffic
-		// as Psiphon to possibly block it; but note that not revealing that the client is Psiphon is outside
-		// our threat model; we merely seek to evade mass blocking by taking steps that require progressively
-		// more effort to block.
-		//
-		// There is a subtle attack remaining: an adversary that can MiM some CDNs but not others (and so can
-		// classify Psiphon traffic on some CDNs but not others) may throttle non-MiM CDNs so that our server
-		// selection always chooses tunnels to the MiM CDN (without any server cert verification, we won't
-		// exclusively connect to non-MiM CDNs); then the adversary kills the underlying TCP connection after
-		// some short period. This is mitigated by the "impaired" protocol classification mechanism.
-
-		customTLSConfig := &CustomTLSConfig{
-			Dial:                          NewTCPDialer(meekConfig),
-			Timeout:                       meekConfig.ConnectTimeout,
-			SendServerName:                false,
-			SkipVerify:                    true,
-			UseIndistinguishableTLS:       config.UseIndistinguishableTLS,
-			TrustedCACertificatesFilename: config.TrustedCACertificatesFilename,
-		}
+	if frontingAddress != "" {
+
+		// In this case, host is not what is dialed but is what ends up in the HTTP Host header
+		host = frontingHost
+
+		if useHTTPS {
+
+			// Custom TLS dialer:
+			//
+			//  1. ignores the HTTP request address and uses the fronting domain
+			//  2. optionally disables SNI -- SNI breaks fronting when used with certain CDNs.
+			//  3. skips verifying the server cert.
+			//
+			// Reasoning for #3:
+			//
+			// With a TLS MiM attack in place, and server certs verified, we'll fail to connect because the client
+			// will refuse to connect. That's not a successful outcome.
+			//
+			// With a MiM attack in place, and server certs not verified, we'll fail to connect if the MiM is actively
+			// targeting Psiphon and classifying the HTTP traffic by Host header or payload signature.
+			//
+			// However, in the case of a passive MiM that's just recording traffic or an active MiM that's targeting
+			// something other than Psiphon, the client will connect. This is a successful outcome.
+			//
+			// What is exposed to the MiM? The Host header does not contain a Psiphon server IP address, just an
+			// unrelated, randomly generated domain name which cannot be used to block direct connections. The
+			// Psiphon server IP is sent over meek, but it's in the encrypted cookie.
+			//
+			// The payload (user traffic) gets its confidentiality and integrity from the underlying SSH protocol.
+			// So, nothing is leaked to the MiM apart from signatures which could be used to classify the traffic
+			// as Psiphon to possibly block it; but note that not revealing that the client is Psiphon is outside
+			// our threat model; we merely seek to evade mass blocking by taking steps that require progressively
+			// more effort to block.
+			//
+			// There is a subtle attack remaining: an adversary that can MiM some CDNs but not others (and so can
+			// classify Psiphon traffic on some CDNs but not others) may throttle non-MiM CDNs so that our server
+			// selection always chooses tunnels to the MiM CDN (without any server cert verification, we won't
+			// exclusively connect to non-MiM CDNs); then the adversary kills the underlying TCP connection after
+			// some short period. This is mitigated by the "impaired" protocol classification mechanism.
+
+			customTLSConfig := &CustomTLSConfig{
+				FrontingAddr:                  fmt.Sprintf("%s:%d", frontingAddress, 443),
+				Dial:                          NewTCPDialer(meekConfig),
+				Timeout:                       meekConfig.ConnectTimeout,
+				SendServerName:                useSNI,
+				SkipVerify:                    true,
+				UseIndistinguishableTLS:       config.UseIndistinguishableTLS,
+				TrustedCACertificatesFilename: config.TrustedCACertificatesFilename,
+			}
+
+			dialer = NewCustomTLSDialer(customTLSConfig)
 
-		if frontingAddress != "" {
-			// In this case, host is not what is dialed but is what ends up in the HTTP Host header
-			host = serverEntry.MeekFrontingHost
-			customTLSConfig.FrontingAddr = fmt.Sprintf("%s:%d", frontingAddress, 443)
+		} else { // !useHTTPS
+
+			dialer = func(string, string) (net.Conn, error) {
+				return NewTCPDialer(meekConfig)("tcp", frontingAddress+":80")
+			}
 		}
 
-		dialer = NewCustomTLSDialer(customTLSConfig)
+	} else { // frontingAddress == ""
 
-	} else {
+		// host is both what is dialed and what ends up in the HTTP Host header
+		host = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.MeekServerPort)
 
-		if strings.HasPrefix(meekConfig.UpstreamProxyUrl, "http://") {
-			// For unfronted meek, we let the http.Transport handle proxying, as the
-			// target server hostname has to be in the HTTP request line. Also, in this
-			// case, we don't require the proxy to support CONNECT and so we can work
-			// through HTTP proxies that don't support it.
-			url, err := url.Parse(meekConfig.UpstreamProxyUrl)
-			if err != nil {
-				return nil, ContextError(err)
+		if useHTTPS {
+
+			customTLSConfig := &CustomTLSConfig{
+				Dial:                          NewTCPDialer(meekConfig),
+				Timeout:                       meekConfig.ConnectTimeout,
+				SendServerName:                useSNI,
+				SkipVerify:                    true,
+				UseIndistinguishableTLS:       config.UseIndistinguishableTLS,
+				TrustedCACertificatesFilename: config.TrustedCACertificatesFilename,
 			}
-			proxyUrl = http.ProxyURL(url)
-			meekConfig.UpstreamProxyUrl = ""
+
+			dialer = NewCustomTLSDialer(customTLSConfig)
+
+		} else { // !useHTTPS
+
+			if strings.HasPrefix(meekConfig.UpstreamProxyUrl, "http://") {
+				// For unfronted meek, we let the http.Transport handle proxying, as the
+				// target server hostname has to be in the HTTP request line. Also, in this
+				// case, we don't require the proxy to support CONNECT and so we can work
+				// through HTTP proxies that don't support it.
+				url, err := url.Parse(meekConfig.UpstreamProxyUrl)
+				if err != nil {
+					return nil, ContextError(err)
+				}
+				proxyUrl = http.ProxyURL(url)
+				meekConfig.UpstreamProxyUrl = ""
+			}
+
+			dialer = NewTCPDialer(meekConfig)
 		}
 
-		dialer = NewTCPDialer(meekConfig)
 	}
 
 	// Scheme is always "http". Otherwise http.Transport will try to do another TLS
@@ -240,6 +268,7 @@ func DialMeek(
 	// sendBuffer.
 	meek = &MeekConn{
 		frontingAddress:      frontingAddress,
+		useHTTPS:             useHTTPS,
 		url:                  url,
 		cookie:               cookie,
 		pendingConns:         pendingConns,
@@ -495,7 +524,7 @@ func (meek *MeekConn) roundTrip(sendPayload []byte) (receivedPayload io.ReadClos
 		return nil, ContextError(err)
 	}
 
-	if meek.frontingAddress != "" && nil == net.ParseIP(meek.frontingAddress) {
+	if meek.useHTTPS {
 		request.Header.Set("X-Psiphon-Fronting-Address", meek.frontingAddress)
 	}
 

+ 74 - 34
psiphon/notice.go

@@ -29,11 +29,25 @@ import (
 	"sort"
 	"strings"
 	"sync"
+	"sync/atomic"
 	"time"
 )
 
 var noticeLoggerMutex sync.Mutex
 var noticeLogger = log.New(os.Stderr, "", 0)
+var noticeLogDiagnostics = int32(0)
+
+func setEmitDiagnosticNotices(enable bool) {
+	if enable {
+		atomic.StoreInt32(&noticeLogDiagnostics, 1)
+	} else {
+		atomic.StoreInt32(&noticeLogDiagnostics, 0)
+	}
+}
+
+func getEmitDiagnoticNotices() bool {
+	return atomic.LoadInt32(&noticeLogDiagnostics) == 1
+}
 
 // SetNoticeOutput sets a target writer to receive notices. By default,
 // notices are written to stderr.
@@ -60,7 +74,12 @@ func SetNoticeOutput(output io.Writer) {
 }
 
 // outputNotice encodes a notice in JSON and writes it to the output writer.
-func outputNotice(noticeType string, showUser bool, args ...interface{}) {
+func outputNotice(noticeType string, isDiagnostic, showUser bool, args ...interface{}) {
+
+	if isDiagnostic && !getEmitDiagnoticNotices() {
+		return
+	}
+
 	obj := make(map[string]interface{})
 	noticeData := make(map[string]interface{})
 	obj["noticeType"] = noticeType
@@ -88,22 +107,22 @@ func outputNotice(noticeType string, showUser bool, args ...interface{}) {
 
 // NoticeInfo is an informational message
 func NoticeInfo(format string, args ...interface{}) {
-	outputNotice("Info", false, "message", fmt.Sprintf(format, args...))
+	outputNotice("Info", true, false, "message", fmt.Sprintf(format, args...))
 }
 
 // NoticeAlert is an alert message; typically a recoverable error condition
 func NoticeAlert(format string, args ...interface{}) {
-	outputNotice("Alert", false, "message", fmt.Sprintf(format, args...))
+	outputNotice("Alert", true, false, "message", fmt.Sprintf(format, args...))
 }
 
 // NoticeError is an error message; typically an unrecoverable error condition
 func NoticeError(format string, args ...interface{}) {
-	outputNotice("Error", true, "message", fmt.Sprintf(format, args...))
+	outputNotice("Error", true, false, "message", fmt.Sprintf(format, args...))
 }
 
 // NoticeCandidateServers is how many possible servers are available for the selected region and protocol
 func NoticeCandidateServers(region, protocol string, count int) {
-	outputNotice("CandidateServers", false, "region", region, "protocol", protocol, "count", count)
+	outputNotice("CandidateServers", false, false, "region", region, "protocol", protocol, "count", count)
 }
 
 // NoticeAvailableEgressRegions is what regions are available for egress from.
@@ -114,63 +133,63 @@ func NoticeAvailableEgressRegions(regions []string) {
 	repetitionMessage := strings.Join(sortedRegions, "")
 	outputRepetitiveNotice(
 		"AvailableEgressRegions", repetitionMessage, 0,
-		"AvailableEgressRegions", false, "regions", sortedRegions)
+		"AvailableEgressRegions", false, false, "regions", sortedRegions)
 }
 
 // NoticeConnectingServer is details on a connection attempt
 func NoticeConnectingServer(ipAddress, region, protocol, frontingAddress string) {
-	outputNotice("ConnectingServer", false, "ipAddress", ipAddress, "region",
+	outputNotice("ConnectingServer", true, false, "ipAddress", ipAddress, "region",
 		region, "protocol", protocol, "frontingAddress", frontingAddress)
 }
 
 // NoticeActiveTunnel is a successful connection that is used as an active tunnel for port forwarding
 func NoticeActiveTunnel(ipAddress, protocol string) {
-	outputNotice("ActiveTunnel", false, "ipAddress", ipAddress, "protocol", protocol)
+	outputNotice("ActiveTunnel", true, false, "ipAddress", ipAddress, "protocol", protocol)
 }
 
 // NoticeSocksProxyPortInUse is a failure to use the configured LocalSocksProxyPort
 func NoticeSocksProxyPortInUse(port int) {
-	outputNotice("SocksProxyPortInUse", true, "port", port)
+	outputNotice("SocksProxyPortInUse", false, true, "port", port)
 }
 
 // NoticeListeningSocksProxyPort is the selected port for the listening local SOCKS proxy
 func NoticeListeningSocksProxyPort(port int) {
-	outputNotice("ListeningSocksProxyPort", false, "port", port)
+	outputNotice("ListeningSocksProxyPort", false, false, "port", port)
 }
 
 // NoticeSocksProxyPortInUse is a failure to use the configured LocalHttpProxyPort
 func NoticeHttpProxyPortInUse(port int) {
-	outputNotice("HttpProxyPortInUse", true, "port", port)
+	outputNotice("HttpProxyPortInUse", false, true, "port", port)
 }
 
 // NoticeListeningSocksProxyPort is the selected port for the listening local HTTP proxy
 func NoticeListeningHttpProxyPort(port int) {
-	outputNotice("ListeningHttpProxyPort", false, "port", port)
+	outputNotice("ListeningHttpProxyPort", false, false, "port", port)
 }
 
 // NoticeClientUpgradeAvailable is an available client upgrade, as per the handshake. The
 // client should download and install an upgrade.
 func NoticeClientUpgradeAvailable(version string) {
-	outputNotice("ClientUpgradeAvailable", false, "version", version)
+	outputNotice("ClientUpgradeAvailable", false, false, "version", version)
 }
 
 // NoticeClientUpgradeAvailable is a sponsor homepage, as per the handshake. The client
 // should display the sponsor's homepage.
 func NoticeHomepage(url string) {
-	outputNotice("Homepage", false, "url", url)
+	outputNotice("Homepage", false, false, "url", url)
 }
 
 // NoticeClientRegion is the client's region, as determined by the server and
 // reported to the client in the handshake.
 func NoticeClientRegion(region string) {
-	outputNotice("ClientRegion", false, "region", region)
+	outputNotice("ClientRegion", true, false, "region", region)
 }
 
 // NoticeTunnels is how many active tunnels are available. The client should use this to
 // determine connecting/unexpected disconnect state transitions. When count is 0, the core is
 // disconnected; when count > 1, the core is connected.
 func NoticeTunnels(count int) {
-	outputNotice("Tunnels", false, "count", count)
+	outputNotice("Tunnels", false, false, "count", count)
 }
 
 // NoticeUntunneled indicates than an address has been classified as untunneled and is being
@@ -180,47 +199,48 @@ func NoticeTunnels(count int) {
 // users, not for diagnostics logs.
 //
 func NoticeUntunneled(address string) {
-	outputNotice("Untunneled", true, "address", address)
+	outputNotice("Untunneled", false, true, "address", address)
 }
 
 // NoticeSplitTunnelRegion reports that split tunnel is on for the given region.
 func NoticeSplitTunnelRegion(region string) {
-	outputNotice("SplitTunnelRegion", true, "region", region)
+	outputNotice("SplitTunnelRegion", false, true, "region", region)
 }
 
 // NoticeUpstreamProxyError reports an error when connecting to an upstream proxy. The
 // user may have input, for example, an incorrect address or incorrect credentials.
 func NoticeUpstreamProxyError(err error) {
-	outputNotice("UpstreamProxyError", true, "message", err.Error())
+	outputNotice("UpstreamProxyError", false, true, "message", err.Error())
 }
 
 // NoticeClientUpgradeDownloaded indicates that a client upgrade download
 // is complete and available at the destination specified.
 func NoticeClientUpgradeDownloaded(filename string) {
-	outputNotice("ClientUpgradeDownloaded", false, "filename", filename)
+	outputNotice("ClientUpgradeDownloaded", false, false, "filename", filename)
 }
 
 // NoticeBytesTransferred reports how many tunneled bytes have been
 // transferred since the last NoticeBytesTransferred, for the tunnel
 // to the server at ipAddress.
 func NoticeBytesTransferred(ipAddress string, sent, received int64) {
-	outputNotice("BytesTransferred", false, "ipAddress", ipAddress, "sent", sent, "received", received)
+	if getEmitDiagnoticNotices() {
+		outputNotice("BytesTransferred", true, false, "ipAddress", ipAddress, "sent", sent, "received", received)
+	} else {
+		// This case keeps the EmitBytesTransferred and EmitDiagnosticNotices config options independent
+		outputNotice("BytesTransferred", false, false, "sent", sent, "received", received)
+	}
 }
 
 // NoticeTotalBytesTransferred reports how many tunneled bytes have been
 // transferred in total up to this point, for the tunnel to the server
 // at ipAddress.
 func NoticeTotalBytesTransferred(ipAddress string, sent, received int64) {
-	outputNotice("TotalBytesTransferred", false, "ipAddress", ipAddress, "sent", sent, "received", received)
-}
-
-// NoticeFrontedMeekStats reports extra network details for a
-// FRONTED-MEEK-OSSH tunnel connection.
-func NoticeFrontedMeekStats(ipAddress string, frontedMeekStats *FrontedMeekStats) {
-	outputNotice("NoticeFrontedMeekStats", false, "ipAddress", ipAddress,
-		"frontingAddress", frontedMeekStats.frontingAddress,
-		"resolvedIPAddress", frontedMeekStats.resolvedIPAddress,
-		"enabledSNI", frontedMeekStats.enabledSNI)
+	if getEmitDiagnoticNotices() {
+		outputNotice("TotalBytesTransferred", true, false, "ipAddress", ipAddress, "sent", sent, "received", received)
+	} else {
+		// This case keeps the EmitBytesTransferred and EmitDiagnosticNotices config options independent
+		outputNotice("TotalBytesTransferred", false, false, "sent", sent, "received", received)
+	}
 }
 
 // NoticeLocalProxyError reports a local proxy error message. Repetitive
@@ -240,7 +260,27 @@ func NoticeLocalProxyError(proxyType string, err error) {
 
 	outputRepetitiveNotice(
 		"LocalProxyError"+proxyType, repetitionMessage, 1,
-		"LocalProxyError", false, "message", err.Error())
+		"LocalProxyError", true, false, "message", err.Error())
+}
+
+// NoticeFrontedMeekStats reports extra network details for a
+// FRONTED-MEEK-OSSH or FRONTED-MEEK-HTTP-OSSH tunnel connection.
+func NoticeFrontedMeekStats(ipAddress string, frontedMeekStats *FrontedMeekStats) {
+	outputNotice("NoticeFrontedMeekStats", true, false, "ipAddress", ipAddress,
+		"frontingAddress", frontedMeekStats.frontingAddress,
+		"resolvedIPAddress", frontedMeekStats.resolvedIPAddress,
+		"enabledSNI", frontedMeekStats.enabledSNI,
+		"frontingHost", frontedMeekStats.frontingHost)
+}
+
+// NoticeBuildInfo reports build version info.
+func NoticeBuildInfo(buildDate, buildRepo, buildRev, goVersion, gomobileVersion string) {
+	outputNotice("NoticeBuildInfo", false, false,
+		"buildDate", buildDate,
+		"buildRepo", buildRepo,
+		"buildRev", buildRev,
+		"goVersion", goVersion,
+		"gomobileVersion", gomobileVersion)
 }
 
 type repetitiveNoticeState struct {
@@ -257,7 +297,7 @@ var repetitiveNoticeStates = make(map[string]*repetitiveNoticeState)
 // until the repetitionMessage differs.
 func outputRepetitiveNotice(
 	repetitionKey, repetitionMessage string, repeatLimit int,
-	noticeType string, showUser bool, args ...interface{}) {
+	noticeType string, isDiagnostic, showUser bool, args ...interface{}) {
 
 	repetitiveNoticeMutex.Lock()
 	defer repetitiveNoticeMutex.Unlock()
@@ -283,7 +323,7 @@ func outputRepetitiveNotice(
 		if state.repeats > 0 {
 			args = append(args, "repeats", state.repeats)
 		}
-		outputNotice(noticeType, showUser, args...)
+		outputNotice(noticeType, isDiagnostic, showUser, args...)
 	}
 }
 

+ 8 - 3
psiphon/opensslConn.go

@@ -97,9 +97,14 @@ func newOpenSSLConn(rawConn net.Conn, hostname string, config *CustomTLSConfig)
 	}
 
 	if config.SendServerName {
-		err = conn.SetTlsExtHostName(hostname)
-		if err != nil {
-			return nil, ContextError(err)
+		// Explicitly exclude IPs:
+		// - "Literal IPv4 and IPv6 addresses are not permitted": https://tools.ietf.org/html/rfc6066#page-6.
+		// - OpenSSL does not appear to enforce this rule itself.
+		if net.ParseIP(hostname) == nil {
+			err = conn.SetTlsExtHostName(hostname)
+			if err != nil {
+				return nil, ContextError(err)
+			}
 		}
 	}
 

+ 4 - 2
psiphon/serverApi.go

@@ -52,12 +52,12 @@ type ServerContext struct {
 }
 
 // FrontedMeekStats holds extra stats that are only gathered for
-// FRONTED-MEEK-OSSH. Specifically, which fronting address was selected, what
-// IP address a fronting domain resolved to, and whether SNI was enabled.
+// FRONTED-MEEK-OSSH, FRONTED-MEEK-HTTP-OSSH.
 type FrontedMeekStats struct {
 	frontingAddress   string
 	resolvedIPAddress string
 	enabledSNI        bool
+	frontingHost      string
 }
 
 // nextTunnelNumber is a monotonically increasing number assigned to each
@@ -635,6 +635,8 @@ func makeBaseRequestUrl(tunnel *Tunnel, port, sessionId string) string {
 		} else {
 			requestUrl.WriteString("0")
 		}
+		requestUrl.WriteString("&fronting_host=")
+		requestUrl.WriteString(tunnel.frontedMeekStats.frontingHost)
 	}
 
 	if tunnel.serverEntry.Region != "" {

+ 4 - 0
psiphon/serverEntry.go

@@ -35,10 +35,12 @@ const (
 	TUNNEL_PROTOCOL_UNFRONTED_MEEK       = "UNFRONTED-MEEK-OSSH"
 	TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS = "UNFRONTED-MEEK-HTTPS-OSSH"
 	TUNNEL_PROTOCOL_FRONTED_MEEK         = "FRONTED-MEEK-OSSH"
+	TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP    = "FRONTED-MEEK-HTTP-OSSH"
 )
 
 var SupportedTunnelProtocols = []string{
 	TUNNEL_PROTOCOL_FRONTED_MEEK,
+	TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP,
 	TUNNEL_PROTOCOL_UNFRONTED_MEEK,
 	TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS,
 	TUNNEL_PROTOCOL_OBFUSCATED_SSH,
@@ -66,9 +68,11 @@ type ServerEntry struct {
 	MeekCookieEncryptionPublicKey string   `json:"meekCookieEncryptionPublicKey"`
 	MeekObfuscatedKey             string   `json:"meekObfuscatedKey"`
 	MeekFrontingHost              string   `json:"meekFrontingHost"`
+	MeekFrontingHosts             []string `json:"meekFrontingHosts"`
 	MeekFrontingDomain            string   `json:"meekFrontingDomain"`
 	MeekFrontingAddresses         []string `json:"meekFrontingAddresses"`
 	MeekFrontingAddressesRegex    string   `json:"meekFrontingAddressesRegex"`
+	MeekFrontingDisableSNI        bool     `json:"meekFrontingDisableSNI"`
 
 	// These local fields are not expected to be present in downloaded server
 	// entries. They are added by the client to record and report stats about

+ 1 - 3
psiphon/tlsDialer.go

@@ -175,9 +175,7 @@ func CustomTLSDial(network, addr string, config *CustomTLSConfig) (net.Conn, err
 	if config.SendServerName && config.VerifyLegacyCertificate == nil {
 		// Set the ServerName and rely on the usual logic in
 		// tls.Conn.Handshake() to do its verification.
-		// Explicitly exclude IPs:
-		// - "Literal IPv4 and IPv6 addresses are not permitted": https://tools.ietf.org/html/rfc6066#page-6.
-		// - OpenSSL does not appear to enforce this rule itself.
+		// Note: Go TLS will automatically omit this ServerName when it's an IP address
 		if net.ParseIP(hostname) == nil {
 			tlsConfig.ServerName = hostname
 		}

+ 33 - 6
psiphon/tunnel.go

@@ -348,22 +348,33 @@ func dialSsh(
 	// So depending on which protocol is used, multiple layers are initialized.
 	port := 0
 	useMeek := false
-	useMeekHttps := false
+	useMeekHTTPS := false
+	useMeekSNI := false
 	useFronting := false
 	useObfuscatedSsh := false
 	switch selectedProtocol {
 	case TUNNEL_PROTOCOL_FRONTED_MEEK:
 		useMeek = true
-		useMeekHttps = true
+		useMeekHTTPS = true
+		useMeekSNI = !serverEntry.MeekFrontingDisableSNI
+		useFronting = true
+		useObfuscatedSsh = true
+	case TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP:
+		useMeek = true
+		useMeekHTTPS = false
+		useMeekSNI = false
 		useFronting = true
 		useObfuscatedSsh = true
 	case TUNNEL_PROTOCOL_UNFRONTED_MEEK:
 		useMeek = true
+		useMeekHTTPS = false
+		useMeekSNI = false
 		useObfuscatedSsh = true
 		port = serverEntry.SshObfuscatedPort
 	case TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS:
 		useMeek = true
-		useMeekHttps = true
+		useMeekHTTPS = true
+		useMeekSNI = false
 		useObfuscatedSsh = true
 		port = serverEntry.SshObfuscatedPort
 	case TUNNEL_PROTOCOL_OBFUSCATED_SSH:
@@ -374,6 +385,7 @@ func dialSsh(
 	}
 
 	frontingAddress := ""
+	frontingHost := ""
 	if useFronting {
 		if len(serverEntry.MeekFrontingAddressesRegex) > 0 {
 
@@ -397,6 +409,17 @@ func dialSsh(
 			}
 			frontingAddress = serverEntry.MeekFrontingAddresses[index]
 		}
+
+		if len(serverEntry.MeekFrontingHosts) > 0 {
+			index, err := MakeSecureRandomInt(len(serverEntry.MeekFrontingHosts))
+			if err != nil {
+				return nil, nil, nil, ContextError(err)
+			}
+			frontingHost = serverEntry.MeekFrontingHosts[index]
+		} else {
+			// Backwards compatibility case
+			frontingHost = serverEntry.MeekFrontingHost
+		}
 	}
 
 	NoticeConnectingServer(
@@ -430,7 +453,8 @@ func dialSsh(
 		ResolvedIPCallback:            setResolvedIPAddress,
 	}
 	if useMeek {
-		conn, err = DialMeek(serverEntry, sessionId, useMeekHttps, frontingAddress, dialConfig)
+		conn, err = DialMeek(
+			serverEntry, sessionId, useMeekHTTPS, useMeekSNI, frontingAddress, frontingHost, dialConfig)
 		if err != nil {
 			return nil, nil, nil, ContextError(err)
 		}
@@ -525,11 +549,14 @@ func dialSsh(
 		return nil, nil, nil, ContextError(result.err)
 	}
 
-	if selectedProtocol == TUNNEL_PROTOCOL_FRONTED_MEEK {
+	if selectedProtocol == TUNNEL_PROTOCOL_FRONTED_MEEK ||
+		selectedProtocol == TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP {
+
 		frontedMeekStats = &FrontedMeekStats{
 			frontingAddress:   frontingAddress,
 			resolvedIPAddress: resolvedIPAddress.Load().(string),
-			enabledSNI:        false,
+			enabledSNI:        useMeekSNI,
+			frontingHost:      frontingHost,
 		}
 
 		NoticeFrontedMeekStats(serverEntry.IpAddress, frontedMeekStats)