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

Merge branch 'master' into session-ticket-protocol

Rod Hynes 9 лет назад
Родитель
Сommit
81d0c0f3f1

+ 35 - 30
ConsoleClient/README.md

@@ -1,42 +1,47 @@
-##Psiphon Console Client README
+## Psiphon Console Client README
 
-###Building with Docker
+### Building with Docker
 
 Note that you may need to use `sudo docker` below, depending on your OS.
 
-#####Create the build image:
-  1. Run the command: `docker build --no-cache=true -t psiclient .` (this may take some time to complete)
-  2. Once completed, verify that you see an image named `psiclient` when running: `docker images`
-
-#####Run the build:
-  *Ensure that the command below is run from within the `ConsoleClient` directory*
-
-  ```bash
-  cd .. && \
-    docker run \
-    --rm \
-    -v $PWD:/go/src/github.com/Psiphon-Labs/psiphon-tunnel-core \
-    psiclient \
-    /bin/bash -c './make.bash all' \
-  ; cd -
-  ```
+##### Create the build image:
+
+1. While in the `ConsoleClient` directory, run the command: `docker build --no-cache=true -t psiclient .`
+
+2. Once completed, verify that you see an image named `psiclient` when running: `docker images`
+
+##### Run the build:
+
+*Ensure that the command below is run from within the `ConsoleClient` directory*
+
+```bash
+cd .. && \
+  docker run \
+  --rm \
+  -v $PWD:/go/src/github.com/Psiphon-Labs/psiphon-tunnel-core \
+  psiclient \
+  /bin/bash -c './make.bash all' \
+; cd -
+```
+
 This command can also be modified by:
  - replacing `all` with `windows`, `linux`, or `osx` as the first parameter to `make.bash` (as in `...&& ./make.bash windows`) to only build binaries for the operating system of choice
    - if `windows` or `linux` is specified as the first parameter, the second parameter can be passed as either `32` or `64` (as in `...&& ./make.bash windows 32`)to limit the builds to just one or the other (no second parameter means both will build)
 
 When that command completes, the compiled binaries will be located in the `bin` directory (`./bin`, and everything under it will likely be owned by root, so be sure to `chown` to an appropriate user) under the current directory. The structure will be:
-  ```
-  bin
-  ├── darwin
-  │   └── psiphon-tunnel-core-x86_64
-  ├── linux
-  │   └── psiphon-tunnel-core-i686
-  │   └── psiphon-tunnel-core-x86_64
-  └── windows
-      └── psiphon-tunnel-core-i686.exe
-      └── psiphon-tunnel-core-x86_64.exe
-
-  ```
+
+```
+bin
+├── darwin
+│   └── psiphon-tunnel-core-x86_64
+├── linux
+│   └── psiphon-tunnel-core-i686
+│   └── psiphon-tunnel-core-x86_64
+└── windows
+    └── psiphon-tunnel-core-i686.exe
+    └── psiphon-tunnel-core-x86_64.exe
+
+```
 
 ### Building without Docker
 

+ 15 - 9
ConsoleClient/make.bash

@@ -1,6 +1,6 @@
 #!/usr/bin/env bash
 
-set -e
+set -e -x
 
 if [ ! -f make.bash ]; then
   echo "make.bash must be run from $GOPATH/src/github.com/Psiphon-Labs/psiphon-tunnel-core/ConsoleClient"
@@ -9,6 +9,12 @@ fi
 
 EXE_BASENAME="psiphon-tunnel-core"
 
+# The "OPENSSL" tag enables support of OpenSSL for use by IndistinguishableTLS.
+# This needs to be outside of prepare_build because it's used by go-get.
+WINDOWS_BUILD_TAGS="OPENSSL"
+LINUX_BUILD_TAGS=
+OSX_BUILD_TAGS=
+
 prepare_build () {
   BUILDINFOFILE="${EXE_BASENAME}_buildinfo.txt"
   BUILDDATE=$(date --iso-8601=seconds)
@@ -49,7 +55,7 @@ fi
 
 build_for_windows () {
   echo "...Getting project dependencies (via go get) for Windows. Parameter is: '$1'"
-  GOOS=windows go get -d -v ./...
+  GOOS=windows go get -d -v -tags "$WINDOWS_BUILD_TAGS" ./...
   prepare_build
   if [ $? != 0 ]; then
     echo "....'go get' failed, exiting"
@@ -66,7 +72,7 @@ build_for_windows () {
     CGO_CFLAGS="-I $PKG_CONFIG_PATH/include/" \
     CGO_LDFLAGS="-L $PKG_CONFIG_PATH -L /usr/i686-w64-mingw32/lib/ -lssl -lcrypto -lwsock32 -lcrypt32 -lgdi32" \
     CC=/usr/bin/i686-w64-mingw32-gcc \
-    gox -verbose -ldflags "$LDFLAGS" -osarch windows/386 -output bin/windows/${EXE_BASENAME}-i686
+    gox -verbose -ldflags "$LDFLAGS" -osarch windows/386 -tags "$WINDOWS_BUILD_TAGS" -output bin/windows/${EXE_BASENAME}-i686
     RETVAL=$?
     echo ".....gox completed, exit code: $?"
     if [ $RETVAL != 0 ]; then
@@ -89,7 +95,7 @@ build_for_windows () {
     CGO_CFLAGS="-I $PKG_CONFIG_PATH/include/" \
     CGO_LDFLAGS="-L $PKG_CONFIG_PATH -L /usr/x86_64-w64-mingw32/lib/ -lssl -lcrypto -lwsock32 -lcrypt32 -lgdi32" \
     CC=/usr/bin/x86_64-w64-mingw32-gcc \
-    gox -verbose -ldflags "$LDFLAGS" -osarch windows/amd64 -output bin/windows/${EXE_BASENAME}-x86_64
+    gox -verbose -ldflags "$LDFLAGS" -osarch windows/amd64 -tags "$WINDOWS_BUILD_TAGS" -output bin/windows/${EXE_BASENAME}-x86_64
     RETVAL=$?
     if [ $RETVAL != 0 ]; then
       echo ".....gox failed, exiting"
@@ -104,7 +110,7 @@ build_for_windows () {
 
 build_for_linux () {
   echo "Getting project dependencies (via go get) for Linux. Parameter is: '$1'"
-  GOOS=linux go get -d -v ./...
+  GOOS=linux go get -d -v -tags "$LINUX_BUILD_TAGS" ./...
   prepare_build
   if [ $? != 0 ]; then
     echo "...'go get' failed, exiting"
@@ -113,7 +119,7 @@ build_for_linux () {
 
   if [ -z $1 ] || [ "$1" == "32" ]; then
     echo "...Building linux-i686"
-    CFLAGS=-m32 gox -verbose -ldflags "$LDFLAGS" -osarch linux/386 -output bin/linux/${EXE_BASENAME}-i686
+    CFLAGS=-m32 gox -verbose -ldflags "$LDFLAGS" -osarch linux/386 -tags "$LINUX_BUILD_TAGS" -output bin/linux/${EXE_BASENAME}-i686
     RETVAL=$?
     if [ $RETVAL != 0 ]; then
       echo ".....gox failed, exiting"
@@ -133,7 +139,7 @@ build_for_linux () {
 
   if [ -z $1 ] || [ "$1" == "64" ]; then
     echo "...Building linux-x86_64"
-    gox -verbose -ldflags "$LDFLAGS" -osarch linux/amd64 -output bin/linux/${EXE_BASENAME}-x86_64
+    gox -verbose -ldflags "$LDFLAGS" -osarch linux/amd64 -tags "$LINUX_BUILD_TAGS" -output bin/linux/${EXE_BASENAME}-x86_64
     RETVAL=$?
     if [ $RETVAL != 0 ]; then
       echo "....gox failed, exiting"
@@ -154,7 +160,7 @@ build_for_linux () {
 
 build_for_osx () {
   echo "Getting project dependencies (via go get) for OSX"
-  GOOS=darwin go get -d -v ./...
+  GOOS=darwin go get -d -v -tags "$OSX_BUILD_TAGS" ./...
   prepare_build
   if [ $? != 0 ]; then
     echo "..'go get' failed, exiting"
@@ -163,7 +169,7 @@ build_for_osx () {
 
   echo "Building darwin-x86_64..."
   echo "..Disabling CGO for this build"
-  CGO_ENABLED=0 gox -verbose -ldflags "$LDFLAGS" -osarch darwin/amd64 -output bin/darwin/${EXE_BASENAME}-x86_64
+  CGO_ENABLED=0 gox -verbose -ldflags "$LDFLAGS" -osarch darwin/amd64 -tags "$OSX_BUILD_TAGS" -output bin/darwin/${EXE_BASENAME}-x86_64
   # Darwin binaries don't seem to be UPXable when built this way
   echo "..No UPX for this build"
 }

+ 24 - 22
MobileLibrary/Android/README.md

@@ -1,38 +1,40 @@
-##Psiphon Android Library README
+## Psiphon Android Library README
 
-###Overview
+### Overview
 
 Psiphon Library for Android enables you to easily embed Psiphon in your Android
 app. The Psiphon Library for Android is implemented in Go and follows the standard
 conventions for using a Go library in an Android app.
 
-###Building with Docker
+### Building with Docker
 
 Note that you may need to use `sudo docker` below, depending on your OS.
 
-#####Create the build image:
+##### Create the build image:
 
-  1. Run the command: `docker build --no-cache=true -t psiandroid .` (this may take some time to complete)
-  2. Once completed, verify that you see an image named `psiandroid` when running: `docker images`
+1. While in the `MobileLibrary/Android` directory, run the command: `docker build --no-cache=true -t psiandroid .`
 
-#####Run the build:
+2. Once completed, verify that you see an image named `psiandroid` when running: `docker images`
 
-  *Ensure that the command below is run from within the `AndroidLibrary` directory*
+##### Run the build:
+
+*Ensure that the command below is run from within the `MobileLibrary/Android` directory*
+
+```bash
+cd ../.. && \
+  docker run \
+  --rm \
+  -v $(pwd):/go/src/github.com/Psiphon-Labs/psiphon-tunnel-core \
+  psiandroid \
+  /bin/bash -c 'source /tmp/setenv-android.sh && cd /go/src/github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/Android && ./make.bash' \
+; cd -
+```
 
-  ```bash
-  cd ../.. && \
-    docker run \
-    --rm \
-    -v $(pwd):/go/src/github.com/Psiphon-Labs/psiphon-tunnel-core \
-    psiandroid \
-    /bin/bash -c 'source /tmp/setenv-android.sh && cd /go/src/github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/Android && ./make.bash' \
-  ; cd -
-  ```
 When that command completes, the compiled `.aar` files (suitable for use in an Android Studio project) will be located in the current directory (it will likely be owned by root, so be sure to `chown` to an appropriate user).
 
-###Building without Docker (from source)
+### Building without Docker (from source)
 
-#####Prerequisites:
+##### Prerequisites:
 
  - The `build-essential` package (on Debian based systems - or its equivalent for your platform)
  - Go 1.5 or later
@@ -42,17 +44,17 @@ When that command completes, the compiled `.aar` files (suitable for use in an A
  - OpenSSL (tested against the version [here](../../openssl))
   - Follow its [README](../../openssl/README.md) to prepare the environment before you follow the steps below
 
-#####Steps:
+##### Steps:
 
  1. Follow Go Android documentation ([gomobile documentation](https://godoc.org/golang.org/x/mobile/cmd/gomobile))
  2. Run `make.bash`
 
-###Using the Library
+### Using the Library
 
  1. Build `ca.psiphon.aar` from via the docker container, from source, or use the [binary release](https://github.com/Psiphon-Labs/psiphon-tunnel-core/releases)
  2. Add `ca.psiphon.aar` to your Android Studio project as described in the [gomobile documentation](https://godoc.org/golang.org/x/mobile/cmd/gomobile)
  3. Example usage in [TunneledWebView sample app](./SampleApps/TunneledWebView/README.md)
 
-#####Limitations
+##### Limitations
 
  - Only supports one concurrent instance of Psiphon.

+ 8 - 5
MobileLibrary/Android/make.bash

@@ -1,22 +1,25 @@
 #!/usr/bin/env bash
 
-set -e
+set -e -x
 
 if [ ! -f make.bash ]; then
   echo "make.bash must be run from $GOPATH/src/github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/Android"
   exit 1
 fi
 
+# The "OPENSSL" tag enables support of OpenSSL for use by IndistinguishableTLS.
+BUILD_TAGS="OPENSSL"
+
 # Don't use '-u' to force updates because the docker builds always pull
 # the latest versions. Outside of Docker, be aware that these dependencies
 # will not be overridden w/ new versions if they already exist in $GOPATH
 
-GOOS=arm go get -d -v github.com/Psiphon-Inc/openssl
+GOOS=arm go get -d -v -tags "${BUILD_TAGS}" github.com/Psiphon-Inc/openssl
 if [ $? != 0 ]; then
   echo "..'go get -d -v github.com/psiphon-inc/openssl' failed, exiting"
   exit $?
 fi
-GOOS=arm go get -d -v github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon
+GOOS=arm go get -d -v -tags "${BUILD_TAGS}" github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon
 if [ $? != 0 ]; then
   echo "..'go get -d -v github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon' failed, exiting"
   exit $?
@@ -55,7 +58,7 @@ echo " Gomobile version: ${GOMOBILEVERSION}"
 echo " Dependencies: ${DEPENDENCIES}"
 echo ""
 
-gomobile bind -v -target=android/arm -ldflags="$LDFLAGS" github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/psi
+gomobile bind -v -target=android/arm -tags "${BUILD_TAGS}" -ldflags="$LDFLAGS" github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/psi
 if [ $? != 0 ]; then
   echo "..'gomobile bind' failed, exiting"
   exit $?
@@ -66,7 +69,7 @@ unzip -o psi.aar -d build-tmp/psi
 yes | cp -f PsiphonTunnel/AndroidManifest.xml build-tmp/psi/AndroidManifest.xml
 yes | cp -f PsiphonTunnel/libs/libtun2socks.so build-tmp/psi/jni/armeabi-v7a/libtun2socks.so
 
-javac -d build-tmp -bootclasspath $ANDROID_HOME/platforms/android-23/android.jar -source 1.7 -target 1.7 -classpath build-tmp/psi/classes.jar:$ANDROID_HOME/platforms/android-23/optional/org.apache.http.legacy.jar PsiphonTunnel/PsiphonTunnel.java 
+javac -d build-tmp -bootclasspath $ANDROID_HOME/platforms/android-23/android.jar -source 1.7 -target 1.7 -classpath build-tmp/psi/classes.jar:$ANDROID_HOME/platforms/android-23/optional/org.apache.http.legacy.jar PsiphonTunnel/PsiphonTunnel.java
 if [ $? != 0 ]; then
   echo "..'javac' compiling PsiphonTunnel failed, exiting"
   exit $?

+ 9 - 6
MobileLibrary/iOS/build-psiphon-framework.sh

@@ -28,6 +28,9 @@ INTERMEDIATE_OUPUT_DIR="${BASE_DIR}/PsiphonTunnel/PsiphonTunnel"
 INTERMEDIATE_OUPUT_FILE="${FRAMEWORK}.framework"
 FRAMEWORK_BINARY="${INTERMEDIATE_OUPUT_DIR}/${INTERMEDIATE_OUPUT_FILE}/Versions/A/${FRAMEWORK}"
 
+# The "OPENSSL" tag enables support of OpenSSL for use by IndistinguishableTLS.
+BUILD_TAGS="OPENSSL IOS"
+
 LIBSSL=${BASE_DIR}/OpenSSL-for-iPhone/lib/libssl.a
 LIBCRYPTO=${BASE_DIR}/OpenSSL-for-iPhone/lib/libcrypto.a
 OPENSSL_INCLUDE=${BASE_DIR}/OpenSSL-for-iPhone/include/
@@ -116,16 +119,16 @@ cd OpenSSL-for-iPhone && ./build-libssl.sh; cd -
 strip_architectures "${LIBSSL}"
 strip_architectures "${LIBCRYPTO}"
 
-go get -d -u -v github.com/Psiphon-Inc/openssl
+go get -d -u -v -tags "${BUILD_TAGS}" github.com/Psiphon-Inc/openssl
 if [[ $? != 0 ]]; then
-  echo "FAILURE: go get -d -u -v github.com/Psiphon-Inc/openssl"
+  echo "FAILURE: go get -d -u -v -tags "${BUILD_TAGS}" github.com/Psiphon-Inc/openssl"
   exit 1
 fi
 
 # Don't use -u, because this path points to our local repo, and we don't want it overridden.
-go get -d -v github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/psi
+go get -d -v -tags "${BUILD_TAGS}" github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/psi
 if [[ $? != 0 ]]; then
-  echo "FAILURE: go get -d -v github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/psi"
+  echo "FAILURE: go get -d -v -tags "${BUILD_TAGS}" github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/psi"
   exit 1
 fi
 
@@ -152,7 +155,7 @@ set +e
 check_pinned_version
 rc=$?
 set -e
-if [[ rc != 0 ]]; then
+if [[ $rc != 0 ]]; then
     go get -u golang.org/x/mobile/cmd/gomobile
     cd ${GOPATH}/src/golang.org/x/mobile/cmd/gomobile
     git checkout master
@@ -200,7 +203,7 @@ IOS_CGO_BUILD_FLAGS='// #cgo darwin CFLAGS: -I'"${OPENSSL_INCLUDE}"'\
 
 LC_ALL=C sed -i -- "s|// #cgo pkg-config: libssl|${IOS_CGO_BUILD_FLAGS}|" "${OPENSSL_SRC_DIR}/build.go"
 
-${GOPATH}/bin/gomobile bind -target ios -ldflags="${LDFLAGS}" -o "${INTERMEDIATE_OUPUT_DIR}/${INTERMEDIATE_OUPUT_FILE}" github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/psi
+${GOPATH}/bin/gomobile bind -v -x -target ios -tags "${BUILD_TAGS}" -ldflags="${LDFLAGS}" -o "${INTERMEDIATE_OUPUT_DIR}/${INTERMEDIATE_OUPUT_FILE}" github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/psi
 rc=$?; if [[ $rc != 0 ]]; then
   echo "FAILURE: ${GOPATH}/bin/gomobile bind -target ios -ldflags="${LDFLAGS}" -o "${INTERMEDIATE_OUPUT_DIR}/${INTERMEDIATE_OUPUT_FILE}" github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/psi"
   exit $rc

+ 6 - 2
README.md

@@ -46,6 +46,9 @@ Client Setup
 
 ##### macOS
 
+Building without OpenSSL support (that is, without indistinguishable TLS support) requires no extra steps. 
+To compile with OpenSSL support, follow these steps:
+
 1. You must have [Homebrew](http://brew.sh/) installed.
 
 2. `brew install openssl pkg-config`
@@ -58,10 +61,11 @@ Client Setup
    
    Make note of the "build variable" path for `PKG_CONFIG_PATH`.
    
-4. Set `PKG_CONFIG_PATH=<path discovered above>` when building. This can be easily done on the build command line like so:
+4. Set `PKG_CONFIG_PATH=<path discovered above>` when building, and provide the `-tags OPENSSL` flag. 
+   This can be easily done on the build command line like so:
 
    ```
-   $ PKG_CONFIG_PATH=<path discovered above> go build
+   $ PKG_CONFIG_PATH=<path discovered above> go build -tags OPENSSL
    ```
 
 ### Configure

+ 15 - 0
psiphon/common/osl/paver/README.md

@@ -0,0 +1,15 @@
+# paver
+
+Example usage:
+
+```
+./paver -config osl_config.json -key signing_key.pem -count 3
+```
+
+* Paver is a tool that generates OSL files for paving.
+* Output is one directory for each propagation channel ID containing the files to upload to the appropriate campaign buckets.
+* Each output OSL is empty. Support for specifying and paving server entries is pending.
+* The example will pave 3 OSLs (e.g., OSLs for 3 time periods from epoch, where the time period is determined by the config) for each propagation channel ID.
+  * `osl_config.json` is the OSL config in `psinet`.
+  * `signing_key.pem` is `psinet._PsiphonNetwork__get_remote_server_list_signing_key_pair().pem_key_pair`.
+

+ 152 - 0
psiphon/common/osl/paver/main.go

@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) 2016, 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+	"crypto/x509"
+	"encoding/base64"
+	"encoding/pem"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"time"
+
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/osl"
+)
+
+func main() {
+
+	var configFilename string
+	flag.StringVar(&configFilename, "config", "", "OSL configuration file")
+
+	var scheme int
+	flag.IntVar(&scheme, "scheme", 0, "scheme to pave")
+
+	var oslOffset int
+	flag.IntVar(&oslOffset, "offset", 0, "OSL offset")
+
+	var oslCount int
+	flag.IntVar(&oslCount, "count", 1, "OSL count")
+
+	var signingKeyPairFilename string
+	flag.StringVar(&signingKeyPairFilename, "key", "", "signing public key pair")
+
+	var destinationDirectory string
+	flag.StringVar(&destinationDirectory, "output", "", "destination directory for output files")
+
+	flag.Parse()
+
+	configJSON, err := ioutil.ReadFile(configFilename)
+	if err != nil {
+		fmt.Printf("failed loading configuration file: %s\n", err)
+		os.Exit(1)
+	}
+
+	config, err := osl.LoadConfig(configJSON)
+	if err != nil {
+		fmt.Printf("failed processing configuration file: %s\n", err)
+		os.Exit(1)
+	}
+
+	if scheme < 0 || scheme >= len(config.Schemes) {
+		fmt.Printf("failed: invalid scheme\n")
+		os.Exit(1)
+	}
+
+	keyPairPEM, err := ioutil.ReadFile(signingKeyPairFilename)
+	if err != nil {
+		fmt.Printf("failed loading signing public key pair file: %s\n", err)
+		os.Exit(1)
+	}
+
+	// Password "none" from psi_ops:
+	// https://bitbucket.org/psiphon/psiphon-circumvention-system/src/ef4f3d4893bd5259ef24f0cb4525cbbbb0854cf9/Automation/psi_ops.py?at=default&fileviewer=file-view-default#psi_ops.py-297
+
+	block, _ := pem.Decode(keyPairPEM)
+	decryptedKeyPairPEM, err := x509.DecryptPEMBlock(block, []byte("none"))
+	if err != nil {
+		fmt.Printf("failed decrypting signing public key pair file: %s\n", err)
+		os.Exit(1)
+	}
+
+	rsaKey, err := x509.ParsePKCS1PrivateKey(decryptedKeyPairPEM)
+	if err != nil {
+		fmt.Printf("failed parsing signing public key pair file: %s\n", err)
+		os.Exit(1)
+	}
+
+	publicKeyBytes, err := x509.MarshalPKIXPublicKey(rsaKey.Public())
+	if err != nil {
+		fmt.Printf("failed marshaling signing public key: %s\n", err)
+		os.Exit(1)
+	}
+
+	privateKeyBytes := x509.MarshalPKCS1PrivateKey(rsaKey)
+
+	signingPublicKey := base64.StdEncoding.EncodeToString(publicKeyBytes)
+	signingPrivateKey := base64.StdEncoding.EncodeToString(privateKeyBytes)
+
+	slokTimePeriodsPerOSL := 1
+	for _, keySplit := range config.Schemes[scheme].SeedPeriodKeySplits {
+		slokTimePeriodsPerOSL *= keySplit.Total
+	}
+	oslTimePeriod := time.Duration(config.Schemes[0].SeedPeriodNanoseconds * int64(slokTimePeriodsPerOSL))
+
+	for _, propagationChannelID := range config.Schemes[0].PropagationChannelIDs {
+
+		paveServerEntries := make([]map[time.Time]string, len(config.Schemes))
+		paveServerEntries[0] = make(map[time.Time]string)
+
+		epoch, _ := time.Parse(time.RFC3339, config.Schemes[0].Epoch)
+		for i := oslOffset; i < oslOffset+oslCount; i++ {
+			paveServerEntries[0][epoch.Add(time.Duration(i)*oslTimePeriod)] = ""
+		}
+
+		paveFiles, err := config.Pave(
+			epoch.Add(time.Duration(oslOffset+oslCount)*oslTimePeriod),
+			propagationChannelID,
+			signingPublicKey,
+			signingPrivateKey,
+			paveServerEntries)
+		if err != nil {
+			fmt.Printf("failed paving: %s\n", err)
+			os.Exit(1)
+		}
+
+		directory := filepath.Join(destinationDirectory, propagationChannelID)
+
+		err = os.MkdirAll(directory, 0755)
+		if err != nil {
+			fmt.Printf("failed creating output directory: %s\n", err)
+			os.Exit(1)
+		}
+
+		for _, paveFile := range paveFiles {
+			filename := filepath.Join(directory, paveFile.Name)
+			err = ioutil.WriteFile(filename, paveFile.Contents, 0755)
+			if err != nil {
+				fmt.Printf("error writing output file: %s\n", err)
+				os.Exit(1)
+			}
+		}
+	}
+}

+ 1 - 1
psiphon/opensslConn.go

@@ -1,4 +1,4 @@
-// +build android windows darwin
+// +build OPENSSL
 
 /*
  * Copyright (c) 2015, Psiphon Inc.

+ 1 - 1
psiphon/opensslConn_unsupported.go

@@ -1,4 +1,4 @@
-// +build !android,!windows,!darwin
+// +build !OPENSSL
 
 /*
  * Copyright (c) 2015, Psiphon Inc.

+ 18 - 7
psiphon/server/psinet/psinet.go

@@ -42,10 +42,11 @@ import (
 type Database struct {
 	common.ReloadableFile
 
-	Hosts    map[string]Host            `json:"hosts"`
-	Servers  []Server                   `json:"servers"`
-	Sponsors map[string]Sponsor         `json:"sponsors"`
-	Versions map[string][]ClientVersion `json:"client_versions"`
+	Hosts            map[string]Host            `json:"hosts"`
+	Servers          []Server                   `json:"servers"`
+	Sponsors         map[string]Sponsor         `json:"sponsors"`
+	Versions         map[string][]ClientVersion `json:"client_versions"`
+	DefaultSponsorID string                     `json:"default_sponsor_id"`
 }
 
 type Host struct {
@@ -167,7 +168,10 @@ func (db *Database) GetHomepages(sponsorID, clientRegion string, isMobilePlatfor
 	// Sponsor id does not exist: fail gracefully
 	sponsor, ok := db.Sponsors[sponsorID]
 	if !ok {
-		return sponsorHomePages
+		sponsor, ok = db.Sponsors[db.DefaultSponsorID]
+		if !ok {
+			return sponsorHomePages
+		}
 	}
 
 	homePages := sponsor.HomePages
@@ -239,14 +243,21 @@ func (db *Database) GetUpgradeClientVersion(clientVersion, clientPlatform string
 }
 
 // GetHttpsRequestRegexes returns bytes transferred stats regexes for the
-// specified sponsor. The result is nil when an unknown sponsorID is provided.
+// specified sponsor.
 func (db *Database) GetHttpsRequestRegexes(sponsorID string) []map[string]string {
 	db.ReloadableFile.RLock()
 	defer db.ReloadableFile.RUnlock()
 
 	regexes := make([]map[string]string, 0)
 
-	for i := range db.Sponsors[sponsorID].HttpsRequestRegexes {
+	sponsor, ok := db.Sponsors[sponsorID]
+	if !ok {
+		sponsor, ok = db.Sponsors[db.DefaultSponsorID]
+	}
+
+	// If neither sponsorID or DefaultSponsorID were found, sponsor will be the
+	// zero value of the map, an empty Sponsor struct.
+	for i := range sponsor.HttpsRequestRegexes {
 		regex := make(map[string]string)
 		regex["replace"] = db.Sponsors[sponsorID].HttpsRequestRegexes[i].Replace
 		regex["regex"] = db.Sponsors[sponsorID].HttpsRequestRegexes[i].Regex

+ 92 - 87
psiphon/server/tunnelServer.go

@@ -587,25 +587,24 @@ func (sshServer *sshServer) handleClient(tunnelProtocol string, clientConn net.C
 
 type sshClient struct {
 	sync.Mutex
-	sshServer               *sshServer
-	tunnelProtocol          string
-	sshConn                 ssh.Conn
-	activityConn            *common.ActivityMonitoredConn
-	throttledConn           *common.ThrottledConn
-	geoIPData               GeoIPData
-	sessionID               string
-	supportsServerRequests  bool
-	handshakeState          handshakeState
-	udpChannel              ssh.Channel
-	trafficRules            TrafficRules
-	tcpTrafficState         trafficState
-	udpTrafficState         trafficState
-	qualityMetrics          qualityMetrics
-	channelHandlerWaitGroup *sync.WaitGroup
-	tcpPortForwardLRU       *common.LRUConns
-	oslClientSeedState      *osl.ClientSeedState
-	signalIssueSLOKs        chan struct{}
-	stopBroadcast           chan struct{}
+	sshServer              *sshServer
+	tunnelProtocol         string
+	sshConn                ssh.Conn
+	activityConn           *common.ActivityMonitoredConn
+	throttledConn          *common.ThrottledConn
+	geoIPData              GeoIPData
+	sessionID              string
+	supportsServerRequests bool
+	handshakeState         handshakeState
+	udpChannel             ssh.Channel
+	trafficRules           TrafficRules
+	tcpTrafficState        trafficState
+	udpTrafficState        trafficState
+	qualityMetrics         qualityMetrics
+	tcpPortForwardLRU      *common.LRUConns
+	oslClientSeedState     *osl.ClientSeedState
+	signalIssueSLOKs       chan struct{}
+	stopBroadcast          chan struct{}
 }
 
 type trafficState struct {
@@ -637,13 +636,12 @@ type handshakeState struct {
 func newSshClient(
 	sshServer *sshServer, tunnelProtocol string, geoIPData GeoIPData) *sshClient {
 	return &sshClient{
-		sshServer:               sshServer,
-		tunnelProtocol:          tunnelProtocol,
-		geoIPData:               geoIPData,
-		channelHandlerWaitGroup: new(sync.WaitGroup),
-		tcpPortForwardLRU:       common.NewLRUConns(),
-		signalIssueSLOKs:        make(chan struct{}, 1),
-		stopBroadcast:           make(chan struct{}),
+		sshServer:         sshServer,
+		tunnelProtocol:    tunnelProtocol,
+		geoIPData:         geoIPData,
+		tcpPortForwardLRU: common.NewLRUConns(),
+		signalIssueSLOKs:  make(chan struct{}, 1),
+		stopBroadcast:     make(chan struct{}),
 	}
 }
 
@@ -761,12 +759,20 @@ func (sshClient *sshClient) run(clientConn net.Conn) {
 		log.WithContext().Warning("register failed")
 		return
 	}
-	defer sshClient.sshServer.unregisterEstablishedClient(sshClient.sessionID)
 
 	sshClient.runTunnel(result.channels, result.requests)
 
 	// Note: sshServer.unregisterEstablishedClient calls sshClient.stop(),
 	// which also closes underlying transport Conn.
+
+	sshClient.sshServer.unregisterEstablishedClient(sshClient.sessionID)
+
+	sshClient.logTunnel()
+
+	// Initiate cleanup of the GeoIP session cache. To allow for post-tunnel
+	// final status requests, the lifetime of cached GeoIP records exceeds the
+	// lifetime of the sshClient.
+	sshClient.sshServer.support.GeoIPService.MarkSessionCacheToExpire(sshClient.sessionID)
 }
 
 func (sshClient *sshClient) passwordCallback(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
@@ -864,54 +870,13 @@ func (sshClient *sshClient) authLogCallback(conn ssh.ConnMetadata, method string
 	}
 }
 
+// stop signals the ssh connection to shutdown. After sshConn() returns,
+// the connection has terminated but sshClient.run() may still be
+// running and in the process of exiting.
 func (sshClient *sshClient) stop() {
 
 	sshClient.sshConn.Close()
 	sshClient.sshConn.Wait()
-
-	close(sshClient.stopBroadcast)
-	sshClient.channelHandlerWaitGroup.Wait()
-
-	// Note: reporting duration based on last confirmed data transfer, which
-	// is reads for sshClient.activityConn.GetActiveDuration(), and not
-	// connection closing is important for protocols such as meek. For
-	// meek, the connection remains open until the HTTP session expires,
-	// which may be some time after the tunnel has closed. (The meek
-	// protocol has no allowance for signalling payload EOF, and even if
-	// it did the client may not have the opportunity to send a final
-	// request with an EOF flag set.)
-
-	sshClient.Lock()
-
-	logFields := getRequestLogFields(
-		sshClient.sshServer.support,
-		"server_tunnel",
-		sshClient.geoIPData,
-		sshClient.handshakeState.apiParams,
-		baseRequestParams)
-
-	logFields["handshake_completed"] = sshClient.handshakeState.completed
-	logFields["start_time"] = sshClient.activityConn.GetStartTime()
-	logFields["duration"] = sshClient.activityConn.GetActiveDuration() / time.Millisecond
-	logFields["bytes_up_tcp"] = sshClient.tcpTrafficState.bytesUp
-	logFields["bytes_down_tcp"] = sshClient.tcpTrafficState.bytesDown
-	logFields["peak_concurrent_port_forward_count_tcp"] = sshClient.tcpTrafficState.peakConcurrentPortForwardCount
-	logFields["total_port_forward_count_tcp"] = sshClient.tcpTrafficState.totalPortForwardCount
-	logFields["bytes_up_udp"] = sshClient.udpTrafficState.bytesUp
-	logFields["bytes_down_udp"] = sshClient.udpTrafficState.bytesDown
-	logFields["peak_concurrent_port_forward_count_udp"] = sshClient.udpTrafficState.peakConcurrentPortForwardCount
-	logFields["total_port_forward_count_udp"] = sshClient.udpTrafficState.totalPortForwardCount
-
-	sessionID := sshClient.sessionID
-
-	sshClient.Unlock()
-
-	// Initiate cleanup of the GeoIP session cache. To allow for post-tunnel
-	// final status requests, the lifetime of cached GeoIP records exceeds the
-	// lifetime of the sshClient.
-	sshClient.sshServer.support.GeoIPService.MarkSessionCacheToExpire(sessionID)
-
-	log.LogRawFieldsWithTimestamp(logFields)
 }
 
 // runTunnel handles/dispatches new channel and new requests from the client.
@@ -920,13 +885,11 @@ func (sshClient *sshClient) stop() {
 func (sshClient *sshClient) runTunnel(
 	channels <-chan ssh.NewChannel, requests <-chan *ssh.Request) {
 
-	stopBroadcast := make(chan struct{})
-
-	requestsWaitGroup := new(sync.WaitGroup)
+	waitGroup := new(sync.WaitGroup)
 
-	requestsWaitGroup.Add(1)
+	waitGroup.Add(1)
 	go func() {
-		defer requestsWaitGroup.Done()
+		defer waitGroup.Done()
 
 		for request := range requests {
 
@@ -960,10 +923,10 @@ func (sshClient *sshClient) runTunnel(
 	}()
 
 	if sshClient.supportsServerRequests {
-		requestsWaitGroup.Add(1)
+		waitGroup.Add(1)
 		go func() {
-			defer requestsWaitGroup.Done()
-			sshClient.runOSLSender(stopBroadcast)
+			defer waitGroup.Done()
+			sshClient.runOSLSender()
 		}()
 	}
 
@@ -975,23 +938,66 @@ func (sshClient *sshClient) runTunnel(
 		}
 
 		// process each port forward concurrently
-		sshClient.channelHandlerWaitGroup.Add(1)
-		go sshClient.handleNewPortForwardChannel(newChannel)
+		waitGroup.Add(1)
+		go func(channel ssh.NewChannel) {
+			defer waitGroup.Done()
+			sshClient.handleNewPortForwardChannel(channel)
+		}(newChannel)
 	}
 
-	close(stopBroadcast)
+	// The channel loop is interrupted by a client
+	// disconnect or by calling sshClient.stop().
+
+	close(sshClient.stopBroadcast)
+
+	waitGroup.Wait()
+}
+
+func (sshClient *sshClient) logTunnel() {
+
+	// Note: reporting duration based on last confirmed data transfer, which
+	// is reads for sshClient.activityConn.GetActiveDuration(), and not
+	// connection closing is important for protocols such as meek. For
+	// meek, the connection remains open until the HTTP session expires,
+	// which may be some time after the tunnel has closed. (The meek
+	// protocol has no allowance for signalling payload EOF, and even if
+	// it did the client may not have the opportunity to send a final
+	// request with an EOF flag set.)
 
-	requestsWaitGroup.Wait()
+	sshClient.Lock()
+
+	logFields := getRequestLogFields(
+		sshClient.sshServer.support,
+		"server_tunnel",
+		sshClient.geoIPData,
+		sshClient.handshakeState.apiParams,
+		baseRequestParams)
+
+	logFields["handshake_completed"] = sshClient.handshakeState.completed
+	logFields["start_time"] = sshClient.activityConn.GetStartTime()
+	logFields["duration"] = sshClient.activityConn.GetActiveDuration() / time.Millisecond
+	logFields["bytes_up_tcp"] = sshClient.tcpTrafficState.bytesUp
+	logFields["bytes_down_tcp"] = sshClient.tcpTrafficState.bytesDown
+	logFields["peak_concurrent_port_forward_count_tcp"] = sshClient.tcpTrafficState.peakConcurrentPortForwardCount
+	logFields["total_port_forward_count_tcp"] = sshClient.tcpTrafficState.totalPortForwardCount
+	logFields["bytes_up_udp"] = sshClient.udpTrafficState.bytesUp
+	logFields["bytes_down_udp"] = sshClient.udpTrafficState.bytesDown
+	logFields["peak_concurrent_port_forward_count_udp"] = sshClient.udpTrafficState.peakConcurrentPortForwardCount
+	logFields["total_port_forward_count_udp"] = sshClient.udpTrafficState.totalPortForwardCount
+
+	sshClient.Unlock()
+
+	log.LogRawFieldsWithTimestamp(logFields)
 }
 
-func (sshClient *sshClient) runOSLSender(stopBroadcast <-chan struct{}) {
+func (sshClient *sshClient) runOSLSender() {
 
 	for {
 		// Await a signal that there are SLOKs to send
 		// TODO: use reflect.SelectCase, and optionally await timer here?
 		select {
 		case <-sshClient.signalIssueSLOKs:
-		case <-stopBroadcast:
+		case <-sshClient.stopBroadcast:
 			return
 		}
 
@@ -1009,7 +1015,7 @@ func (sshClient *sshClient) runOSLSender(stopBroadcast <-chan struct{}) {
 			select {
 			case <-retryTimer.C:
 			case <-sshClient.signalIssueSLOKs:
-			case <-stopBroadcast:
+			case <-sshClient.stopBroadcast:
 				retryTimer.Stop()
 				return
 			}
@@ -1071,7 +1077,6 @@ func (sshClient *sshClient) rejectNewChannel(newChannel ssh.NewChannel, reason s
 }
 
 func (sshClient *sshClient) handleNewPortForwardChannel(newChannel ssh.NewChannel) {
-	defer sshClient.channelHandlerWaitGroup.Done()
 
 	// http://tools.ietf.org/html/rfc4254#section-7.2
 	var directTcpipExtraData struct {