Sfoglia il codice sorgente

Merge branch 'master' into indistinguishable-tls

Rod Hynes 9 anni fa
parent
commit
64c5152438
63 ha cambiato i file con 1397 aggiunte e 771 eliminazioni
  1. 1 2
      ConsoleClient/Dockerfile
  2. 11 9
      ConsoleClient/make.bash
  3. 5 1
      MobileLibrary/Android/PsiphonTunnel/PsiphonTunnel.java
  4. 1 1
      MobileLibrary/Android/make.bash
  5. 3 18
      MobileLibrary/iOS/OpenSSL-for-iPhone/build-libssl.sh
  6. 55 23
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel.xcodeproj/project.pbxproj
  7. 1 1
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel.xcodeproj/xcshareddata/xcschemes/PsiphonTunnel.xcscheme
  8. 21 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Psi-meta.h
  9. 7 2
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.h
  10. 53 4
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.m
  11. 54 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Reachability/LookupIPv6.c
  12. 25 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Reachability/LookupIPv6.h
  13. 5 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Reachability/README.md
  14. 40 78
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Reachability/Reachability.h
  15. 160 399
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Reachability/Reachability.m
  16. 72 0
      MobileLibrary/iOS/PsiphonTunnel/scripts/strip-frameworks.sh
  17. 17 0
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest.xcodeproj/project.pbxproj
  18. 55 11
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/Contents.json
  19. BIN
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/ipad-app-76pt@1x.png
  20. BIN
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/ipad-app-76pt@2x.png
  21. BIN
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/ipad-notifications-20pt@1x.png
  22. BIN
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/ipad-notifications-20pt@2x.png
  23. BIN
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/ipad-pro-app-83.5pt@2x.png
  24. BIN
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/ipad-settings-29pt@1x.png
  25. BIN
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/ipad-settings-29pt@2x.png
  26. BIN
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/ipad-spotlight-40pt@1x.png
  27. BIN
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/ipad-spotlight-40pt@2x.png
  28. BIN
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/iphone-app-60pt@2x.png
  29. BIN
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/iphone-app-60pt@3x.png
  30. BIN
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/iphone-notification-20pt@2x.png
  31. BIN
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/iphone-notification-20pt@3x.png
  32. BIN
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/iphone-spotlight-40pt@2x.png
  33. BIN
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/iphone-spotlight-40pt@3x.png
  34. BIN
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/iphone-spotlight-settings-29pt@2x.png
  35. BIN
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/iphone-spotlight-settings-29pt@3x.png
  36. 2 0
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Info.plist
  37. 13 1
      MobileLibrary/iOS/USAGE.md
  38. 22 38
      MobileLibrary/iOS/build-psiphon-framework.sh
  39. 6 1
      MobileLibrary/psi/psi.go
  40. 1 1
      Server/make.bash
  41. 30 12
      psiphon/LookupIP.go
  42. 24 0
      psiphon/TCPConn.go
  43. 24 6
      psiphon/TCPConn_bind.go
  44. 10 29
      psiphon/common/osl/osl.go
  45. 2 1
      psiphon/common/osl/osl_test.go
  46. 2 3
      psiphon/common/osl/paver/README.md
  47. 44 23
      psiphon/common/osl/paver/main.go
  48. 26 0
      psiphon/common/utils.go
  49. 17 0
      psiphon/common/utils_test.go
  50. 173 10
      psiphon/config.go
  51. 166 0
      psiphon/config_test.go
  52. 30 27
      psiphon/controller.go
  53. 30 6
      psiphon/controller_test.go
  54. 43 9
      psiphon/dataStore.go
  55. 3 1
      psiphon/feedback.go
  56. 27 4
      psiphon/net.go
  57. 39 15
      psiphon/remoteServerList.go
  58. 50 31
      psiphon/remoteServerList_test.go
  59. 1 1
      psiphon/server/geoip.go
  60. 16 2
      psiphon/server/server_test.go
  61. 1 0
      psiphon/serverApi.go
  62. 1 0
      psiphon/tunnel.go
  63. 8 1
      psiphon/upgradeDownload.go

+ 1 - 2
ConsoleClient/Dockerfile

@@ -30,8 +30,7 @@ RUN curl -L https://storage.googleapis.com/golang/$GOVERSION.linux-amd64.tar.gz
    && echo $GOVERSION > $GOROOT/VERSION
 
 # Get external Go dependencies.
-RUN go get github.com/mitchellh/gox \
-    && go get github.com/pwaller/goupx
+RUN go get github.com/pwaller/goupx
 
 # Setup OpenSSL libray.
 ENV OPENSSL_VERSION=1.0.2h

+ 11 - 9
ConsoleClient/make.bash

@@ -32,7 +32,7 @@ prepare_build () {
   # - pipes to `xargs` again, specifiying `pkg` as the placeholder name for each item being operated on (which is the list of non standard library import paths from the previous step)
   #  - `xargs` runs a bash script (via `-c`) which changes to each import path in sequence, then echoes out `"<import path>":"<subshell output of getting the short git revision>",`
   # - this leaves a trailing `,` at the end, and no close to the JSON object, so simply `sed` replace the comma before the end of the line with `}` and you now have valid JSON
-  DEPENDENCIES=$(echo -n "{" && go list -tags "${BUILD_TAGS}" -f '{{range $dep := .Deps}}{{printf "%s\n" $dep}}{{end}}' | xargs go list -f '{{if not .Standard}}{{.ImportPath}}{{end}}' | xargs -I pkg bash -c 'cd $GOPATH/src/pkg && echo -n "\"pkg\":\"$(git rev-parse --short HEAD)\","' | sed 's/,$/}/')
+  DEPENDENCIES=$(echo -n "{" && go list -tags "$1" -f '{{range $dep := .Deps}}{{printf "%s\n" $dep}}{{end}}' | xargs go list -f '{{if not .Standard}}{{.ImportPath}}{{end}}' | xargs -I pkg bash -c 'cd $GOPATH/src/pkg && echo -n "\"pkg\":\"$(git rev-parse --short HEAD)\","' | sed 's/,$/}/')
 
   LDFLAGS="\
   -X github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common.buildDate=$BUILDDATE \
@@ -59,7 +59,7 @@ fi
 build_for_windows () {
   echo "...Getting project dependencies (via go get) for Windows. Parameter is: '$1'"
   GOOS=windows go get -d -v -tags "$WINDOWS_BUILD_TAGS" ./...
-  prepare_build
+  prepare_build $WINDOWS_BUILD_TAGS
   if [ $? != 0 ]; then
     echo "....'go get' failed, exiting"
     exit $?
@@ -75,7 +75,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 -tags "$WINDOWS_BUILD_TAGS" -output bin/windows/${EXE_BASENAME}-i686
+    GOOS=windows GOARCH=386 go build -v -x -ldflags "$LDFLAGS" -tags "$WINDOWS_BUILD_TAGS" -o bin/windows/${EXE_BASENAME}-i686.exe
     RETVAL=$?
     echo ".....gox completed, exit code: $?"
     if [ $RETVAL != 0 ]; then
@@ -98,7 +98,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 -tags "$WINDOWS_BUILD_TAGS" -output bin/windows/${EXE_BASENAME}-x86_64
+    GOOS=windows GOARCH=amd64 go build -v -x -ldflags "$LDFLAGS" -tags "$WINDOWS_BUILD_TAGS" -o bin/windows/${EXE_BASENAME}-x86_64.exe
     RETVAL=$?
     if [ $RETVAL != 0 ]; then
       echo ".....gox failed, exiting"
@@ -114,7 +114,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 -tags "$LINUX_BUILD_TAGS" ./...
-  prepare_build
+  prepare_build $LINUX_BUILD_TAGS
   if [ $? != 0 ]; then
     echo "...'go get' failed, exiting"
     exit $?
@@ -122,7 +122,8 @@ build_for_linux () {
 
   if [ -z $1 ] || [ "$1" == "32" ]; then
     echo "...Building linux-i686"
-    CFLAGS=-m32 gox -verbose -ldflags "$LDFLAGS" -osarch linux/386 -tags "$LINUX_BUILD_TAGS" -output bin/linux/${EXE_BASENAME}-i686
+    # TODO: is "CFLAGS=-m32" required?
+    CFLAGS=-m32 GOOS=linux GOARCH=386 go build -v -x -ldflags "$LDFLAGS" -tags "$LINUX_BUILD_TAGS" -o bin/linux/${EXE_BASENAME}-i686
     RETVAL=$?
     if [ $RETVAL != 0 ]; then
       echo ".....gox failed, exiting"
@@ -142,7 +143,7 @@ build_for_linux () {
 
   if [ -z $1 ] || [ "$1" == "64" ]; then
     echo "...Building linux-x86_64"
-    gox -verbose -ldflags "$LDFLAGS" -osarch linux/amd64 -tags "$LINUX_BUILD_TAGS" -output bin/linux/${EXE_BASENAME}-x86_64
+    GOOS=linux GOARCH=amd64 go build -v -x -ldflags "$LDFLAGS" -tags "$LINUX_BUILD_TAGS" -o bin/linux/${EXE_BASENAME}-x86_64
     RETVAL=$?
     if [ $RETVAL != 0 ]; then
       echo "....gox failed, exiting"
@@ -164,7 +165,7 @@ build_for_linux () {
 build_for_osx () {
   echo "Getting project dependencies (via go get) for OSX"
   GOOS=darwin go get -d -v -tags "$OSX_BUILD_TAGS" ./...
-  prepare_build
+  prepare_build $OSX_BUILD_TAGS
   if [ $? != 0 ]; then
     echo "..'go get' failed, exiting"
     exit $?
@@ -172,7 +173,8 @@ build_for_osx () {
 
   echo "Building darwin-x86_64..."
   echo "..Disabling CGO for this build"
-  CGO_ENABLED=0 gox -verbose -ldflags "$LDFLAGS" -osarch darwin/amd64 -tags "$OSX_BUILD_TAGS" -output bin/darwin/${EXE_BASENAME}-x86_64
+  # TODO: is "CGO_ENABLED=0" required?
+  CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -v -x -ldflags "$LDFLAGS" -tags "$OSX_BUILD_TAGS" -o bin/darwin/${EXE_BASENAME}-x86_64
   # Darwin binaries don't seem to be UPXable when built this way
   echo "..No UPX for this build"
 }

+ 5 - 1
MobileLibrary/Android/PsiphonTunnel/PsiphonTunnel.java

@@ -335,6 +335,9 @@ public class PsiphonTunnel extends Psi.PsiphonProvider.Stub {
         return DEFAULT_SECONDARY_DNS_SERVER;
     }
 
+    @Override
+    public String IPv6Synthesize(String IPv4Addr) { return IPv4Addr; }
+
     //----------------------------------------------------------------------------------------------
     // Psiphon Tunnel Core
     //----------------------------------------------------------------------------------------------
@@ -347,7 +350,8 @@ public class PsiphonTunnel extends Psi.PsiphonProvider.Stub {
                     loadPsiphonConfig(mHostService.getContext()),
                     embeddedServerEntries,
                     this,
-                    isVpnMode());
+                    isVpnMode(),
+                    false /* Do not use IPv6 synthesizer for android */);
         } catch (java.lang.Exception e) {
             throw new Exception("failed to start Psiphon library", e);
         }

+ 1 - 1
MobileLibrary/Android/make.bash

@@ -55,7 +55,7 @@ echo " Gomobile version: ${GOMOBILEVERSION}"
 echo " Dependencies: ${DEPENDENCIES}"
 echo ""
 
-gomobile bind -v -target=android/arm -tags="${BUILD_TAGS}" -ldflags="$LDFLAGS" github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/psi
+gomobile bind -v -x -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 $?

+ 3 - 18
MobileLibrary/iOS/OpenSSL-for-iPhone/build-libssl.sh

@@ -37,21 +37,6 @@ ENABLE_EC_NISTP_64_GCC_128=""                                             #
 # Don't change anything under this line!                                  #
 #                                                                         #
 ###########################################################################
-spinner()
-{
-  local pid=$!
-  local delay=0.75
-  local spinstr='|/-\'
-  while [ "$(ps a | awk '{print $1}' | grep $pid)" ]; do
-    local temp=${spinstr#?}
-    printf " [%c]  " "$spinstr"
-    local spinstr=$temp${spinstr%"$temp"}
-    sleep $delay
-    printf "\b\b\b\b\b\b"
-  done
-  printf "    \b\b\b\b"
-}
-
 CURRENTPATH=`pwd`
 # PSIPHON: remove unneeded architectures
 #ARCHS="i386 x86_64 armv7 armv7s arm64 tv_x86_64 tv_arm64"
@@ -186,9 +171,9 @@ do
     fi
   else
     if [ "${ARCH}" == "x86_64" ]; then
-      (./Configure no-asm darwin64-x86_64-cc --openssldir="${CURRENTPATH}/bin/${PLATFORM}${SDKVERSION}-${ARCH}.sdk" ${LOCAL_CONFIG_OPTIONS} > "${LOG}" 2>&1) & spinner
+      (./Configure no-asm darwin64-x86_64-cc --openssldir="${CURRENTPATH}/bin/${PLATFORM}${SDKVERSION}-${ARCH}.sdk" ${LOCAL_CONFIG_OPTIONS} > "${LOG}" 2>&1)
     else
-      (./Configure iphoneos-cross --openssldir="${CURRENTPATH}/bin/${PLATFORM}${SDKVERSION}-${ARCH}.sdk" ${LOCAL_CONFIG_OPTIONS} > "${LOG}" 2>&1) & spinner
+      (./Configure iphoneos-cross --openssldir="${CURRENTPATH}/bin/${PLATFORM}${SDKVERSION}-${ARCH}.sdk" ${LOCAL_CONFIG_OPTIONS} > "${LOG}" 2>&1)
     fi
   fi
 
@@ -216,7 +201,7 @@ do
     if [[ ! -z $CONFIG_OPTIONS ]]; then
       make depend >> "${LOG}" 2>&1
     fi
-    (make >> "${LOG}" 2>&1) & spinner
+    (make >> "${LOG}" 2>&1)
   fi
   echo "\n"
 

+ 55 - 23
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel.xcodeproj/project.pbxproj

@@ -7,15 +7,22 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
-		662659271DD270E900872F6C /* Reachability.h in Headers */ = {isa = PBXBuildFile; fileRef = 662659251DD270E900872F6C /* Reachability.h */; };
+		4E89F7FE1E2ED3CE00005F4C /* LookupIPv6.c in Sources */ = {isa = PBXBuildFile; fileRef = 4E89F7FC1E2ED3CE00005F4C /* LookupIPv6.c */; };
+		4E89F7FF1E2ED3CE00005F4C /* LookupIPv6.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E89F7FD1E2ED3CE00005F4C /* LookupIPv6.h */; };
+		660E0B7A1E2D6EB6002BF5D4 /* Psi in Frameworks */ = {isa = PBXBuildFile; fileRef = 660E0B791E2D6EB6002BF5D4 /* Psi */; };
+		662659271DD270E900872F6C /* Reachability.h in Headers */ = {isa = PBXBuildFile; fileRef = 662659251DD270E900872F6C /* Reachability.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		662659281DD270E900872F6C /* Reachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 662659261DD270E900872F6C /* Reachability.m */; };
+		6685BDCA1E2E882800F0E414 /* Psi.h in Headers */ = {isa = PBXBuildFile; fileRef = 6685BDC61E2E882800F0E414 /* Psi.h */; };
+		6685BDCB1E2E882800F0E414 /* ref.h in Headers */ = {isa = PBXBuildFile; fileRef = 6685BDC71E2E882800F0E414 /* ref.h */; };
+		6685BDCD1E2E88A200F0E414 /* Psi-meta.h in Headers */ = {isa = PBXBuildFile; fileRef = 6685BDCC1E2E88A200F0E414 /* Psi-meta.h */; };
+		6685BDD41E2EBB1000F0E414 /* GoPsi.objc.h in Headers */ = {isa = PBXBuildFile; fileRef = 6685BDD21E2EBB1000F0E414 /* GoPsi.objc.h */; };
+		6685BDD51E2EBB1000F0E414 /* Universe.objc.h in Headers */ = {isa = PBXBuildFile; fileRef = 6685BDD31E2EBB1000F0E414 /* Universe.objc.h */; };
+		6685BDD91E300AC200F0E414 /* strip-frameworks.sh in Resources */ = {isa = PBXBuildFile; fileRef = 6685BDD81E300AC200F0E414 /* strip-frameworks.sh */; };
 		66BDB02A1DA6BFCC0079384C /* PsiphonTunnel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66BDB0201DA6BFCC0079384C /* PsiphonTunnel.framework */; };
 		66BDB02F1DA6BFCC0079384C /* PsiphonTunnelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 66BDB02E1DA6BFCC0079384C /* PsiphonTunnelTests.m */; };
 		66BDB0311DA6BFCC0079384C /* PsiphonTunnel.h in Headers */ = {isa = PBXBuildFile; fileRef = 66BDB0231DA6BFCC0079384C /* PsiphonTunnel.h */; settings = {ATTRIBUTES = (Public, ); }; };
-		66BDB03B1DA6C4A70079384C /* Psi.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66BDB03A1DA6C4A70079384C /* Psi.framework */; };
 		66BDB03E1DA6C79E0079384C /* rootCAs.txt in Resources */ = {isa = PBXBuildFile; fileRef = 66BDB03D1DA6C79E0079384C /* rootCAs.txt */; };
 		66BDB0441DA6C7DD0079384C /* PsiphonTunnel.m in Sources */ = {isa = PBXBuildFile; fileRef = 66BDB0431DA6C7DD0079384C /* PsiphonTunnel.m */; };
-		66BDB0491DA6D7050079384C /* Psi.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 66BDB03A1DA6C4A70079384C /* Psi.framework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
 		66BDB05A1DC26CCC0079384C /* SBJson4.h in Headers */ = {isa = PBXBuildFile; fileRef = 66BDB04B1DC26CCC0079384C /* SBJson4.h */; };
 		66BDB05B1DC26CCC0079384C /* SBJson4Parser.h in Headers */ = {isa = PBXBuildFile; fileRef = 66BDB04C1DC26CCC0079384C /* SBJson4Parser.h */; };
 		66BDB05C1DC26CCC0079384C /* SBJson4Parser.m in Sources */ = {isa = PBXBuildFile; fileRef = 66BDB04D1DC26CCC0079384C /* SBJson4Parser.m */; };
@@ -50,22 +57,29 @@
 			dstPath = "";
 			dstSubfolderSpec = 10;
 			files = (
-				66BDB0491DA6D7050079384C /* Psi.framework in CopyFiles */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
 /* End PBXCopyFilesBuildPhase section */
 
 /* Begin PBXFileReference section */
+		4E89F7FC1E2ED3CE00005F4C /* LookupIPv6.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = LookupIPv6.c; sourceTree = "<group>"; };
+		4E89F7FD1E2ED3CE00005F4C /* LookupIPv6.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LookupIPv6.h; sourceTree = "<group>"; };
+		660E0B791E2D6EB6002BF5D4 /* Psi */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = Psi; path = PsiphonTunnel/Psi.framework/Versions/A/Psi; sourceTree = "<group>"; };
 		662659251DD270E900872F6C /* Reachability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Reachability.h; sourceTree = "<group>"; };
 		662659261DD270E900872F6C /* Reachability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Reachability.m; sourceTree = "<group>"; };
+		6685BDC61E2E882800F0E414 /* Psi.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Psi.h; path = PsiphonTunnel/Psi.framework/Versions/A/Headers/Psi.h; sourceTree = "<group>"; };
+		6685BDC71E2E882800F0E414 /* ref.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ref.h; path = PsiphonTunnel/Psi.framework/Versions/A/Headers/ref.h; sourceTree = "<group>"; };
+		6685BDCC1E2E88A200F0E414 /* Psi-meta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "Psi-meta.h"; path = "PsiphonTunnel/Psi-meta.h"; sourceTree = "<group>"; };
+		6685BDD21E2EBB1000F0E414 /* GoPsi.objc.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GoPsi.objc.h; path = PsiphonTunnel/Psi.framework/Versions/A/Headers/GoPsi.objc.h; sourceTree = "<group>"; };
+		6685BDD31E2EBB1000F0E414 /* Universe.objc.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Universe.objc.h; path = PsiphonTunnel/Psi.framework/Versions/A/Headers/Universe.objc.h; sourceTree = "<group>"; };
+		6685BDD81E300AC200F0E414 /* strip-frameworks.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; name = "strip-frameworks.sh"; path = "scripts/strip-frameworks.sh"; sourceTree = "<group>"; };
 		66BDB0201DA6BFCC0079384C /* PsiphonTunnel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PsiphonTunnel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		66BDB0231DA6BFCC0079384C /* PsiphonTunnel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PsiphonTunnel.h; sourceTree = "<group>"; };
 		66BDB0241DA6BFCC0079384C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		66BDB0291DA6BFCC0079384C /* PsiphonTunnelTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PsiphonTunnelTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 		66BDB02E1DA6BFCC0079384C /* PsiphonTunnelTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PsiphonTunnelTests.m; sourceTree = "<group>"; };
 		66BDB0301DA6BFCC0079384C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
-		66BDB03A1DA6C4A70079384C /* Psi.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Psi.framework; sourceTree = "<group>"; };
 		66BDB03D1DA6C79E0079384C /* rootCAs.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = rootCAs.txt; path = PsiphonTunnel/rootCAs.txt; sourceTree = "<group>"; };
 		66BDB0431DA6C7DD0079384C /* PsiphonTunnel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PsiphonTunnel.m; sourceTree = "<group>"; };
 		66BDB04B1DC26CCC0079384C /* SBJson4.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SBJson4.h; sourceTree = "<group>"; };
@@ -90,7 +104,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				66BDB03B1DA6C4A70079384C /* Psi.framework in Frameworks */,
+				660E0B7A1E2D6EB6002BF5D4 /* Psi in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -110,15 +124,31 @@
 			children = (
 				662659251DD270E900872F6C /* Reachability.h */,
 				662659261DD270E900872F6C /* Reachability.m */,
+				4E89F7FC1E2ED3CE00005F4C /* LookupIPv6.c */,
+				4E89F7FD1E2ED3CE00005F4C /* LookupIPv6.h */,
 			);
 			path = Reachability;
 			sourceTree = "<group>";
 		};
+		6685BDC31E2E881200F0E414 /* Psi */ = {
+			isa = PBXGroup;
+			children = (
+				660E0B791E2D6EB6002BF5D4 /* Psi */,
+				6685BDCC1E2E88A200F0E414 /* Psi-meta.h */,
+				6685BDC61E2E882800F0E414 /* Psi.h */,
+				6685BDC71E2E882800F0E414 /* ref.h */,
+				6685BDD21E2EBB1000F0E414 /* GoPsi.objc.h */,
+				6685BDD31E2EBB1000F0E414 /* Universe.objc.h */,
+			);
+			name = Psi;
+			sourceTree = "<group>";
+		};
 		66BDB0161DA6BFCC0079384C = {
 			isa = PBXGroup;
 			children = (
 				66BDB03C1DA6C7940079384C /* Resources */,
 				66BDB0221DA6BFCC0079384C /* PsiphonTunnel */,
+				6685BDC31E2E881200F0E414 /* Psi */,
 				66BDB02D1DA6BFCC0079384C /* PsiphonTunnelTests */,
 				66BDB0211DA6BFCC0079384C /* Products */,
 			);
@@ -141,7 +171,6 @@
 				66BDB0231DA6BFCC0079384C /* PsiphonTunnel.h */,
 				66BDB0431DA6C7DD0079384C /* PsiphonTunnel.m */,
 				66BDB0241DA6BFCC0079384C /* Info.plist */,
-				66BDB03A1DA6C4A70079384C /* Psi.framework */,
 			);
 			path = PsiphonTunnel;
 			sourceTree = "<group>";
@@ -158,6 +187,7 @@
 		66BDB03C1DA6C7940079384C /* Resources */ = {
 			isa = PBXGroup;
 			children = (
+				6685BDD81E300AC200F0E414 /* strip-frameworks.sh */,
 				66BDB03D1DA6C79E0079384C /* rootCAs.txt */,
 			);
 			name = Resources;
@@ -192,16 +222,22 @@
 			isa = PBXHeadersBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				6685BDCB1E2E882800F0E414 /* ref.h in Headers */,
+				4E89F7FF1E2ED3CE00005F4C /* LookupIPv6.h in Headers */,
+				662659271DD270E900872F6C /* Reachability.h in Headers */,
 				66BDB05D1DC26CCC0079384C /* SBJson4StreamParser.h in Headers */,
+				6685BDD41E2EBB1000F0E414 /* GoPsi.objc.h in Headers */,
+				6685BDD51E2EBB1000F0E414 /* Universe.objc.h in Headers */,
 				66BDB05F1DC26CCC0079384C /* SBJson4StreamParserState.h in Headers */,
 				66BDB0311DA6BFCC0079384C /* PsiphonTunnel.h in Headers */,
+				6685BDCA1E2E882800F0E414 /* Psi.h in Headers */,
 				66BDB0651DC26CCC0079384C /* SBJson4StreamWriterState.h in Headers */,
 				66BDB05B1DC26CCC0079384C /* SBJson4Parser.h in Headers */,
+				6685BDCD1E2E88A200F0E414 /* Psi-meta.h in Headers */,
 				66BDB05A1DC26CCC0079384C /* SBJson4.h in Headers */,
 				66BDB0611DC26CCC0079384C /* SBJson4StreamTokeniser.h in Headers */,
 				66BDB0631DC26CCC0079384C /* SBJson4StreamWriter.h in Headers */,
 				66BDB0671DC26CCC0079384C /* SBJson4Writer.h in Headers */,
-				662659271DD270E900872F6C /* Reachability.h in Headers */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -251,7 +287,7 @@
 		66BDB0171DA6BFCC0079384C /* Project object */ = {
 			isa = PBXProject;
 			attributes = {
-				LastUpgradeCheck = 0810;
+				LastUpgradeCheck = 0820;
 				ORGANIZATIONNAME = "Psiphon Inc.";
 				TargetAttributes = {
 					66BDB01F1DA6BFCC0079384C = {
@@ -290,6 +326,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				66BDB03E1DA6C79E0079384C /* rootCAs.txt in Resources */,
+				6685BDD91E300AC200F0E414 /* strip-frameworks.sh in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -311,6 +348,7 @@
 				66BDB0641DC26CCC0079384C /* SBJson4StreamWriter.m in Sources */,
 				66BDB0661DC26CCC0079384C /* SBJson4StreamWriterState.m in Sources */,
 				66BDB05C1DC26CCC0079384C /* SBJson4Parser.m in Sources */,
+				4E89F7FE1E2ED3CE00005F4C /* LookupIPv6.c in Sources */,
 				66BDB0681DC26CCC0079384C /* SBJson4Writer.m in Sources */,
 				66BDB0621DC26CCC0079384C /* SBJson4StreamTokeniser.m in Sources */,
 				66BDB0441DA6C7DD0079384C /* PsiphonTunnel.m in Sources */,
@@ -342,7 +380,6 @@
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				ALWAYS_SEARCH_USER_PATHS = NO;
-				ARCHS = "$(ARCHS_STANDARD)";
 				CLANG_ANALYZER_NONNULL = YES;
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
 				CLANG_CXX_LIBRARY = "libc++";
@@ -386,7 +423,6 @@
 				ONLY_ACTIVE_ARCH = YES;
 				SDKROOT = iphoneos;
 				TARGETED_DEVICE_FAMILY = "1,2";
-				VALID_ARCHS = "";
 				VERSIONING_SYSTEM = "apple-generic";
 				VERSION_INFO_PREFIX = "";
 			};
@@ -396,7 +432,6 @@
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				ALWAYS_SEARCH_USER_PATHS = NO;
-				ARCHS = "$(ARCHS_STANDARD)";
 				CLANG_ANALYZER_NONNULL = YES;
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
 				CLANG_CXX_LIBRARY = "libc++";
@@ -434,7 +469,6 @@
 				SDKROOT = iphoneos;
 				TARGETED_DEVICE_FAMILY = "1,2";
 				VALIDATE_PRODUCT = YES;
-				VALID_ARCHS = "";
 				VERSIONING_SYSTEM = "apple-generic";
 				VERSION_INFO_PREFIX = "";
 			};
@@ -450,18 +484,17 @@
 				DYLIB_CURRENT_VERSION = 1;
 				DYLIB_INSTALL_NAME_BASE = "@rpath";
 				ENABLE_BITCODE = NO;
-				FRAMEWORK_SEARCH_PATHS = (
-					"$(inherited)",
-					"$(PROJECT_DIR)/PsiphonTunnel",
-				);
 				INFOPLIST_FILE = PsiphonTunnel/Info.plist;
 				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
 				IPHONEOS_DEPLOYMENT_TARGET = 9.3;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				LIBRARY_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/PsiphonTunnel/Psi.framework/Versions/A",
+				);
 				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.PsiphonTunnel;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SKIP_INSTALL = YES;
-				VALID_ARCHS = "$(ARCHS) x86_64";
 			};
 			name = Debug;
 		};
@@ -475,18 +508,17 @@
 				DYLIB_CURRENT_VERSION = 1;
 				DYLIB_INSTALL_NAME_BASE = "@rpath";
 				ENABLE_BITCODE = NO;
-				FRAMEWORK_SEARCH_PATHS = (
-					"$(inherited)",
-					"$(PROJECT_DIR)/PsiphonTunnel",
-				);
 				INFOPLIST_FILE = PsiphonTunnel/Info.plist;
 				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
 				IPHONEOS_DEPLOYMENT_TARGET = 9.3;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				LIBRARY_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/PsiphonTunnel/Psi.framework/Versions/A",
+				);
 				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.PsiphonTunnel;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SKIP_INSTALL = YES;
-				VALID_ARCHS = "$(ARCHS) x86_64";
 			};
 			name = Release;
 		};

+ 1 - 1
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel.xcodeproj/xcshareddata/xcschemes/PsiphonTunnel.xcscheme

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "0810"
+   LastUpgradeVersion = "0820"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"

+ 21 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Psi-meta.h

@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2017, 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/>.
+ *
+ */
+
+#import "ref.h"
+#import "Psi.h"

+ 7 - 2
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.h

@@ -23,6 +23,8 @@
  */
 
 #import <UIKit/UIKit.h>
+#import "Reachability.h"
+
 
 //! Project version number for PsiphonTunnel.
 FOUNDATION_EXPORT double PsiphonTunnelVersionNumber;
@@ -52,10 +54,13 @@ FOUNDATION_EXPORT const unsigned char PsiphonTunnelVersionString[];
  - Remote server list functionality is not strictly required, but absence greatly undermines circumvention ability.
    - `RemoteServerListUrl`
    - `RemoteServerListSignaturePublicKey`
+ - Obfuscated server list functionality is also not strictly required, but aids circumvention ability.
+   - `ObfuscatedServerListRootURL`
 
  Optional fields (if you don't need them, don't set them):
- - `DataStoreDirectory`: If not set, the library will use a sane location. Override if the client wants to restrict where operational data is kept.
- - `RemoteServerListDownloadFilename`: See comment for `DataStoreDirectory`.
+ - `DataStoreDirectory`: If not set, the library will use a sane location. Override if the client wants to restrict where operational data is kept. If overridden, the directory must already exist and be writable.
+ - `RemoteServerListDownloadFilename`: If not set, the library will use a sane location. Override if the client wants to restrict where operational data is kept.
+ - `ObfuscatedServerListDownloadDirectory`: If not set, the library will use a sane location. Override if the client wants to restrict where operational data is kept. If overridden, the directory must already exist and be writable.
  - `ClientPlatform`: Should not be set by most library consumers.
  - `UpstreamProxyUrl`
  - `EmitDiagnosticNotices`

+ 53 - 4
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.m

@@ -19,9 +19,9 @@
 
 #import <CoreTelephony/CTTelephonyNetworkInfo.h>
 #import <CoreTelephony/CTCarrier.h>
-#import <Psi/Psi.h>
+#import "LookupIPv6.h"
+#import "Psi-meta.h"
 #import "PsiphonTunnel.h"
-#import "Reachability.h"
 #import "json-framework/SBJson4.h"
 
 
@@ -62,6 +62,9 @@
 
         // Not supported on iOS.
         const BOOL useDeviceBinder = FALSE;
+
+        // Must always use IPv6Synthesizer for iOS
+        const BOOL useIPv6Synthesizer = TRUE;
         
         NSString *configStr = [self getConfig];
         if (configStr == nil) {
@@ -76,6 +79,7 @@
                            embeddedServerEntries,
                            self,
                            useDeviceBinder,
+                           useIPv6Synthesizer,
                            &e);
             
             [self logMessage:[NSString stringWithFormat: @"GoPsiStart: %@", res ? @"TRUE" : @"FALSE"]];
@@ -183,6 +187,10 @@
         return nil;
     }
     
+    //
+    // DataStoreDirectory
+    //
+    
     // Some clients will have a data directory that they'd prefer the Psiphon
     // library use, but if not we'll default to the user Library directory.
     NSURL *defaultDataStoreDirectoryURL = [libraryURL URLByAppendingPathComponent:@"datastore" isDirectory:YES];
@@ -205,9 +213,11 @@
         [self logMessage:[NSString stringWithFormat: @"DataStoreDirectory overridden from '%@' to '%@'", [defaultDataStoreDirectoryURL path], config[@"DataStoreDirectory"]]];
     }
     
-    // See previous comment.
-    NSString *defaultRemoteServerListFilename = [[libraryURL URLByAppendingPathComponent:@"remote_server_list" isDirectory:NO] path];
+    //
+    // Remote Server List
+    //
     
+    NSString *defaultRemoteServerListFilename = [[libraryURL URLByAppendingPathComponent:@"remote_server_list" isDirectory:NO] path];
     if (defaultRemoteServerListFilename == nil) {
         [self logMessage:@"Unable to create defaultRemoteServerListFilename"];
         return nil;
@@ -226,6 +236,34 @@
         config[@"RemoteServerListSignaturePublicKey"] == nil) {
         [self logMessage:@"Remote server list functionality will be disabled"];
     }
+    
+    //
+    // Obfuscated Server List
+    //
+    
+    NSURL *defaultOSLDirectoryURL = [libraryURL URLByAppendingPathComponent:@"osl" isDirectory:YES];
+    if (defaultOSLDirectoryURL == nil) {
+        [self logMessage:@"Unable to create defaultOSLDirectory"];
+        return nil;
+    }
+    
+    if (config[@"ObfuscatedServerListDownloadDirectory"] == nil) {
+        [fileManager createDirectoryAtURL:defaultOSLDirectoryURL withIntermediateDirectories:YES attributes:nil error:&err];
+        if (err != nil) {
+            [self logMessage:[NSString stringWithFormat: @"Unable to create defaultOSLDirectoryURL: %@", err.localizedDescription]];
+            return nil;
+        }
+        
+        config[@"ObfuscatedServerListDownloadDirectory"] = [defaultOSLDirectoryURL path];
+    }
+    else {
+        [self logMessage:[NSString stringWithFormat: @"ObfuscatedServerListDownloadDirectory overridden from '%@' to '%@'", [defaultOSLDirectoryURL path], config[@"ObfuscatedServerListDownloadDirectory"]]];
+    }
+    
+    // If ObfuscatedServerListRootURL is absent, we'll leave it out, but log the absence.
+    if (config[@"ObfuscatedServerListRootURL"] == nil) {
+        [self logMessage:@"Obfuscated server list functionality will be disabled"];
+    }
 
     // Other optional fields not being altered. If not set, their defaults will be used:
     // * EstablishTunnelTimeoutSeconds
@@ -521,6 +559,17 @@
     return (netstat != NotReachable) ? 1 : 0;
 }
 
+- (NSString *)iPv6Synthesize:(NSString *)IPv4Addr {
+    // This function is called to synthesize an ipv6 address from an ipv4 one on a DNS64/NAT64 network
+    char *result = getIPv6ForIPv4([IPv4Addr UTF8String]);
+    if (result != NULL) {
+        NSString *IPv6Addr = [NSString stringWithUTF8String:result];
+        free(result);
+        return IPv6Addr;
+    }
+    return @"";
+}
+
 - (void)notice:(NSString *)noticeJSON {
     [self handlePsiphonNotice:noticeJSON];
 }

+ 54 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Reachability/LookupIPv6.c

@@ -0,0 +1,54 @@
+/*
+ * 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/>.
+ *
+ */
+
+#include "LookupIPv6.h"
+
+#include <arpa/inet.h>
+#include <err.h>
+#include <netdb.h>
+#include <stdlib.h>
+#include <string.h>
+
+char *getIPv6ForIPv4(const char *ipv4_str) {
+    char *ipv6_str = NULL;
+    struct addrinfo hints, *res, *res0;
+    int error;
+    
+    memset(&hints, 0, sizeof(hints));
+    hints.ai_family = AF_INET6;
+    hints.ai_socktype = SOCK_STREAM;
+    hints.ai_flags = AI_DEFAULT;
+    error = getaddrinfo(ipv4_str, NULL, &hints, &res0);
+    if (error) {
+        /* NOTREACHED */
+        return NULL;
+    }
+
+    for (res = res0; res; res = res->ai_next) {
+        if (res->ai_family == AF_INET6) {
+            struct sockaddr_in6 *sockaddr = (struct sockaddr_in6*)res->ai_addr;
+            ipv6_str = (char *)malloc(sizeof(char)*(INET6_ADDRSTRLEN)); // INET6_ADDRSTRLEN includes null terminating character
+            inet_ntop(AF_INET6, &(sockaddr->sin6_addr), ipv6_str, INET6_ADDRSTRLEN);
+            break;
+        }
+    }
+    
+    freeaddrinfo(res0);
+    return ipv6_str;
+}

+ 25 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Reachability/LookupIPv6.h

@@ -0,0 +1,25 @@
+/*
+ * 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/>.
+ *
+ */
+
+#ifndef LookupIPv6_h
+#define LookupIPv6_h
+
+char *getIPv6ForIPv4(const char *ipv4_str);
+
+#endif /* LookupIPv6_h */

+ 5 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Reachability/README.md

@@ -0,0 +1,5 @@
+# Reachability
+
+This code is from: 
+
+https://developer.apple.com/library/content/samplecode/Reachability/Introduction/Intro.html

+ 40 - 78
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Reachability/Reachability.h

@@ -1,102 +1,64 @@
 /*
- Copyright (c) 2011, Tony Million.
- All rights reserved.
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
  
- Redistribution and use in source and binary forms, with or without
- modification, are permitted provided that the following conditions are met:
- 
- 1. Redistributions of source code must retain the above copyright notice, this
- list of conditions and the following disclaimer.
- 
- 2. Redistributions in binary form must reproduce the above copyright notice,
- this list of conditions and the following disclaimer in the documentation
- and/or other materials provided with the distribution.
- 
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- POSSIBILITY OF SUCH DAMAGE.
+ Abstract:
+ Basic demonstration of how to use the SystemConfiguration Reachablity APIs.
  */
 
 #import <Foundation/Foundation.h>
 #import <SystemConfiguration/SystemConfiguration.h>
+#import <netinet/in.h>
 
-//! Project version number for MacOSReachability.
-FOUNDATION_EXPORT double ReachabilityVersionNumber;
-
-//! Project version string for MacOSReachability.
-FOUNDATION_EXPORT const unsigned char ReachabilityVersionString[];
 
-/**
- * Create NS_ENUM macro if it does not exist on the targeted version of iOS or OS X.
- *
- * @see http://nshipster.com/ns_enum-ns_options/
- **/
-#ifndef NS_ENUM
-#define NS_ENUM(_type, _name) enum _name : _type _name; enum _name : _type
-#endif
+typedef enum : NSInteger {
+	NotReachable = 0,
+	ReachableViaWiFi,
+	ReachableViaWWAN
+} NetworkStatus;
 
-extern NSString *const kReachabilityChangedNotification;
+#pragma mark IPv6 Support
+//Reachability fully support IPv6.  For full details, see ReadMe.md.
 
-typedef NS_ENUM(NSInteger, NetworkStatus) {
-    // Apple NetworkStatus Compatible Names.
-    NotReachable = 0,
-    ReachableViaWiFi = 2,
-    ReachableViaWWAN = 1
-};
 
-@class Reachability;
-
-typedef void (^NetworkReachable)(Reachability * reachability);
-typedef void (^NetworkUnreachable)(Reachability * reachability);
-typedef void (^NetworkReachability)(Reachability * reachability, SCNetworkConnectionFlags flags);
+extern NSString *kReachabilityChangedNotification;
 
 
 @interface Reachability : NSObject
 
-@property (nonatomic, copy) NetworkReachable    reachableBlock;
-@property (nonatomic, copy) NetworkUnreachable  unreachableBlock;
-@property (nonatomic, copy) NetworkReachability reachabilityBlock;
-
-@property (nonatomic, assign) BOOL reachableOnWWAN;
+/*!
+ * Use to check the reachability of a given host name.
+ */
++ (instancetype)reachabilityWithHostName:(NSString *)hostName;
 
+/*!
+ * Use to check the reachability of a given IP address.
+ */
++ (instancetype)reachabilityWithAddress:(const struct sockaddr *)hostAddress;
 
-+(instancetype)reachabilityWithHostname:(NSString*)hostname;
-// This is identical to the function above, but is here to maintain
-//compatibility with Apples original code. (see .m)
-+(instancetype)reachabilityWithHostName:(NSString*)hostname;
-+(instancetype)reachabilityForInternetConnection;
-+(instancetype)reachabilityWithAddress:(void *)hostAddress;
-+(instancetype)reachabilityForLocalWiFi;
+/*!
+ * Checks whether the default route is available. Should be used by applications that do not connect to a particular host.
+ */
++ (instancetype)reachabilityForInternetConnection;
 
--(instancetype)initWithReachabilityRef:(SCNetworkReachabilityRef)ref;
 
--(BOOL)startNotifier;
--(void)stopNotifier;
+#pragma mark reachabilityForLocalWiFi
+//reachabilityForLocalWiFi has been removed from the sample.  See ReadMe.md for more information.
+//+ (instancetype)reachabilityForLocalWiFi;
 
--(BOOL)isReachable;
--(BOOL)isReachableViaWWAN;
--(BOOL)isReachableViaWiFi;
+/*!
+ * Start listening for reachability notifications on the current run loop.
+ */
+- (BOOL)startNotifier;
+- (void)stopNotifier;
 
-// WWAN may be available, but not active until a connection has been established.
-// WiFi may require a connection for VPN on Demand.
--(BOOL)isConnectionRequired; // Identical DDG variant.
--(BOOL)connectionRequired; // Apple's routine.
-// Dynamic, on demand connection?
--(BOOL)isConnectionOnDemand;
-// Is user intervention required?
--(BOOL)isInterventionRequired;
+- (NetworkStatus)currentReachabilityStatus;
 
--(NetworkStatus)currentReachabilityStatus;
--(SCNetworkReachabilityFlags)reachabilityFlags;
--(NSString*)currentReachabilityString;
--(NSString*)currentReachabilityFlags;
+/*!
+ * WWAN may be available, but not active until a connection has been established. WiFi may require a connection for VPN on Demand.
+ */
+- (BOOL)connectionRequired;
 
 @end
+
+

+ 160 - 399
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Reachability/Reachability.m

@@ -1,481 +1,242 @@
 /*
- Copyright (c) 2011, Tony Million.
- All rights reserved.
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
  
- Redistribution and use in source and binary forms, with or without
- modification, are permitted provided that the following conditions are met:
- 
- 1. Redistributions of source code must retain the above copyright notice, this
- list of conditions and the following disclaimer.
- 
- 2. Redistributions in binary form must reproduce the above copyright notice,
- this list of conditions and the following disclaimer in the documentation
- and/or other materials provided with the distribution.
- 
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- POSSIBILITY OF SUCH DAMAGE.
+ Abstract:
+ Basic demonstration of how to use the SystemConfiguration Reachablity APIs.
  */
 
-#import "Reachability.h"
-
-#import <sys/socket.h>
-#import <netinet/in.h>
-#import <netinet6/in6.h>
 #import <arpa/inet.h>
 #import <ifaddrs.h>
 #import <netdb.h>
+#import <sys/socket.h>
+#import <netinet/in.h>
 
+#import <CoreFoundation/CoreFoundation.h>
 
-NSString *const kReachabilityChangedNotification = @"kReachabilityChangedNotification";
+#import "Reachability.h"
 
+#pragma mark IPv6 Support
+//Reachability fully support IPv6.  For full details, see ReadMe.md.
 
-@interface Reachability ()
 
-@property (nonatomic, assign) SCNetworkReachabilityRef  reachabilityRef;
-@property (nonatomic, strong) dispatch_queue_t          reachabilitySerialQueue;
-@property (nonatomic, strong) id                        reachabilityObject;
+NSString *kReachabilityChangedNotification = @"kNetworkReachabilityChangedNotification";
 
--(void)reachabilityChanged:(SCNetworkReachabilityFlags)flags;
--(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags;
 
-@end
+#pragma mark - Supporting functions
 
+#define kShouldPrintReachabilityFlags 1
 
-static NSString *reachabilityFlags(SCNetworkReachabilityFlags flags)
+static void PrintReachabilityFlags(SCNetworkReachabilityFlags flags, const char* comment)
 {
-    return [NSString stringWithFormat:@"%c%c %c%c%c%c%c%c%c",
-#if	TARGET_OS_IPHONE
-            (flags & kSCNetworkReachabilityFlagsIsWWAN)               ? 'W' : '-',
-#else
-            'X',
-#endif
-            (flags & kSCNetworkReachabilityFlagsReachable)            ? 'R' : '-',
-            (flags & kSCNetworkReachabilityFlagsConnectionRequired)   ? 'c' : '-',
-            (flags & kSCNetworkReachabilityFlagsTransientConnection)  ? 't' : '-',
-            (flags & kSCNetworkReachabilityFlagsInterventionRequired) ? 'i' : '-',
-            (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic)  ? 'C' : '-',
-            (flags & kSCNetworkReachabilityFlagsConnectionOnDemand)   ? 'D' : '-',
-            (flags & kSCNetworkReachabilityFlagsIsLocalAddress)       ? 'l' : '-',
-            (flags & kSCNetworkReachabilityFlagsIsDirect)             ? 'd' : '-'];
-}
+#if kShouldPrintReachabilityFlags
 
-// Start listening for reachability notifications on the current run loop
-static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info)
-{
-#pragma unused (target)
-    
-    Reachability *reachability = ((__bridge Reachability*)info);
-    
-    // We probably don't need an autoreleasepool here, as GCD docs state each queue has its own autorelease pool,
-    // but what the heck eh?
-    @autoreleasepool
-    {
-        [reachability reachabilityChanged:flags];
-    }
-}
+    NSLog(@"Reachability Flag Status: %c%c %c%c%c%c%c%c%c %s\n",
+          (flags & kSCNetworkReachabilityFlagsIsWWAN)				? 'W' : '-',
+          (flags & kSCNetworkReachabilityFlagsReachable)            ? 'R' : '-',
 
-
-@implementation Reachability
-
-#pragma mark - Class Constructor Methods
-
-+(instancetype)reachabilityWithHostName:(NSString*)hostname
-{
-    return [Reachability reachabilityWithHostname:hostname];
+          (flags & kSCNetworkReachabilityFlagsTransientConnection)  ? 't' : '-',
+          (flags & kSCNetworkReachabilityFlagsConnectionRequired)   ? 'c' : '-',
+          (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic)  ? 'C' : '-',
+          (flags & kSCNetworkReachabilityFlagsInterventionRequired) ? 'i' : '-',
+          (flags & kSCNetworkReachabilityFlagsConnectionOnDemand)   ? 'D' : '-',
+          (flags & kSCNetworkReachabilityFlagsIsLocalAddress)       ? 'l' : '-',
+          (flags & kSCNetworkReachabilityFlagsIsDirect)             ? 'd' : '-',
+          comment
+          );
+#endif
 }
 
-+(instancetype)reachabilityWithHostname:(NSString*)hostname
-{
-    SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithName(NULL, [hostname UTF8String]);
-    if (ref)
-    {
-        id reachability = [[self alloc] initWithReachabilityRef:ref];
-        
-        CFRelease(ref);
-        
-        return reachability;
-    }
-    
-    return nil;
-}
 
-+(instancetype)reachabilityWithAddress:(void *)hostAddress
+static void ReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info)
 {
-    SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr*)hostAddress);
-    if (ref)
-    {
-        id reachability = [[self alloc] initWithReachabilityRef:ref];
-        
-        CFRelease(ref);
-        
-        return reachability;
-    }
-    
-    return nil;
-}
+#pragma unused (target, flags)
+	NSCAssert(info != NULL, @"info was NULL in ReachabilityCallback");
+	NSCAssert([(__bridge NSObject*) info isKindOfClass: [Reachability class]], @"info was wrong class in ReachabilityCallback");
 
-+(instancetype)reachabilityForInternetConnection
-{
-    struct sockaddr_in zeroAddress;
-    bzero(&zeroAddress, sizeof(zeroAddress));
-    zeroAddress.sin_len = sizeof(zeroAddress);
-    zeroAddress.sin_family = AF_INET;
-    
-    return [self reachabilityWithAddress:&zeroAddress];
-}
-
-+(instancetype)reachabilityForLocalWiFi
-{
-    struct sockaddr_in localWifiAddress;
-    bzero(&localWifiAddress, sizeof(localWifiAddress));
-    localWifiAddress.sin_len            = sizeof(localWifiAddress);
-    localWifiAddress.sin_family         = AF_INET;
-    // IN_LINKLOCALNETNUM is defined in <netinet/in.h> as 169.254.0.0
-    localWifiAddress.sin_addr.s_addr    = htonl(IN_LINKLOCALNETNUM);
-    
-    return [self reachabilityWithAddress:&localWifiAddress];
+    Reachability* noteObject = (__bridge Reachability *)info;
+    // Post a notification to notify the client that the network reachability changed.
+    [[NSNotificationCenter defaultCenter] postNotificationName: kReachabilityChangedNotification object: noteObject];
 }
 
 
-// Initialization methods
+#pragma mark - Reachability implementation
 
--(instancetype)initWithReachabilityRef:(SCNetworkReachabilityRef)ref
+@implementation Reachability
 {
-    self = [super init];
-    if (self != nil)
-    {
-        self.reachableOnWWAN = YES;
-        self.reachabilityRef = ref;
-        
-        CFRetain(self.reachabilityRef);
-        
-        // We need to create a serial queue.
-        // We allocate this once for the lifetime of the notifier.
-        
-        self.reachabilitySerialQueue = dispatch_queue_create("com.tonymillion.reachability", NULL);
-    }
-    
-    return self;
+	SCNetworkReachabilityRef _reachabilityRef;
 }
 
--(void)dealloc
++ (instancetype)reachabilityWithHostName:(NSString *)hostName
 {
-    [self stopNotifier];
-    
-    if(self.reachabilityRef)
-    {
-        CFRelease(self.reachabilityRef);
-        self.reachabilityRef = nil;
-    }
-    
-    self.reachableBlock          = nil;
-    self.unreachableBlock        = nil;
-    self.reachabilityBlock       = nil;
-    self.reachabilitySerialQueue = nil;
+	Reachability* returnValue = NULL;
+	SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithName(NULL, [hostName UTF8String]);
+	if (reachability != NULL)
+	{
+		returnValue= [[self alloc] init];
+		if (returnValue != NULL)
+		{
+			returnValue->_reachabilityRef = reachability;
+		}
+        else {
+            CFRelease(reachability);
+        }
+	}
+	return returnValue;
 }
 
-#pragma mark - Notifier Methods
 
-// Notifier
-// NOTE: This uses GCD to trigger the blocks - they *WILL NOT* be called on THE MAIN THREAD
-// - In other words DO NOT DO ANY UI UPDATES IN THE BLOCKS.
-//   INSTEAD USE dispatch_async(dispatch_get_main_queue(), ^{UISTUFF}) (or dispatch_sync if you want)
-
--(BOOL)startNotifier
++ (instancetype)reachabilityWithAddress:(const struct sockaddr *)hostAddress
 {
-    // allow start notifier to be called multiple times
-    if(self.reachabilityObject && (self.reachabilityObject == self))
-    {
-        return YES;
-    }
-    
-    
-    SCNetworkReachabilityContext    context = { 0, NULL, NULL, NULL, NULL };
-    context.info = (__bridge void *)self;
-    
-    if(SCNetworkReachabilitySetCallback(self.reachabilityRef, TMReachabilityCallback, &context))
-    {
-        // Set it as our reachability queue, which will retain the queue
-        if(SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef, self.reachabilitySerialQueue))
-        {
-            // this should do a retain on ourself, so as long as we're in notifier mode we shouldn't disappear out from under ourselves
-            // woah
-            self.reachabilityObject = self;
-            return YES;
-        }
-        else
-        {
-#ifdef DEBUG
-            NSLog(@"SCNetworkReachabilitySetDispatchQueue() failed: %s", SCErrorString(SCError()));
-#endif
-            
-            // UH OH - FAILURE - stop any callbacks!
-            SCNetworkReachabilitySetCallback(self.reachabilityRef, NULL, NULL);
+	SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, hostAddress);
+
+	Reachability* returnValue = NULL;
+
+	if (reachability != NULL)
+	{
+		returnValue = [[self alloc] init];
+		if (returnValue != NULL)
+		{
+			returnValue->_reachabilityRef = reachability;
+		}
+        else {
+            CFRelease(reachability);
         }
-    }
-    else
-    {
-#ifdef DEBUG
-        NSLog(@"SCNetworkReachabilitySetCallback() failed: %s", SCErrorString(SCError()));
-#endif
-    }
-    
-    // if we get here we fail at the internet
-    self.reachabilityObject = nil;
-    return NO;
+	}
+	return returnValue;
 }
 
--(void)stopNotifier
+
++ (instancetype)reachabilityForInternetConnection
 {
-    // First stop, any callbacks!
-    SCNetworkReachabilitySetCallback(self.reachabilityRef, NULL, NULL);
+	struct sockaddr_in zeroAddress;
+	bzero(&zeroAddress, sizeof(zeroAddress));
+	zeroAddress.sin_len = sizeof(zeroAddress);
+	zeroAddress.sin_family = AF_INET;
     
-    // Unregister target from the GCD serial dispatch queue.
-    SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef, NULL);
-    
-    self.reachabilityObject = nil;
+    return [self reachabilityWithAddress: (const struct sockaddr *) &zeroAddress];
 }
 
-#pragma mark - reachability tests
+#pragma mark reachabilityForLocalWiFi
+//reachabilityForLocalWiFi has been removed from the sample.  See ReadMe.md for more information.
+//+ (instancetype)reachabilityForLocalWiFi
 
-// This is for the case where you flick the airplane mode;
-// you end up getting something like this:
-//Reachability: WR ct-----
-//Reachability: -- -------
-//Reachability: WR ct-----
-//Reachability: -- -------
-// We treat this as 4 UNREACHABLE triggers - really apple should do better than this
 
-#define testcase (kSCNetworkReachabilityFlagsConnectionRequired | kSCNetworkReachabilityFlagsTransientConnection)
 
--(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags
-{
-    BOOL connectionUP = YES;
-    
-    if(!(flags & kSCNetworkReachabilityFlagsReachable))
-        connectionUP = NO;
-    
-    if( (flags & testcase) == testcase )
-        connectionUP = NO;
-    
-#if	TARGET_OS_IPHONE
-    if(flags & kSCNetworkReachabilityFlagsIsWWAN)
-    {
-        // We're on 3G.
-        if(!self.reachableOnWWAN)
-        {
-            // We don't want to connect when on 3G.
-            connectionUP = NO;
-        }
-    }
-#endif
-    
-    return connectionUP;
-}
+#pragma mark - Start and stop notifier
 
--(BOOL)isReachable
+- (BOOL)startNotifier
 {
-    SCNetworkReachabilityFlags flags;
-    
-    if(!SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
-        return NO;
-    
-    return [self isReachableWithFlags:flags];
-}
+	BOOL returnValue = NO;
+	SCNetworkReachabilityContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
 
--(BOOL)isReachableViaWWAN
-{
-#if	TARGET_OS_IPHONE
+	if (SCNetworkReachabilitySetCallback(_reachabilityRef, ReachabilityCallback, &context))
+	{
+		if (SCNetworkReachabilityScheduleWithRunLoop(_reachabilityRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode))
+		{
+			returnValue = YES;
+		}
+	}
     
-    SCNetworkReachabilityFlags flags = 0;
-    
-    if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
-    {
-        // Check we're REACHABLE
-        if(flags & kSCNetworkReachabilityFlagsReachable)
-        {
-            // Now, check we're on WWAN
-            if(flags & kSCNetworkReachabilityFlagsIsWWAN)
-            {
-                return YES;
-            }
-        }
-    }
-#endif
-    
-    return NO;
+	return returnValue;
 }
 
--(BOOL)isReachableViaWiFi
+
+- (void)stopNotifier
 {
-    SCNetworkReachabilityFlags flags = 0;
-    
-    if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
-    {
-        // Check we're reachable
-        if((flags & kSCNetworkReachabilityFlagsReachable))
-        {
-#if	TARGET_OS_IPHONE
-            // Check we're NOT on WWAN
-            if((flags & kSCNetworkReachabilityFlagsIsWWAN))
-            {
-                return NO;
-            }
-#endif
-            return YES;
-        }
-    }
-    
-    return NO;
+	if (_reachabilityRef != NULL)
+	{
+		SCNetworkReachabilityUnscheduleFromRunLoop(_reachabilityRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
+	}
 }
 
 
-// WWAN may be available, but not active until a connection has been established.
-// WiFi may require a connection for VPN on Demand.
--(BOOL)isConnectionRequired
+- (void)dealloc
 {
-    return [self connectionRequired];
+	[self stopNotifier];
+	if (_reachabilityRef != NULL)
+	{
+		CFRelease(_reachabilityRef);
+	}
 }
 
--(BOOL)connectionRequired
-{
-    SCNetworkReachabilityFlags flags;
-    
-    if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
-    {
-        return (flags & kSCNetworkReachabilityFlagsConnectionRequired);
-    }
-    
-    return NO;
-}
 
-// Dynamic, on demand connection?
--(BOOL)isConnectionOnDemand
-{
-    SCNetworkReachabilityFlags flags;
-    
-    if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
-    {
-        return ((flags & kSCNetworkReachabilityFlagsConnectionRequired) &&
-                (flags & (kSCNetworkReachabilityFlagsConnectionOnTraffic | kSCNetworkReachabilityFlagsConnectionOnDemand)));
-    }
-    
-    return NO;
-}
+#pragma mark - Network Flag Handling
 
-// Is user intervention required?
--(BOOL)isInterventionRequired
+- (NetworkStatus)networkStatusForFlags:(SCNetworkReachabilityFlags)flags
 {
-    SCNetworkReachabilityFlags flags;
-    
-    if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
-    {
-        return ((flags & kSCNetworkReachabilityFlagsConnectionRequired) &&
-                (flags & kSCNetworkReachabilityFlagsInterventionRequired));
-    }
-    
-    return NO;
-}
+	PrintReachabilityFlags(flags, "networkStatusForFlags");
+	if ((flags & kSCNetworkReachabilityFlagsReachable) == 0)
+	{
+		// The target host is not reachable.
+		return NotReachable;
+	}
 
+    NetworkStatus returnValue = NotReachable;
 
-#pragma mark - reachability status stuff
+	if ((flags & kSCNetworkReachabilityFlagsConnectionRequired) == 0)
+	{
+		/*
+         If the target host is reachable and no connection is required then we'll assume (for now) that you're on Wi-Fi...
+         */
+		returnValue = ReachableViaWiFi;
+	}
 
--(NetworkStatus)currentReachabilityStatus
-{
-    if([self isReachable])
-    {
-        if([self isReachableViaWiFi])
-            return ReachableViaWiFi;
-        
-#if	TARGET_OS_IPHONE
-        return ReachableViaWWAN;
-#endif
-    }
-    
-    return NotReachable;
-}
+	if ((((flags & kSCNetworkReachabilityFlagsConnectionOnDemand ) != 0) ||
+        (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) != 0))
+	{
+        /*
+         ... and the connection is on-demand (or on-traffic) if the calling application is using the CFSocketStream or higher APIs...
+         */
 
--(SCNetworkReachabilityFlags)reachabilityFlags
-{
-    SCNetworkReachabilityFlags flags = 0;
-    
-    if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
-    {
-        return flags;
+        if ((flags & kSCNetworkReachabilityFlagsInterventionRequired) == 0)
+        {
+            /*
+             ... and no [user] intervention is needed...
+             */
+            returnValue = ReachableViaWiFi;
+        }
     }
-    
-    return 0;
-}
 
--(NSString*)currentReachabilityString
-{
-    NetworkStatus temp = [self currentReachabilityStatus];
+	if ((flags & kSCNetworkReachabilityFlagsIsWWAN) == kSCNetworkReachabilityFlagsIsWWAN)
+	{
+		/*
+         ... but WWAN connections are OK if the calling application is using the CFNetwork APIs.
+         */
+		returnValue = ReachableViaWWAN;
+	}
     
-    if(temp == ReachableViaWWAN)
-    {
-        // Updated for the fact that we have CDMA phones now!
-        return NSLocalizedString(@"Cellular", @"");
-    }
-    if (temp == ReachableViaWiFi)
-    {
-        return NSLocalizedString(@"WiFi", @"");
-    }
-    
-    return NSLocalizedString(@"No Connection", @"");
+	return returnValue;
 }
 
--(NSString*)currentReachabilityFlags
+
+- (BOOL)connectionRequired
 {
-    return reachabilityFlags([self reachabilityFlags]);
+	NSAssert(_reachabilityRef != NULL, @"connectionRequired called with NULL reachabilityRef");
+	SCNetworkReachabilityFlags flags;
+
+	if (SCNetworkReachabilityGetFlags(_reachabilityRef, &flags))
+	{
+		return (flags & kSCNetworkReachabilityFlagsConnectionRequired);
+	}
+
+    return NO;
 }
 
-#pragma mark - Callback function calls this method
 
--(void)reachabilityChanged:(SCNetworkReachabilityFlags)flags
+- (NetworkStatus)currentReachabilityStatus
 {
-    if([self isReachableWithFlags:flags])
-    {
-        if(self.reachableBlock)
-        {
-            self.reachableBlock(self);
-        }
-    }
-    else
-    {
-        if(self.unreachableBlock)
-        {
-            self.unreachableBlock(self);
-        }
-    }
+	NSAssert(_reachabilityRef != NULL, @"currentNetworkStatus called with NULL SCNetworkReachabilityRef");
+	NetworkStatus returnValue = NotReachable;
+	SCNetworkReachabilityFlags flags;
     
-    if(self.reachabilityBlock)
-    {
-        self.reachabilityBlock(self, flags);
-    }
+	if (SCNetworkReachabilityGetFlags(_reachabilityRef, &flags))
+	{
+        returnValue = [self networkStatusForFlags:flags];
+	}
     
-    // this makes sure the change notification happens on the MAIN THREAD
-    dispatch_async(dispatch_get_main_queue(), ^{
-        [[NSNotificationCenter defaultCenter] postNotificationName:kReachabilityChangedNotification
-                                                            object:self];
-    });
+	return returnValue;
 }
 
-#pragma mark - Debug Description
-
-- (NSString *) description
-{
-    NSString *description = [NSString stringWithFormat:@"<%@: %#x (%@)>",
-                             NSStringFromClass([self class]), (unsigned int) self, [self currentReachabilityFlags]];
-    return description;
-}
 
 @end

+ 72 - 0
MobileLibrary/iOS/PsiphonTunnel/scripts/strip-frameworks.sh

@@ -0,0 +1,72 @@
+################################################################################
+#
+# Copyright 2015 Realm Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+################################################################################
+
+# This script strips all non-valid architectures from dynamic libraries in
+# the application's `Frameworks` directory.
+#
+# The following environment variables are required:
+#
+# BUILT_PRODUCTS_DIR
+# FRAMEWORKS_FOLDER_PATH
+# VALID_ARCHS
+# EXPANDED_CODE_SIGN_IDENTITY
+
+
+# Signs a framework with the provided identity
+code_sign() {
+  # Use the current code_sign_identitiy
+  echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}"
+  echo "/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --preserve-metadata=identifier,entitlements $1"
+  /usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --preserve-metadata=identifier,entitlements "$1"
+}
+
+# Set working directory to product’s embedded frameworks 
+cd "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}"
+
+if [ "$ACTION" = "install" ]; then
+  echo "Copy .bcsymbolmap files to .xcarchive"
+  find . -name '*.bcsymbolmap' -type f -exec mv {} "${CONFIGURATION_BUILD_DIR}" \;
+else
+  # Delete *.bcsymbolmap files from framework bundle unless archiving
+  find . -name '*.bcsymbolmap' -type f -exec rm -rf "{}" +\;
+fi
+
+echo "Stripping frameworks"
+
+for file in $(find . -type f -perm +111); do
+  # Skip non-dynamic libraries
+  if ! [[ "$(file "$file")" == *"dynamically linked shared library"* ]]; then
+    continue
+  fi
+  # Get architectures for current file
+  archs="$(lipo -info "${file}" | rev | cut -d ':' -f1 | rev)"
+  stripped=""
+  for arch in $archs; do
+    if ! [[ "${VALID_ARCHS}" == *"$arch"* ]]; then
+      # Strip non-valid architectures in-place
+      lipo -remove "$arch" -output "$file" "$file" || exit 1
+      stripped="$stripped $arch"
+    fi
+  done
+  if [[ "$stripped" != "" ]]; then
+    echo "Stripped $file of architectures:$stripped"
+    if [ "${CODE_SIGNING_REQUIRED}" == "YES" ]; then
+      code_sign "${file}"
+    fi
+  fi
+done

+ 17 - 0
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest.xcodeproj/project.pbxproj

@@ -157,6 +157,7 @@
 				662658E71DCB8CF300872F6C /* Frameworks */,
 				662658E81DCB8CF300872F6C /* Resources */,
 				662659221DCBC8CB00872F6C /* CopyFiles */,
+				6685BDD71E300A7800F0E414 /* ShellScript */,
 			);
 			buildRules = (
 			);
@@ -280,6 +281,22 @@
 		};
 /* End PBXResourcesBuildPhase section */
 
+/* Begin PBXShellScriptBuildPhase section */
+		6685BDD71E300A7800F0E414 /* ShellScript */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "bash \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/PsiphonTunnel.framework/strip-frameworks.sh\"";
+		};
+/* End PBXShellScriptBuildPhase section */
+
 /* Begin PBXSourcesBuildPhase section */
 		662658E61DCB8CF300872F6C /* Sources */ = {
 			isa = PBXSourcesBuildPhase;

+ 55 - 11
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/Contents.json

@@ -1,63 +1,105 @@
 {
   "images" : [
     {
+      "size" : "20x20",
       "idiom" : "iphone",
-      "size" : "29x29",
+      "filename" : "iphone-notification-20pt@2x.png",
       "scale" : "2x"
     },
     {
+      "size" : "20x20",
       "idiom" : "iphone",
-      "size" : "29x29",
+      "filename" : "iphone-notification-20pt@3x.png",
       "scale" : "3x"
     },
     {
+      "size" : "29x29",
       "idiom" : "iphone",
-      "size" : "40x40",
+      "filename" : "iphone-spotlight-settings-29pt@2x.png",
       "scale" : "2x"
     },
     {
+      "size" : "29x29",
       "idiom" : "iphone",
-      "size" : "40x40",
+      "filename" : "iphone-spotlight-settings-29pt@3x.png",
       "scale" : "3x"
     },
     {
+      "size" : "40x40",
       "idiom" : "iphone",
-      "size" : "60x60",
+      "filename" : "iphone-spotlight-40pt@2x.png",
       "scale" : "2x"
     },
     {
+      "size" : "40x40",
+      "idiom" : "iphone",
+      "filename" : "iphone-spotlight-40pt@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "60x60",
       "idiom" : "iphone",
+      "filename" : "iphone-app-60pt@2x.png",
+      "scale" : "2x"
+    },
+    {
       "size" : "60x60",
+      "idiom" : "iphone",
+      "filename" : "iphone-app-60pt@3x.png",
       "scale" : "3x"
     },
     {
+      "size" : "20x20",
       "idiom" : "ipad",
-      "size" : "29x29",
+      "filename" : "ipad-notifications-20pt@1x.png",
       "scale" : "1x"
     },
     {
+      "size" : "20x20",
       "idiom" : "ipad",
-      "size" : "29x29",
+      "filename" : "ipad-notifications-20pt@2x.png",
       "scale" : "2x"
     },
     {
+      "size" : "29x29",
       "idiom" : "ipad",
-      "size" : "40x40",
+      "filename" : "ipad-settings-29pt@1x.png",
       "scale" : "1x"
     },
     {
+      "size" : "29x29",
       "idiom" : "ipad",
-      "size" : "40x40",
+      "filename" : "ipad-settings-29pt@2x.png",
       "scale" : "2x"
     },
     {
+      "size" : "40x40",
       "idiom" : "ipad",
-      "size" : "76x76",
+      "filename" : "ipad-spotlight-40pt@1x.png",
       "scale" : "1x"
     },
     {
+      "size" : "40x40",
+      "idiom" : "ipad",
+      "filename" : "ipad-spotlight-40pt@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "76x76",
       "idiom" : "ipad",
+      "filename" : "ipad-app-76pt@1x.png",
+      "scale" : "1x"
+    },
+    {
       "size" : "76x76",
+      "idiom" : "ipad",
+      "filename" : "ipad-app-76pt@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "83.5x83.5",
+      "idiom" : "ipad",
+      "filename" : "ipad-pro-app-83.5pt@2x.png",
       "scale" : "2x"
     }
   ],
@@ -65,4 +107,6 @@
     "version" : 1,
     "author" : "xcode"
   }
-}
+}
+
+

BIN
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/ipad-app-76pt@1x.png


BIN
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/ipad-app-76pt@2x.png


BIN
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/ipad-notifications-20pt@1x.png


BIN
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/ipad-notifications-20pt@2x.png


BIN
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/ipad-pro-app-83.5pt@2x.png


BIN
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/ipad-settings-29pt@1x.png


BIN
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/ipad-settings-29pt@2x.png


BIN
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/ipad-spotlight-40pt@1x.png


BIN
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/ipad-spotlight-40pt@2x.png


BIN
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/iphone-app-60pt@2x.png


BIN
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/iphone-app-60pt@3x.png


BIN
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/iphone-notification-20pt@2x.png


BIN
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/iphone-notification-20pt@3x.png


BIN
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/iphone-spotlight-40pt@2x.png


BIN
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/iphone-spotlight-40pt@3x.png


BIN
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/iphone-spotlight-settings-29pt@2x.png


BIN
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/iphone-spotlight-settings-29pt@3x.png


+ 2 - 0
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Info.plist

@@ -18,6 +18,8 @@
 	<string>1.0</string>
 	<key>CFBundleVersion</key>
 	<string>1</string>
+	<key>ITSAppUsesNonExemptEncryption</key>
+	<false/>
 	<key>LSRequiresIPhoneOS</key>
 	<true/>
 	<key>UILaunchStoryboardName</key>

+ 13 - 1
MobileLibrary/iOS/USAGE.md

@@ -9,6 +9,10 @@ blocked by censors.
 The Psiphon Library is available as a `.framework` that can be easily included
 in your project using these instructions.
 
+## Using the Psiphon network
+
+In order to use a Psiphon library over the Psiphon network, you need to contact Psiphon to obtain connection parameters to use with your application. Please email us at [info@psiphon.ca](mailto:info@psiphon.ca).
+
 ## Using the Library in your App
 
 **First step:** Review the sample app, located under `SampleApps`.
@@ -28,7 +32,15 @@ This code is a canonical guide for integrating the Library.
 
 5. In the "Build Settings" for the target, click the `+` at the top, then "Add User-Defined Setting". Name the new setting `STRIP_BITCODE_FROM_COPIED_FILES` and set it to `NO`.
 
-6. In target Build Phases, add a "Copy Files" phase. Set "Destination" to "Frameworks". Add `PsiphonTunnel.framework` to the list. Ensure "Code Sign on Copy" is checked.
+6. In the "Build Phases" for the target, add a "Copy Files" phase. Set "Destination" to "Frameworks". Add `PsiphonTunnel.framework` to the list. Ensure "Code Sign on Copy" is checked.
+
+7. In the "Build Phases" for the target, add a "Run Script" phase. Set the script contents to:
+
+   ```no-highlight
+   bash "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/PsiphonTunnel.framework/strip-frameworks.sh"
+   ```
+
+   This step is required to work around an [App Store submission validation bug](http://www.openradar.me/23681704) that rejects apps containing a framework with simulator slices.
 
 ## Compiling and testing
 

+ 22 - 38
MobileLibrary/iOS/build-psiphon-framework.sh

@@ -1,5 +1,15 @@
 #!/usr/bin/env bash
 
+# This script takes one optional argument: 'private', if private plugins should
+# be used. It should be omitted if private plugins are not desired.
+if [[ $1 == "private" ]]; then
+  FORCE_PRIVATE_PLUGINS=true
+  echo "TRUE"
+else
+  FORCE_PRIVATE_PLUGINS=false
+  echo "FALSE"
+fi
+
 # -x echos commands. -u exits if an unintialized variable is used.
 # -e exits if a command returns an error.
 set -x -u -e
@@ -31,6 +41,7 @@ FRAMEWORK_BINARY="${INTERMEDIATE_OUPUT_DIR}/${INTERMEDIATE_OUPUT_FILE}/Versions/
 # The "OPENSSL" tag enables support of OpenSSL for use by IndistinguishableTLS.
 
 PRIVATE_PLUGINS_TAG=""
+if [[ ${FORCE_PRIVATE_PLUGINS} == true ]]; then PRIVATE_PLUGINS_TAG="PRIVATE_PLUGINS"; fi
 BUILD_TAGS="OPENSSL IOS ${PRIVATE_PLUGINS_TAG}"
 
 LIBSSL=${BASE_DIR}/OpenSSL-for-iPhone/lib/libssl.a
@@ -55,10 +66,9 @@ export PATH=${GOPATH}/bin:${PATH}
 rm -rf ${GOPATH}
 
 # When updating the pinned rev, you will have to manually delete go-ios-build
-GOMOBILE_PINNED_REV=72eef9d09307f0b437153fd152229f56edc0ab20
+GOMOBILE_PINNED_REV=a0f998b2d8c7ee81ddbead9202dd5e0184a998ad
 GOMOBILE_PATH=${GOPATH}/src/golang.org/x/mobile/cmd/gomobile
 
-IOS_SRC_DIR=${GOPATH}/src/github.com/Psiphon-Labs/psiphon-ios
 TUNNEL_CORE_SRC_DIR=${GOPATH}/src/github.com/Psiphon-Labs/psiphon-tunnel-core
 OPENSSL_SRC_DIR=${GOPATH}/src/github.com/Psiphon-Inc/openssl
 
@@ -89,16 +99,6 @@ if [[ $? != 0 ]]; then
   exit 1
 fi
 
-if [ ! -e ${IOS_SRC_DIR} ]; then
-  echo "iOS source directory (${IOS_SRC_DIR}) not found, creating link"
-  mkdir -p $(dirname ${IOS_SRC_DIR})
-  ln -s $(pwd -P) $IOS_SRC_DIR
-  if [[ $? != 0 ]]; then
-    echo "..Could not create symlink, aborting"
-    exit 1
-  fi
-fi
-
 # arg: binary_path
 function strip_architectures() {
   valid_archs="${VALID_IOS_ARCHS} ${VALID_SIMULATOR_ARCHS}"
@@ -143,29 +143,6 @@ cd ${GOPATH}/src/golang.org/x/mobile/cmd/gomobile
 git checkout master
 git checkout -b pinned ${GOMOBILE_PINNED_REV}
 
-# Gomobile messes up the build tags by quoting them incorrectly. We'll hack a fix for it.
-# First do a grep to see if this code is still there (or has been fixed upstream).
-grep -q 'strconv.Quote' ./build.go 
-if [[ $? != 0 ]]; then
-  echo "Upstream gomobile code has changed, breaking hacks."
-  exit 1
-fi
-# Then do the hack-fix-replacement.
-perl -i -pe 's/"-tags="\+strconv\.Quote\(strings.Join\(ctx\.BuildTags, ","\)\),/"-tags",strings.Join(ctx.BuildTags, " "),/g' ./build.go
-# We also need to remove the now-unused strconv import.
-perl -i -pe 's/"strconv"//g' ./build.go
-
-# Gomobile's iOS code puts an *additional* build tags flag at the end of the command line. This
-# overrides any existing build tags and messes up our builds. So we'll hack a fix for that, too.
-# First do a grep to see if this code is still there (or has been fixed upstream).
-grep -q '"-tags=ios",' ./bind_iosapp.go 
-if [[ $? != 0 ]]; then
-  echo "Upstream gomobile code has changed, breaking hacks."
-  exit 1
-fi
-# Then do the hack-fix-replacement.
-perl -i -pe 's/(.+)"-tags=ios",(.+)/\tctx.BuildTags = append(ctx.BuildTags, "ios")\n\1\2/g' ./bind_iosapp.go
-
 go install
 ${GOPATH}/bin/gomobile init -v
 if [[ $? != 0 ]]; then
@@ -212,7 +189,8 @@ 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 -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
+# We're using a generated-code prefix to workaround https://github.com/golang/go/issues/18693
+${GOPATH}/bin/gomobile bind -v -x -target ios -prefix Go -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 -tags="${BUILD_TAGS}" -ldflags="${LDFLAGS}" -o "${INTERMEDIATE_OUPUT_DIR}/${INTERMEDIATE_OUPUT_FILE}" github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/psi"
   exit $rc
@@ -229,14 +207,14 @@ rm -rf "${BUILD_DIR}"
 rm -rf "${BUILD_DIR}-SIMULATOR"
 
 # Build the outer framework for phones...
-xcodebuild clean build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO -configuration Release -sdk iphoneos ONLY_ACTIVE_ARCH=NO -project ${UMBRELLA_FRAMEWORK_XCODE_PROJECT} CONFIGURATION_BUILD_DIR="${BUILD_DIR}"
+xcodebuild clean build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGN_ENTITLEMENTS="" CODE_SIGNING_ALLOWED="NO" -configuration Release -sdk iphoneos ONLY_ACTIVE_ARCH=NO -project ${UMBRELLA_FRAMEWORK_XCODE_PROJECT} CONFIGURATION_BUILD_DIR="${BUILD_DIR}"
 rc=$?; if [[ $rc != 0 ]]; then
   echo "FAILURE: xcodebuild iphoneos"
   exit $rc
 fi
 
 # ...and for the simulator.
-xcodebuild clean build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO -configuration Release -sdk iphonesimulator ARCHS=x86_64 VALID_ARCHS=x86_64 ONLY_ACTIVE_ARCH=NO -project ${UMBRELLA_FRAMEWORK_XCODE_PROJECT} CONFIGURATION_BUILD_DIR="${BUILD_DIR}-SIMULATOR"
+xcodebuild clean build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGN_ENTITLEMENTS="" CODE_SIGNING_ALLOWED="NO" -configuration Release -sdk iphonesimulator ARCHS=x86_64 VALID_ARCHS=x86_64 ONLY_ACTIVE_ARCH=NO -project ${UMBRELLA_FRAMEWORK_XCODE_PROJECT} CONFIGURATION_BUILD_DIR="${BUILD_DIR}-SIMULATOR"
 rc=$?; if [[ $rc != 0 ]]; then
   echo "FAILURE: xcodebuild iphonesimulator"
   exit $rc
@@ -252,4 +230,10 @@ fi
 # Delete the temporary simulator build files.
 rm -rf "${BUILD_DIR}-SIMULATOR"
 
+# Jenkins loses symlinks from the framework directory, which results in a build
+# artifact that is invalid to use in an App Store app. Instead, we will zip the
+# resulting build and use that as the artifact.
+cd "${BUILD_DIR}"
+zip --recurse-paths --symlinks build.zip * --exclude "*.DS_Store"
+
 echo "BUILD DONE"

+ 6 - 1
MobileLibrary/psi/psi.go

@@ -37,6 +37,7 @@ type PsiphonProvider interface {
 	Notice(noticeJSON string)
 	HasNetworkConnectivity() int
 	BindToDevice(fileDescriptor int) error
+	IPv6Synthesize(IPv4Addr string) string
 	GetPrimaryDnsServer() string
 	GetSecondaryDnsServer() string
 }
@@ -49,7 +50,7 @@ var controllerWaitGroup *sync.WaitGroup
 func Start(
 	configJson, embeddedServerEntryList string,
 	provider PsiphonProvider,
-	useDeviceBinder bool) error {
+	useDeviceBinder bool, useIPv6Synthesizer bool) error {
 
 	controllerMutex.Lock()
 	defer controllerMutex.Unlock()
@@ -69,6 +70,10 @@ func Start(
 		config.DnsServerGetter = provider
 	}
 
+	if useIPv6Synthesizer {
+		config.IPv6Synthesizer = provider
+	}
+
 	psiphon.SetNoticeOutput(psiphon.NewNoticeReceiver(
 		func(notice []byte) {
 			provider.Notice(string(notice))

+ 1 - 1
Server/make.bash

@@ -57,7 +57,7 @@ build_for_linux () {
     exit $?
   fi
 
-  GOOS=linux GOARCH=amd64 go build -tags "${BUILD_TAGS}" -ldflags "$LDFLAGS" -o psiphond
+  GOOS=linux GOARCH=amd64 go build -v -x -tags "${BUILD_TAGS}" -ldflags "$LDFLAGS" -o psiphond
   if [ $? != 0 ]; then
     echo "...'go build' failed, exiting"
     exit $?

+ 30 - 12
psiphon/LookupIP.go

@@ -69,7 +69,28 @@ func bindLookupIP(host, dnsServer string, config *DialConfig) (addrs []net.IP, e
 		return []net.IP{ipAddr}, nil
 	}
 
-	socketFd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, 0)
+	// config.DnsServerGetter.GetDnsServers() must return IP addresses
+	ipAddr = net.ParseIP(dnsServer)
+	if ipAddr == nil {
+		return nil, common.ContextError(errors.New("invalid IP address"))
+	}
+
+	var ipv4 [4]byte
+	var ipv6 [16]byte
+	var domain int
+
+	// Get address type (IPv4 or IPv6)
+	if ipAddr.To4() != nil {
+		copy(ipv4[:], ipAddr.To4())
+		domain = syscall.AF_INET
+	} else if ipAddr.To16() != nil {
+		copy(ipv6[:], ipAddr.To16())
+		domain = syscall.AF_INET6
+	} else {
+		return nil, common.ContextError(fmt.Errorf("Got invalid IP address for dns server: %s", ipAddr.String()))
+	}
+
+	socketFd, err := syscall.Socket(domain, syscall.SOCK_DGRAM, 0)
 	if err != nil {
 		return nil, common.ContextError(err)
 	}
@@ -80,18 +101,15 @@ func bindLookupIP(host, dnsServer string, config *DialConfig) (addrs []net.IP, e
 		return nil, common.ContextError(fmt.Errorf("BindToDevice failed: %s", err))
 	}
 
-	// config.DnsServerGetter.GetDnsServers() must return IP addresses
-	ipAddr = net.ParseIP(dnsServer)
-	if ipAddr == nil {
-		return nil, common.ContextError(errors.New("invalid IP address"))
-	}
-
-	// TODO: IPv6 support
-	var ip [4]byte
-	copy(ip[:], ipAddr.To4())
-	sockAddr := syscall.SockaddrInet4{Addr: ip, Port: DNS_PORT}
+	// Connect socket to the server's IP address
 	// Note: no timeout or interrupt for this connect, as it's a datagram socket
-	err = syscall.Connect(socketFd, &sockAddr)
+	if domain == syscall.AF_INET {
+		sockAddr := syscall.SockaddrInet4{Addr: ipv4, Port: DNS_PORT}
+		err = syscall.Connect(socketFd, &sockAddr)
+	} else if domain == syscall.AF_INET6 {
+		sockAddr := syscall.SockaddrInet6{Addr: ipv6, Port: DNS_PORT}
+		err = syscall.Connect(socketFd, &sockAddr)
+	}
 	if err != nil {
 		return nil, common.ContextError(err)
 	}

+ 24 - 0
psiphon/TCPConn.go

@@ -104,6 +104,30 @@ func interruptibleTCPDial(addr string, config *DialConfig) (*TCPConn, error) {
 	// when tcpDial, amoung other things, when makes a blocking syscall.Connect()
 	// call.
 	go func() {
+		if config.IPv6Synthesizer != nil {
+			// Synthesize an IPv6 address from an IPv4 one
+			// This is for compatibility on DNS64/NAT64 networks
+			host, port, err := net.SplitHostPort(addr)
+			if err != nil {
+				select {
+				case conn.dialResult <- err:
+				default:
+				}
+				return
+			}
+			ip := net.ParseIP(host)
+			if ip != nil && ip.To4() != nil {
+				synthesizedAddr := config.IPv6Synthesizer.IPv6Synthesize(host)
+				// If IPv6Synthesize fails we will try dialing with the
+				// original IPv4 address instead of logging an error. If
+				// the address is unreachable an error will be emitted
+				// from tcpDial.
+				if synthesizedAddr != "" {
+					addr = net.JoinHostPort(synthesizedAddr, port)
+				}
+			}
+		}
+
 		var netConn net.Conn
 		var err error
 		if config.UpstreamProxyUrl != "" {

+ 24 - 6
psiphon/TCPConn_bind.go

@@ -78,12 +78,24 @@ func tcpDial(addr string, config *DialConfig, dialResult chan error) (net.Conn,
 		return nil, common.ContextError(err)
 	}
 
-	// TODO: IPv6 support
-	var ip [4]byte
-	copy(ip[:], ipAddrs[index].To4())
+	var ipv4 [4]byte
+	var ipv6 [16]byte
+	var domain int
+	ipAddr := ipAddrs[index]
+
+	// Get address type (IPv4 or IPv6)
+	if ipAddr != nil && ipAddr.To4() != nil {
+		copy(ipv4[:], ipAddr.To4())
+		domain = syscall.AF_INET
+	} else if ipAddr != nil && ipAddr.To16() != nil {
+		copy(ipv6[:], ipAddr.To16())
+		domain = syscall.AF_INET6
+	} else {
+		return nil, common.ContextError(fmt.Errorf("Got invalid IP address: %s", ipAddr.String()))
+	}
 
 	// Create a socket and bind to device, when configured to do so
-	socketFd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0)
+	socketFd, err := syscall.Socket(domain, syscall.SOCK_STREAM, 0)
 	if err != nil {
 		return nil, common.ContextError(err)
 	}
@@ -100,8 +112,14 @@ func tcpDial(addr string, config *DialConfig, dialResult chan error) (net.Conn,
 		}
 	}
 
-	sockAddr := syscall.SockaddrInet4{Addr: ip, Port: port}
-	err = syscall.Connect(socketFd, &sockAddr)
+	// Connect socket to the server's IP address
+	if domain == syscall.AF_INET {
+		sockAddr := syscall.SockaddrInet4{Addr: ipv4, Port: port}
+		err = syscall.Connect(socketFd, &sockAddr)
+	} else if domain == syscall.AF_INET6 {
+		sockAddr := syscall.SockaddrInet6{Addr: ipv6, Port: port}
+		err = syscall.Connect(socketFd, &sockAddr)
+	}
 	if err != nil {
 		syscall.Close(socketFd)
 		return nil, common.ContextError(err)

+ 10 - 29
psiphon/common/osl/osl.go

@@ -30,8 +30,6 @@
 package osl
 
 import (
-	"bytes"
-	"compress/zlib"
 	"crypto/hmac"
 	"crypto/md5"
 	"crypto/sha256"
@@ -41,7 +39,6 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"io/ioutil"
 	"net"
 	"net/url"
 	"path"
@@ -705,7 +702,8 @@ func (config *Config) Pave(
 	propagationChannelID string,
 	signingPublicKey string,
 	signingPrivateKey string,
-	paveServerEntries []map[time.Time]string) ([]*PaveFile, error) {
+	paveServerEntries []map[time.Time]string,
+	logCallback func(int, time.Time, string)) ([]*PaveFile, error) {
 
 	config.ReloadableFile.RLock()
 	defer config.ReloadableFile.RUnlock()
@@ -749,7 +747,7 @@ func (config *Config) Pave(
 						return nil, common.ContextError(err)
 					}
 
-					boxedServerEntries, err := box(fileKey, compress(signedServerEntries))
+					boxedServerEntries, err := box(fileKey, common.Compress(signedServerEntries))
 					if err != nil {
 						return nil, common.ContextError(err)
 					}
@@ -764,6 +762,10 @@ func (config *Config) Pave(
 						Name:     fileName,
 						Contents: boxedServerEntries,
 					})
+
+					if logCallback != nil {
+						logCallback(schemeIndex, oslTime, fileName)
+					}
 				}
 
 				oslTime = oslTime.Add(
@@ -788,7 +790,7 @@ func (config *Config) Pave(
 
 	paveFiles = append(paveFiles, &PaveFile{
 		Name:     REGISTRY_FILENAME,
-		Contents: compress(signedRegistry),
+		Contents: common.Compress(signedRegistry),
 	})
 
 	return paveFiles, nil
@@ -986,7 +988,7 @@ func GetOSLFilename(baseDirectory string, oslID []byte) string {
 func UnpackRegistry(
 	compressedRegistry []byte, signingPublicKey string) (*Registry, []byte, error) {
 
-	packagedRegistry, err := uncompress(compressedRegistry)
+	packagedRegistry, err := common.Decompress(compressedRegistry)
 	if err != nil {
 		return nil, nil, common.ContextError(err)
 	}
@@ -1168,7 +1170,7 @@ func (registry *Registry) UnpackOSL(
 		return "", common.ContextError(err)
 	}
 
-	dataPackage, err := uncompress(decryptedContents)
+	dataPackage, err := common.Decompress(decryptedContents)
 	if err != nil {
 		return "", common.ContextError(err)
 	}
@@ -1278,24 +1280,3 @@ func unbox(key, box []byte) ([]byte, error) {
 	}
 	return plaintext, nil
 }
-
-func compress(data []byte) []byte {
-	var compressedData bytes.Buffer
-	writer := zlib.NewWriter(&compressedData)
-	writer.Write(data)
-	writer.Close()
-	return compressedData.Bytes()
-}
-
-func uncompress(data []byte) ([]byte, error) {
-	reader, err := zlib.NewReader(bytes.NewReader(data))
-	if err != nil {
-		return nil, common.ContextError(err)
-	}
-	uncompressedData, err := ioutil.ReadAll(reader)
-	reader.Close()
-	if err != nil {
-		return nil, common.ContextError(err)
-	}
-	return uncompressedData, nil
-}

+ 2 - 1
psiphon/common/osl/osl_test.go

@@ -350,7 +350,8 @@ func TestOSL(t *testing.T) {
 				propagationChannelID,
 				signingPublicKey,
 				signingPrivateKey,
-				paveServerEntries)
+				paveServerEntries,
+				nil)
 			if err != nil {
 				t.Fatalf("Pave failed: %s", err)
 			}

+ 2 - 3
psiphon/common/osl/paver/README.md

@@ -3,13 +3,12 @@
 Example usage:
 
 ```
-./paver -config osl_config.json -key signing_key.pem -count 3
+./paver -config osl_config.json -key signing_key.pem -offset -1h -period 2h
 ```
 
 * 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.
+* The example will pave all OSLs, for each propagation channel ID, within a 2 hour period starting 1 hour ago.
   * `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`.
-

+ 44 - 23
psiphon/common/osl/paver/main.go

@@ -38,14 +38,11 @@ func main() {
 	var configFilename string
 	flag.StringVar(&configFilename, "config", "", "OSL configuration file")
 
-	var scheme int
-	flag.IntVar(&scheme, "scheme", 0, "scheme to pave")
+	var offset time.Duration
+	flag.DurationVar(&offset, "offset", 0, "pave OSL start time (offset from now)")
 
-	var oslOffset int
-	flag.IntVar(&oslOffset, "offset", 0, "OSL offset")
-
-	var oslCount int
-	flag.IntVar(&oslCount, "count", 1, "OSL count")
+	var period time.Duration
+	flag.DurationVar(&period, "period", 0, "pave OSL total period (starting from offset)")
 
 	var signingKeyPairFilename string
 	flag.StringVar(&signingKeyPairFilename, "key", "", "signing public key pair")
@@ -67,11 +64,6 @@ func main() {
 		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)
@@ -105,28 +97,57 @@ func main() {
 	signingPublicKey := base64.StdEncoding.EncodeToString(publicKeyBytes)
 	signingPrivateKey := base64.StdEncoding.EncodeToString(privateKeyBytes)
 
-	slokTimePeriodsPerOSL := 1
-	for _, keySplit := range config.Schemes[scheme].SeedPeriodKeySplits {
-		slokTimePeriodsPerOSL *= keySplit.Total
+	paveTime := time.Now().UTC()
+	startTime := paveTime.Add(offset)
+	endTime := startTime.Add(period)
+
+	schemeOSLTimePeriods := make(map[int]time.Duration)
+	for index, scheme := range config.Schemes {
+		slokTimePeriodsPerOSL := 1
+		for _, keySplit := range scheme.SeedPeriodKeySplits {
+			slokTimePeriodsPerOSL *= keySplit.Total
+		}
+		schemeOSLTimePeriods[index] =
+			time.Duration(scheme.SeedPeriodNanoseconds * int64(slokTimePeriodsPerOSL))
 	}
-	oslTimePeriod := time.Duration(config.Schemes[scheme].SeedPeriodNanoseconds * int64(slokTimePeriodsPerOSL))
 
-	for _, propagationChannelID := range config.Schemes[scheme].PropagationChannelIDs {
+	allPropagationChannelIDs := make(map[string][]int)
+	for index, scheme := range config.Schemes {
+		for _, propagationChannelID := range scheme.PropagationChannelIDs {
+			allPropagationChannelIDs[propagationChannelID] =
+				append(allPropagationChannelIDs[propagationChannelID], index)
+		}
+	}
+
+	for propagationChannelID, schemeIndexes := range allPropagationChannelIDs {
 
 		paveServerEntries := make([]map[time.Time]string, len(config.Schemes))
-		paveServerEntries[0] = make(map[time.Time]string)
 
-		epoch, _ := time.Parse(time.RFC3339, config.Schemes[scheme].Epoch)
-		for i := oslOffset; i < oslOffset+oslCount; i++ {
-			paveServerEntries[0][epoch.Add(time.Duration(i)*oslTimePeriod)] = ""
+		for _, index := range schemeIndexes {
+
+			paveServerEntries[index] = make(map[time.Time]string)
+
+			oslTime, _ := time.Parse(time.RFC3339, config.Schemes[index].Epoch)
+			for !oslTime.After(endTime) {
+				if !oslTime.Before(startTime) {
+					paveServerEntries[index][oslTime] = ""
+				}
+				oslTime = oslTime.Add(schemeOSLTimePeriods[index])
+			}
+
+			fmt.Printf("Paving propagation channel %s, scheme #%d, [%s - %s], %s\n",
+				propagationChannelID, index, startTime, endTime, schemeOSLTimePeriods[index])
 		}
 
 		paveFiles, err := config.Pave(
-			epoch.Add(time.Duration(oslOffset+oslCount)*oslTimePeriod),
+			endTime,
 			propagationChannelID,
 			signingPublicKey,
 			signingPrivateKey,
-			paveServerEntries)
+			paveServerEntries,
+			func(schemeIndex int, oslTime time.Time, fileName string) {
+				fmt.Printf("\tPaved scheme %d %s: %s\n", schemeIndex, oslTime, fileName)
+			})
 		if err != nil {
 			fmt.Printf("failed paving: %s\n", err)
 			os.Exit(1)

+ 26 - 0
psiphon/common/utils.go

@@ -20,11 +20,14 @@
 package common
 
 import (
+	"bytes"
+	"compress/zlib"
 	"crypto/rand"
 	"encoding/base64"
 	"encoding/hex"
 	"errors"
 	"fmt"
+	"io/ioutil"
 	"math/big"
 	"runtime"
 	"strings"
@@ -176,3 +179,26 @@ func ContextError(err error) error {
 	pc, _, line, _ := runtime.Caller(1)
 	return fmt.Errorf("%s#%d: %s", getFunctionName(pc), line, err)
 }
+
+// Compress returns zlib compressed data
+func Compress(data []byte) []byte {
+	var compressedData bytes.Buffer
+	writer := zlib.NewWriter(&compressedData)
+	writer.Write(data)
+	writer.Close()
+	return compressedData.Bytes()
+}
+
+// Decompress returns zlib decompressed data
+func Decompress(data []byte) ([]byte, error) {
+	reader, err := zlib.NewReader(bytes.NewReader(data))
+	if err != nil {
+		return nil, ContextError(err)
+	}
+	uncompressedData, err := ioutil.ReadAll(reader)
+	reader.Close()
+	if err != nil {
+		return nil, ContextError(err)
+	}
+	return uncompressedData, nil
+}

+ 17 - 0
psiphon/common/utils_test.go

@@ -20,6 +20,7 @@
 package common
 
 import (
+	"bytes"
 	"testing"
 	"time"
 )
@@ -52,3 +53,19 @@ func TestMakeRandomPeriod(t *testing.T) {
 		t.Error("duration should have randomness difference between calls")
 	}
 }
+
+func TestCompress(t *testing.T) {
+
+	originalData := []byte("test data")
+
+	compressedData := Compress(originalData)
+
+	decompressedData, err := Decompress(compressedData)
+	if err != nil {
+		t.Error("Uncompress failed: %s", err)
+	}
+
+	if bytes.Compare(originalData, decompressedData) != 0 {
+		t.Error("decompressed data doesn't match original data")
+	}
+}

+ 173 - 10
psiphon/config.go

@@ -20,6 +20,7 @@
 package psiphon
 
 import (
+	"encoding/base64"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -120,8 +121,20 @@ type Config struct {
 	// 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
 
+	// 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 DownloadURL must have OnlyAfterAttempts = 0.
+	RemoteServerListURLs []*DownloadURL
+
 	// 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
@@ -138,8 +151,19 @@ type Config struct {
 	// 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
 
+	// 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 []*DownloadURL
+
 	// ObfuscatedServerListDownloadDirectory specifies a target directory for
 	// storing the obfuscated remote server list downloads. Data is stored in
 	// co-located files (<OSL filename>.part*) to allow for resumable
@@ -230,6 +254,11 @@ type Config struct {
 	// deployments.
 	DeviceBinder DeviceBinder
 
+	// IPv6Synthesizer is an interface that allows the core tunnel to call
+	// into the host application to synthesize IPv6 addresses from IPv4 ones. This
+	// is used to correctly lookup IPs on DNS64/NAT64 networks.
+	IPv6Synthesizer IPv6Synthesizer
+
 	// DnsServerGetter is an interface that enables the core tunnel to call
 	// into the host application to discover the native network DNS server settings.
 	// This parameter is only applicable to library deployments.
@@ -284,17 +313,30 @@ type Config struct {
 	// 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
 
+	// 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 []*DownloadURL
+
 	// UpgradeDownloadClientVersionHeader specifies the HTTP header name for the
-	// entity at UpgradeDownloadUrl which specifies the client version (an integer
+	// entity at UpgradeDownloadURLs which specifies the client version (an integer
 	// value). A HEAD request may be made to check the version number available at
-	// UpgradeDownloadUrl. UpgradeDownloadClientVersionHeader is required when
-	// UpgradeDownloadUrl is specified.
+	// UpgradeDownloadURLs. UpgradeDownloadClientVersionHeader is required when
+	// UpgradeDownloadURLs is specified.
 	UpgradeDownloadClientVersionHeader string
 
 	// UpgradeDownloadFilename is the local target filename for an upgrade download.
-	// This parameter is required when UpgradeDownloadUrl is specified.
+	// This parameter is required when UpgradeDownloadURLs is specified.
 	// Data is stored in co-located files (UpgradeDownloadFilename.part*) to allow
 	// for resumable downloading.
 	UpgradeDownloadFilename string
@@ -414,6 +456,26 @@ type Config struct {
 	EmitSLOKs bool
 }
 
+// DownloadURL specifies a URL for downloading resources along with parameters
+// for the download strategy.
+type DownloadURL struct {
+
+	// URL is the location of the resource. This string is slightly obfuscated
+	// with base64 encoding to mitigate trivial binary executable string scanning.
+	URL string
+
+	// SkipVerify indicates whether to verify HTTPS certificates. It some
+	// circumvention scenarios, verification is not possible. This must
+	// only be set to true when the resource its own verification mechanism.
+	SkipVerify bool
+
+	// OnlyAfterAttempts specifies how to schedule this URL when downloading
+	// the same resource (same entity, same ETag) from multiple different
+	// candidate locations. For a value of N, this URL is only a candidate
+	// after N rounds of attempting the download from other URLs.
+	OnlyAfterAttempts int
+}
+
 // LoadConfig parses and validates a JSON format Psiphon config JSON
 // string and returns a Config struct populated with config values.
 func LoadConfig(configJson []byte) (*Config, error) {
@@ -506,15 +568,37 @@ func LoadConfig(configJson []byte) (*Config, error) {
 			errors.New("invalid TargetApiProtocol"))
 	}
 
-	if config.UpgradeDownloadUrl != "" &&
-		(config.UpgradeDownloadClientVersionHeader == "" || config.UpgradeDownloadFilename == "") {
-		return nil, common.ContextError(errors.New(
-			"UpgradeDownloadUrl requires UpgradeDownloadClientVersionHeader and UpgradeDownloadFilename"))
+	if config.UpgradeDownloadUrl != "" && config.UpgradeDownloadURLs == nil {
+		config.UpgradeDownloadURLs = promoteLegacyDownloadURL(config.UpgradeDownloadUrl)
+	}
+
+	if config.UpgradeDownloadURLs != nil {
+
+		err := decodeAndValidateDownloadURLs("UpgradeDownloadURLs", config.UpgradeDownloadURLs)
+		if err != nil {
+			return nil, common.ContextError(err)
+		}
+
+		if config.UpgradeDownloadClientVersionHeader == "" {
+			return nil, common.ContextError(errors.New("missing UpgradeDownloadClientVersionHeader"))
+		}
+		if config.UpgradeDownloadFilename == "" {
+			return nil, common.ContextError(errors.New("missing UpgradeDownloadFilename"))
+		}
 	}
 
 	if !config.DisableRemoteServerListFetcher {
 
-		if config.RemoteServerListUrl != "" {
+		if config.RemoteServerListUrl != "" && config.RemoteServerListURLs == nil {
+			config.RemoteServerListURLs = promoteLegacyDownloadURL(config.RemoteServerListUrl)
+		}
+
+		if config.RemoteServerListURLs != nil {
+
+			err := decodeAndValidateDownloadURLs("RemoteServerListURLs", config.RemoteServerListURLs)
+			if err != nil {
+				return nil, common.ContextError(err)
+			}
 
 			if config.RemoteServerListSignaturePublicKey == "" {
 				return nil, common.ContextError(errors.New("missing RemoteServerListSignaturePublicKey"))
@@ -525,7 +609,16 @@ func LoadConfig(configJson []byte) (*Config, error) {
 			}
 		}
 
-		if config.ObfuscatedServerListRootURL != "" {
+		if config.ObfuscatedServerListRootURL != "" && config.ObfuscatedServerListRootURLs == nil {
+			config.ObfuscatedServerListRootURLs = promoteLegacyDownloadURL(config.ObfuscatedServerListRootURL)
+		}
+
+		if config.ObfuscatedServerListRootURLs != nil {
+
+			err := decodeAndValidateDownloadURLs("ObfuscatedServerListRootURLs", config.ObfuscatedServerListRootURLs)
+			if err != nil {
+				return nil, common.ContextError(err)
+			}
 
 			if config.RemoteServerListSignaturePublicKey == "" {
 				return nil, common.ContextError(errors.New("missing RemoteServerListSignaturePublicKey"))
@@ -594,3 +687,73 @@ func LoadConfig(configJson []byte) (*Config, error) {
 
 	return &config, nil
 }
+
+func promoteLegacyDownloadURL(URL string) []*DownloadURL {
+	downloadURLs := make([]*DownloadURL, 1)
+	downloadURLs[0] = &DownloadURL{
+		URL:               base64.StdEncoding.EncodeToString([]byte(URL)),
+		SkipVerify:        false,
+		OnlyAfterAttempts: 0,
+	}
+	return downloadURLs
+}
+
+func decodeAndValidateDownloadURLs(name string, downloadURLs []*DownloadURL) error {
+
+	hasOnlyAfterZero := false
+	for _, downloadURL := range downloadURLs {
+		if downloadURL.OnlyAfterAttempts == 0 {
+			hasOnlyAfterZero = true
+		}
+		decodedURL, err := base64.StdEncoding.DecodeString(downloadURL.URL)
+		if err != nil {
+			return fmt.Errorf("failed to decode URL in %s: %s", name, err)
+		}
+
+		downloadURL.URL = string(decodedURL)
+	}
+
+	var err error
+	if !hasOnlyAfterZero {
+		err = fmt.Errorf("must be at least one DownloadURL with OnlyAfterAttempts = 0 in %s", name)
+	}
+
+	return err
+}
+
+func selectDownloadURL(attempt int, downloadURLs []*DownloadURL) (string, string, bool) {
+
+	// The first OnlyAfterAttempts = 0 URL is the canonical URL. This
+	// is the value used as the key for SetUrlETag when multiple download
+	// URLs can be used to fetch a single entity.
+
+	canonicalURL := ""
+	for _, downloadURL := range downloadURLs {
+		if downloadURL.OnlyAfterAttempts == 0 {
+			canonicalURL = downloadURL.URL
+			break
+		}
+	}
+
+	candidates := make([]int, 0)
+	for index, URL := range downloadURLs {
+		if attempt >= URL.OnlyAfterAttempts {
+			candidates = append(candidates, index)
+		}
+	}
+
+	if len(candidates) < 1 {
+		// This case is not expected, as decodeAndValidateDownloadURLs
+		// should reject configs that would have no candidates for
+		// 0 attempts.
+		return "", "", true
+	}
+
+	selection, err := common.MakeSecureRandomInt(len(candidates))
+	if err != nil {
+		selection = 0
+	}
+	downloadURL := downloadURLs[candidates[selection]]
+
+	return downloadURL.URL, canonicalURL, downloadURL.SkipVerify
+}

+ 166 - 0
psiphon/config_test.go

@@ -20,6 +20,7 @@
 package psiphon
 
 import (
+	"encoding/base64"
 	"encoding/json"
 	"io/ioutil"
 	"strings"
@@ -157,3 +158,168 @@ func (suite *ConfigTestSuite) Test_LoadConfig_GoodJson() {
 	_, err = LoadConfig(testObjJSON)
 	suite.Nil(err, "JSON with null for optional values should succeed")
 }
+
+func TestDownloadURLs(t *testing.T) {
+
+	decodedA := "a.example.com"
+	encodedA := base64.StdEncoding.EncodeToString([]byte(decodedA))
+	encodedB := base64.StdEncoding.EncodeToString([]byte("b.example.com"))
+	encodedC := base64.StdEncoding.EncodeToString([]byte("c.example.com"))
+
+	testCases := []struct {
+		description                string
+		downloadURLs               []*DownloadURL
+		attempts                   int
+		expectedValid              bool
+		expectedCanonicalURL       string
+		expectedDistinctSelections int
+	}{
+		{
+			"missing OnlyAfterAttempts = 0",
+			[]*DownloadURL{
+				&DownloadURL{
+					URL:               encodedA,
+					OnlyAfterAttempts: 1,
+				},
+			},
+			1,
+			false,
+			decodedA,
+			0,
+		},
+		{
+			"single URL, multiple attempts",
+			[]*DownloadURL{
+				&DownloadURL{
+					URL:               encodedA,
+					OnlyAfterAttempts: 0,
+				},
+			},
+			2,
+			true,
+			decodedA,
+			1,
+		},
+		{
+			"multiple URLs, single attempt",
+			[]*DownloadURL{
+				&DownloadURL{
+					URL:               encodedA,
+					OnlyAfterAttempts: 0,
+				},
+				&DownloadURL{
+					URL:               encodedB,
+					OnlyAfterAttempts: 1,
+				},
+				&DownloadURL{
+					URL:               encodedC,
+					OnlyAfterAttempts: 1,
+				},
+			},
+			1,
+			true,
+			decodedA,
+			1,
+		},
+		{
+			"multiple URLs, multiple attempts",
+			[]*DownloadURL{
+				&DownloadURL{
+					URL:               encodedA,
+					OnlyAfterAttempts: 0,
+				},
+				&DownloadURL{
+					URL:               encodedB,
+					OnlyAfterAttempts: 1,
+				},
+				&DownloadURL{
+					URL:               encodedC,
+					OnlyAfterAttempts: 1,
+				},
+			},
+			2,
+			true,
+			decodedA,
+			3,
+		},
+		{
+			"multiple URLs, multiple attempts",
+			[]*DownloadURL{
+				&DownloadURL{
+					URL:               encodedA,
+					OnlyAfterAttempts: 0,
+				},
+				&DownloadURL{
+					URL:               encodedB,
+					OnlyAfterAttempts: 1,
+				},
+				&DownloadURL{
+					URL:               encodedC,
+					OnlyAfterAttempts: 3,
+				},
+			},
+			4,
+			true,
+			decodedA,
+			3,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.description, func(t *testing.T) {
+
+			err := decodeAndValidateDownloadURLs(
+				testCase.description,
+				testCase.downloadURLs)
+
+			if testCase.expectedValid {
+				if err != nil {
+					t.Fatalf("unexpected validation error: %s", err)
+				}
+			} else {
+				if err == nil {
+					t.Fatalf("expected validation error")
+				}
+				return
+			}
+
+			// Track distinct selections for each attempt; the
+			// expected number of distinct should be for at least
+			// one particular attempt.
+			attemptDistinctSelections := make(map[int]map[string]int)
+			for i := 0; i < testCase.attempts; i++ {
+				attemptDistinctSelections[i] = make(map[string]int)
+			}
+
+			// Perform enough runs to account for random selection.
+			runs := 1000
+
+			attempt := 0
+			for i := 0; i < runs; i++ {
+				url, canonicalURL, skipVerify := selectDownloadURL(attempt, testCase.downloadURLs)
+				if canonicalURL != testCase.expectedCanonicalURL {
+					t.Fatalf("unexpected canonical URL: %s", canonicalURL)
+				}
+				if skipVerify {
+					t.Fatalf("expected skipVerify")
+				}
+				attemptDistinctSelections[attempt][url] += 1
+				attempt = (attempt + 1) % testCase.attempts
+			}
+
+			maxDistinctSelections := 0
+			for _, m := range attemptDistinctSelections {
+				if len(m) > maxDistinctSelections {
+					maxDistinctSelections = len(m)
+				}
+			}
+
+			if maxDistinctSelections != testCase.expectedDistinctSelections {
+				t.Fatalf("got %d distinct selections, expected %d",
+					maxDistinctSelections,
+					testCase.expectedDistinctSelections)
+			}
+		})
+	}
+
+}

+ 30 - 27
psiphon/controller.go

@@ -100,6 +100,7 @@ func NewController(config *Config) (controller *Controller, err error) {
 		PendingConns:                  untunneledPendingConns,
 		DeviceBinder:                  config.DeviceBinder,
 		DnsServerGetter:               config.DnsServerGetter,
+		IPv6Synthesizer:               config.IPv6Synthesizer,
 		UseIndistinguishableTLS:       config.UseIndistinguishableTLS,
 		TrustedCACertificatesFilename: config.TrustedCACertificatesFilename,
 		DeviceRegion:                  config.DeviceRegion,
@@ -184,7 +185,7 @@ func (controller *Controller) Run(shutdownBroadcast <-chan struct{}) {
 		retryPeriod := time.Duration(
 			*controller.config.FetchRemoteServerListRetryPeriodSeconds) * time.Second
 
-		if controller.config.RemoteServerListUrl != "" {
+		if controller.config.RemoteServerListURLs != nil {
 			controller.runWaitGroup.Add(1)
 			go controller.remoteServerListFetcher(
 				"common",
@@ -194,7 +195,7 @@ func (controller *Controller) Run(shutdownBroadcast <-chan struct{}) {
 				FETCH_REMOTE_SERVER_LIST_STALE_PERIOD)
 		}
 
-		if controller.config.ObfuscatedServerListRootURL != "" {
+		if controller.config.ObfuscatedServerListRootURLs != nil {
 			controller.runWaitGroup.Add(1)
 			go controller.remoteServerListFetcher(
 				"obfuscated",
@@ -205,9 +206,7 @@ func (controller *Controller) Run(shutdownBroadcast <-chan struct{}) {
 		}
 	}
 
-	if controller.config.UpgradeDownloadUrl != "" &&
-		controller.config.UpgradeDownloadFilename != "" {
-
+	if controller.config.UpgradeDownloadURLs != nil {
 		controller.runWaitGroup.Add(1)
 		go controller.upgradeDownloader()
 	}
@@ -324,7 +323,7 @@ fetcherLoop:
 		}
 
 	retryLoop:
-		for {
+		for attempt := 0; ; attempt++ {
 			// Don't attempt to fetch while there is no network connectivity,
 			// to avoid alert notice noise.
 			if !WaitForNetworkConnectivity(
@@ -339,6 +338,7 @@ fetcherLoop:
 
 			err := fetcher(
 				controller.config,
+				attempt,
 				tunnel,
 				controller.untunneledDialConfig)
 
@@ -491,7 +491,7 @@ downloadLoop:
 		}
 
 	retryLoop:
-		for {
+		for attempt := 0; ; attempt++ {
 			// Don't attempt to download while there is no network connectivity,
 			// to avoid alert notice noise.
 			if !WaitForNetworkConnectivity(
@@ -506,6 +506,7 @@ downloadLoop:
 
 			err := DownloadUpgrade(
 				controller.config,
+				attempt,
 				handshakeVersion,
 				tunnel,
 				controller.untunneledDialConfig)
@@ -584,19 +585,15 @@ loop:
 
 			if controller.isImpairedProtocol(establishedTunnel.protocol) {
 
-				NoticeAlert("established tunnel with impaired protocol: %s", establishedTunnel.protocol)
+				// Protocol was classified as impaired while this tunnel established.
+				// This is most likely to occur with TunnelPoolSize > 0. We log the
+				// event but take no action. Discarding the tunnel would break the
+				// impaired logic unless we did that (a) only if there are other
+				// unimpaired protocols; (b) only during the first interation of the
+				// ESTABLISH_TUNNEL_WORK_TIME loop. By not discarding here, a true
+				// impaired protocol may require an extra reconnect.
 
-				// Protocol was classified as impaired while this tunnel
-				// established, so discard.
-				controller.discardTunnel(establishedTunnel)
-
-				// Reset establish generator to stop producing tunnels
-				// with impaired protocols.
-				if controller.isEstablishing {
-					controller.stopEstablishing()
-					controller.startEstablishing()
-				}
-				break
+				NoticeAlert("established tunnel with impaired protocol: %s", establishedTunnel.protocol)
 			}
 
 			tunnelCount, registered := controller.registerTunnel(establishedTunnel)
@@ -687,16 +684,24 @@ loop:
 //
 // Concurrency note: only the runTunnels() goroutine may call classifyImpairedProtocol
 func (controller *Controller) classifyImpairedProtocol(failedTunnel *Tunnel) {
+
 	if failedTunnel.establishedTime.Add(IMPAIRED_PROTOCOL_CLASSIFICATION_DURATION).After(monotime.Now()) {
 		controller.impairedProtocolClassification[failedTunnel.protocol] += 1
 	} else {
 		controller.impairedProtocolClassification[failedTunnel.protocol] = 0
 	}
-	if len(controller.getImpairedProtocols()) == len(protocol.SupportedTunnelProtocols) {
-		// Reset classification if all protocols are classified as impaired as
-		// the network situation (or attack) may not be protocol-specific.
-		// TODO: compare against count of distinct supported protocols for
-		// current known server entries.
+
+	// Reset classification once all known protocols are classified as impaired, as
+	// there is now no way to proceed with only unimpaired protocols. The network
+	// situation (or attack) resulting in classification may not be protocol-specific.
+	//
+	// Note: with controller.config.TunnelProtocol set, this will always reset once
+	// that protocol has reached IMPAIRED_PROTOCOL_CLASSIFICATION_THRESHOLD.
+	if CountNonImpairedProtocols(
+		controller.config.EgressRegion,
+		controller.config.TunnelProtocol,
+		controller.getImpairedProtocols()) == 0 {
+
 		controller.impairedProtocolClassification = make(map[string]int)
 	}
 }
@@ -1084,11 +1089,9 @@ loop:
 			// evade the attack; (b) there's a good chance of false
 			// positives (such as short tunnel durations due to network
 			// hopping on a mobile device).
-			// Impaired protocols logic is not applied when
-			// config.TunnelProtocol is specified.
 			// The edited serverEntry is temporary copy which is not
 			// stored or reused.
-			if i == 0 && controller.config.TunnelProtocol == "" {
+			if i == 0 {
 				serverEntry.DisableImpairedProtocols(impairedProtocols)
 				if len(serverEntry.GetSupportedProtocols()) == 0 {
 					// Skip this server entry, as it has no supported

+ 30 - 6
psiphon/controller_test.go

@@ -37,10 +37,10 @@ import (
 	"time"
 
 	"github.com/Psiphon-Inc/goarista/monotime"
+	"github.com/Psiphon-Inc/goproxy"
 	socks "github.com/Psiphon-Inc/goptlib"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
-	"github.com/elazarl/goproxy"
 )
 
 var testDataDirName string
@@ -650,8 +650,15 @@ func controllerRun(t *testing.T, runConfig *controllerRunConfig) {
 
 				count, ok := classification[serverProtocol]
 				if ok && count >= IMPAIRED_PROTOCOL_CLASSIFICATION_THRESHOLD {
-					// TODO: wrong goroutine for t.FatalNow()
-					t.Fatalf("unexpected tunnel using impaired protocol: %s, %+v",
+
+					// TODO: Fix this test case. Use of TunnelPoolSize breaks this
+					// case, as many tunnel establishments are occurring in parallel,
+					// and it can happen that a protocol is classified as impaired
+					// while a tunnel with that protocol is established and set
+					// active.
+
+					// *not* t.Fatalf
+					t.Logf("unexpected tunnel using impaired protocol: %s, %+v",
 						serverProtocol, classification)
 				}
 
@@ -827,7 +834,13 @@ func fetchAndVerifyWebsite(t *testing.T, httpProxyPort int) {
 		Timeout: roundTripTimeout,
 	}
 
-	response, err := httpClient.Get(testUrl)
+	request, err := http.NewRequest("GET", testUrl, nil)
+	if err != nil {
+		t.Fatalf("error preparing proxied HTTP request: %s", err)
+	}
+	request.Close = true
+
+	response, err := httpClient.Do(request)
 	if err != nil {
 		t.Fatalf("error sending proxied HTTP request: %s", err)
 	}
@@ -842,6 +855,9 @@ func fetchAndVerifyWebsite(t *testing.T, httpProxyPort int) {
 		t.Fatalf("unexpected proxied HTTP response")
 	}
 
+	// Delay before requesting from external service again
+	time.Sleep(1 * time.Second)
+
 	// Test: use direct URL proxy
 
 	httpClient = &http.Client{
@@ -849,9 +865,17 @@ func fetchAndVerifyWebsite(t *testing.T, httpProxyPort int) {
 		Timeout:   roundTripTimeout,
 	}
 
-	response, err = httpClient.Get(
+	request, err = http.NewRequest(
+		"GET",
 		fmt.Sprintf("http://127.0.0.1:%d/direct/%s",
-			httpProxyPort, url.QueryEscape(testUrl)))
+			httpProxyPort, url.QueryEscape(testUrl)),
+		nil)
+	if err != nil {
+		t.Fatalf("error preparing direct URL request: %s", err)
+	}
+	request.Close = true
+
+	response, err = httpClient.Do(request)
 	if err != nil {
 		t.Fatalf("error sending direct URL request: %s", err)
 	}

+ 43 - 9
psiphon/dataStore.go

@@ -365,13 +365,6 @@ func insertRankedServerEntry(tx *bolt.Tx, serverEntryId string, position int) er
 	return nil
 }
 
-func serverEntrySupportsProtocol(serverEntry *protocol.ServerEntry, protocol string) bool {
-	// Note: for meek, the capabilities are FRONTED-MEEK and UNFRONTED-MEEK
-	// and the additonal OSSH service is assumed to be available internally.
-	requiredCapability := strings.TrimSuffix(protocol, "-OSSH")
-	return common.Contains(serverEntry.Capabilities, requiredCapability)
-}
-
 // ServerEntryIterator is used to iterate over
 // stored server entries in rank order.
 type ServerEntryIterator struct {
@@ -573,7 +566,7 @@ func (iterator *ServerEntryIterator) Next() (serverEntry *protocol.ServerEntry,
 
 		// Check filter requirements
 		if (iterator.region == "" || serverEntry.Region == iterator.region) &&
-			(iterator.protocol == "" || serverEntrySupportsProtocol(serverEntry, iterator.protocol)) {
+			(iterator.protocol == "" || serverEntry.SupportsProtocol(iterator.protocol)) {
 
 			break
 		}
@@ -630,7 +623,7 @@ func CountServerEntries(region, tunnelProtocol string) int {
 	count := 0
 	err := scanServerEntries(func(serverEntry *protocol.ServerEntry) {
 		if (region == "" || serverEntry.Region == region) &&
-			(tunnelProtocol == "" || serverEntrySupportsProtocol(serverEntry, tunnelProtocol)) {
+			(tunnelProtocol == "" || serverEntry.SupportsProtocol(tunnelProtocol)) {
 			count += 1
 		}
 	})
@@ -643,6 +636,47 @@ func CountServerEntries(region, tunnelProtocol string) int {
 	return count
 }
 
+// CountNonImpairedProtocols returns the number of distinct tunnel
+// protocols supported by stored server entries, excluding the
+// specified impaired protocols.
+func CountNonImpairedProtocols(
+	region, tunnelProtocol string,
+	impairedProtocols []string) int {
+
+	checkInitDataStore()
+
+	distinctProtocols := make(map[string]bool)
+
+	err := scanServerEntries(func(serverEntry *protocol.ServerEntry) {
+		if region == "" || serverEntry.Region == region {
+			if tunnelProtocol != "" {
+				if serverEntry.SupportsProtocol(tunnelProtocol) {
+					distinctProtocols[tunnelProtocol] = true
+					// Exit early, since only one protocol is enabled
+					return
+				}
+			} else {
+				for _, protocol := range protocol.SupportedTunnelProtocols {
+					if serverEntry.SupportsProtocol(protocol) {
+						distinctProtocols[protocol] = true
+					}
+				}
+			}
+		}
+	})
+
+	for _, protocol := range impairedProtocols {
+		delete(distinctProtocols, protocol)
+	}
+
+	if err != nil {
+		NoticeAlert("CountNonImpairedProtocols failed: %s", err)
+		return 0
+	}
+
+	return len(distinctProtocols)
+}
+
 // ReportAvailableRegions prints a notice with the available egress regions.
 // Note that this report ignores config.TunnelProtocol.
 func ReportAvailableRegions() {

+ 3 - 1
psiphon/feedback.go

@@ -113,6 +113,7 @@ func SendFeedback(configJson, diagnosticsJson, b64EncodedPublicKey, uploadServer
 		UpstreamProxyCustomHeaders:    config.UpstreamProxyCustomHeaders,
 		PendingConns:                  nil,
 		DeviceBinder:                  nil,
+		IPv6Synthesizer:               nil,
 		DnsServerGetter:               nil,
 		UseIndistinguishableTLS:       config.UseIndistinguishableTLS,
 		TrustedCACertificatesFilename: config.TrustedCACertificatesFilename,
@@ -151,7 +152,8 @@ func SendFeedback(configJson, diagnosticsJson, b64EncodedPublicKey, uploadServer
 
 // Attempt to upload feedback data to server.
 func uploadFeedback(config *DialConfig, feedbackData []byte, url string, headerPieces []string) error {
-	client, parsedUrl, err := MakeUntunneledHttpsClient(config, nil, url, time.Duration(FEEDBACK_UPLOAD_TIMEOUT_SECONDS*time.Second))
+	client, parsedUrl, err := MakeUntunneledHttpsClient(
+		config, nil, url, false, time.Duration(FEEDBACK_UPLOAD_TIMEOUT_SECONDS*time.Second))
 	if err != nil {
 		return err
 	}

+ 27 - 4
psiphon/net.go

@@ -79,6 +79,7 @@ type DialConfig struct {
 	// current active untunneled network DNS server.
 	DeviceBinder    DeviceBinder
 	DnsServerGetter DnsServerGetter
+	IPv6Synthesizer IPv6Synthesizer
 
 	// UseIndistinguishableTLS specifies whether to try to use an
 	// alternative stack for TLS. From a circumvention perspective,
@@ -123,6 +124,11 @@ type DnsServerGetter interface {
 	GetSecondaryDnsServer() string
 }
 
+// IPv6Synthesizer defines the interface to the external IPv6Synthesize provider
+type IPv6Synthesizer interface {
+	IPv6Synthesize(IPv4Addr string) string
+}
+
 // TimeoutError implements the error interface
 type TimeoutError struct{}
 
@@ -239,11 +245,16 @@ func MakeUntunneledHttpsClient(
 	dialConfig *DialConfig,
 	verifyLegacyCertificate *x509.Certificate,
 	requestUrl string,
+	skipVerify bool,
 	requestTimeout time.Duration) (*http.Client, string, error) {
 
 	// Change the scheme to "http"; otherwise http.Transport will try to do
 	// another TLS handshake inside the explicit TLS session. Also need to
 	// force an explicit port, as the default for "http", 80, won't talk TLS.
+	//
+	// TODO: set http.Transport.DialTLS instead of Dial to avoid this hack?
+	// See: https://golang.org/pkg/net/http/#Transport. DialTLS was added in
+	// Go 1.4 but this code may pre-date that.
 
 	urlComponents, err := url.Parse(requestUrl)
 	if err != nil {
@@ -272,7 +283,7 @@ func MakeUntunneledHttpsClient(
 			Dial: NewTCPDialer(dialConfig),
 			VerifyLegacyCertificate:       verifyLegacyCertificate,
 			SNIServerName:                 host,
-			SkipVerify:                    false,
+			SkipVerify:                    skipVerify,
 			UseIndistinguishableTLS:       useIndistinguishableTLS,
 			TrustedCACertificatesFilename: dialConfig.TrustedCACertificatesFilename,
 		})
@@ -297,6 +308,7 @@ func MakeUntunneledHttpsClient(
 func MakeTunneledHttpClient(
 	config *Config,
 	tunnel *Tunnel,
+	skipVerify bool,
 	requestTimeout time.Duration) (*http.Client, error) {
 
 	tunneledDialer := func(_, addr string) (conn net.Conn, err error) {
@@ -307,7 +319,12 @@ func MakeTunneledHttpClient(
 		Dial: tunneledDialer,
 	}
 
-	if config.UseTrustedCACertificatesForStockTLS {
+	if skipVerify {
+
+		transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+
+	} else if config.UseTrustedCACertificatesForStockTLS {
+
 		if config.TrustedCACertificatesFilename == "" {
 			return nil, common.ContextError(errors.New(
 				"UseTrustedCACertificatesForStockTLS requires TrustedCACertificatesFilename"))
@@ -336,6 +353,7 @@ func MakeDownloadHttpClient(
 	tunnel *Tunnel,
 	untunneledDialConfig *DialConfig,
 	requestUrl string,
+	skipVerify bool,
 	requestTimeout time.Duration) (*http.Client, string, error) {
 
 	var httpClient *http.Client
@@ -343,7 +361,8 @@ func MakeDownloadHttpClient(
 
 	if tunnel != nil {
 		// MakeTunneledHttpClient works with both "http" and "https" schemes
-		httpClient, err = MakeTunneledHttpClient(config, tunnel, requestTimeout)
+		httpClient, err = MakeTunneledHttpClient(
+			config, tunnel, skipVerify, requestTimeout)
 		if err != nil {
 			return nil, "", common.ContextError(err)
 		}
@@ -355,7 +374,7 @@ func MakeDownloadHttpClient(
 		// MakeUntunneledHttpsClient works only with "https" schemes
 		if urlComponents.Scheme == "https" {
 			httpClient, requestUrl, err = MakeUntunneledHttpsClient(
-				untunneledDialConfig, nil, requestUrl, requestTimeout)
+				untunneledDialConfig, nil, requestUrl, skipVerify, requestTimeout)
 			if err != nil {
 				return nil, "", common.ContextError(err)
 			}
@@ -467,6 +486,10 @@ func ResumeDownload(
 	// receive 412 on ETag mismatch.
 	if err == nil &&
 		(response.StatusCode != http.StatusPartialContent &&
+
+			// Certain http servers return 200 OK where we expect 206, so accept that.
+			response.StatusCode != http.StatusOK &&
+
 			response.StatusCode != http.StatusRequestedRangeNotSatisfiable &&
 			response.StatusCode != http.StatusPreconditionFailed &&
 			response.StatusCode != http.StatusNotModified) {

+ 39 - 15
psiphon/remoteServerList.go

@@ -34,7 +34,7 @@ import (
 )
 
 type RemoteServerListFetcher func(
-	config *Config, tunnel *Tunnel, untunneledDialConfig *DialConfig) error
+	config *Config, attempt int, tunnel *Tunnel, untunneledDialConfig *DialConfig) error
 
 // FetchCommonRemoteServerList downloads the common remote server list from
 // config.RemoteServerListUrl. It validates its digital signature using the
@@ -45,16 +45,21 @@ type RemoteServerListFetcher func(
 // be unique and persistent.
 func FetchCommonRemoteServerList(
 	config *Config,
+	attempt int,
 	tunnel *Tunnel,
 	untunneledDialConfig *DialConfig) error {
 
 	NoticeInfo("fetching common remote server list")
 
+	downloadURL, canonicalURL, skipVerify := selectDownloadURL(attempt, config.RemoteServerListURLs)
+
 	newETag, err := downloadRemoteServerListFile(
 		config,
 		tunnel,
 		untunneledDialConfig,
-		config.RemoteServerListUrl,
+		downloadURL,
+		canonicalURL,
+		skipVerify,
 		"",
 		config.RemoteServerListDownloadFilename)
 	if err != nil {
@@ -78,7 +83,7 @@ func FetchCommonRemoteServerList(
 
 	// Now that the server entries are successfully imported, store the response
 	// ETag so we won't re-download this same data again.
-	err = SetUrlETag(config.RemoteServerListUrl, newETag)
+	err = SetUrlETag(canonicalURL, newETag)
 	if err != nil {
 		NoticeAlert("failed to set ETag for common remote server list: %s", common.ContextError(err))
 		// This fetch is still reported as a success, even if we can't store the etag
@@ -100,13 +105,17 @@ func FetchCommonRemoteServerList(
 // must be unique and persistent.
 func FetchObfuscatedServerLists(
 	config *Config,
+	attempt int,
 	tunnel *Tunnel,
 	untunneledDialConfig *DialConfig) error {
 
 	NoticeInfo("fetching obfuscated remote server lists")
 
 	downloadFilename := osl.GetOSLRegistryFilename(config.ObfuscatedServerListDownloadDirectory)
-	downloadURL := osl.GetOSLRegistryURL(config.ObfuscatedServerListRootURL)
+
+	rootURL, canonicalRootURL, skipVerify := selectDownloadURL(attempt, config.ObfuscatedServerListRootURLs)
+	downloadURL := osl.GetOSLRegistryURL(rootURL)
+	canonicalURL := osl.GetOSLRegistryURL(canonicalRootURL)
 
 	// failed is set if any operation fails and should trigger a retry. When the OSL registry
 	// fails to download, any cached registry is used instead; when any single OSL fails
@@ -122,6 +131,8 @@ func FetchObfuscatedServerLists(
 		tunnel,
 		untunneledDialConfig,
 		downloadURL,
+		canonicalURL,
+		skipVerify,
 		"",
 		downloadFilename)
 	if err != nil {
@@ -173,7 +184,7 @@ func FetchObfuscatedServerLists(
 	// When a new registry is downloaded, validated, and parsed, store the
 	// response ETag so we won't re-download this same data again.
 	if !failed && newETag != "" {
-		err = SetUrlETag(downloadURL, newETag)
+		err = SetUrlETag(canonicalURL, newETag)
 		if err != nil {
 			NoticeAlert("failed to set ETag for obfuscated server list registry: %s", common.ContextError(err))
 			// This fetch is still reported as a success, even if we can't store the etag
@@ -199,16 +210,20 @@ func FetchObfuscatedServerLists(
 		})
 
 	for _, oslID := range oslIDs {
+
 		downloadFilename := osl.GetOSLFilename(config.ObfuscatedServerListDownloadDirectory, oslID)
-		downloadURL := osl.GetOSLFileURL(config.ObfuscatedServerListRootURL, oslID)
+
+		downloadURL := osl.GetOSLFileURL(rootURL, oslID)
+		canonicalURL := osl.GetOSLFileURL(canonicalRootURL, oslID)
+
 		hexID := hex.EncodeToString(oslID)
 
 		// Note: the MD5 checksum step assumes the remote server list host's ETag uses MD5
-		// with a hex encoding. If this is not the case, the remoteETag should be left blank.
-		remoteETag := ""
+		// with a hex encoding. If this is not the case, the sourceETag should be left blank.
+		sourceETag := ""
 		md5sum, err := oslRegistry.GetOSLMD5Sum(oslID)
 		if err == nil {
-			remoteETag = hex.EncodeToString(md5sum)
+			sourceETag = fmt.Sprintf("\"%s\"", hex.EncodeToString(md5sum))
 		}
 
 		// TODO: store ETags in OSL registry to enable skipping requests entirely
@@ -218,7 +233,9 @@ func FetchObfuscatedServerLists(
 			tunnel,
 			untunneledDialConfig,
 			downloadURL,
-			remoteETag,
+			canonicalURL,
+			skipVerify,
+			sourceETag,
 			downloadFilename)
 		if err != nil {
 			failed = true
@@ -255,7 +272,7 @@ func FetchObfuscatedServerLists(
 
 		// Now that the server entries are successfully imported, store the response
 		// ETag so we won't re-download this same data again.
-		err = SetUrlETag(downloadURL, newETag)
+		err = SetUrlETag(canonicalURL, newETag)
 		if err != nil {
 			failed = true
 			NoticeAlert("failed to set Etag for obfuscated server list file (%s): %s", hexID, common.ContextError(err))
@@ -280,14 +297,20 @@ func downloadRemoteServerListFile(
 	config *Config,
 	tunnel *Tunnel,
 	untunneledDialConfig *DialConfig,
-	sourceURL, sourceETag, destinationFilename string) (string, error) {
-
-	lastETag, err := GetUrlETag(sourceURL)
+	sourceURL string,
+	canonicalURL string,
+	skipVerify bool,
+	sourceETag string,
+	destinationFilename string) (string, error) {
+
+	// All download URLs with the same canonicalURL
+	// must have the same entity and ETag.
+	lastETag, err := GetUrlETag(canonicalURL)
 	if err != nil {
 		return "", common.ContextError(err)
 	}
 
-	// sourceETag, when specified, is prior knowlegde of the
+	// sourceETag, when specified, is prior knowledge of the
 	// remote ETag that can be used to skip the request entirely.
 	// This will be set in the case of OSL files, from the MD5Sum
 	// values stored in the registry.
@@ -304,6 +327,7 @@ func downloadRemoteServerListFile(
 		tunnel,
 		untunneledDialConfig,
 		sourceURL,
+		skipVerify,
 		time.Duration(*config.FetchRemoteServerListTimeoutSeconds)*time.Second)
 	if err != nil {
 		return "", common.ContextError(err)

+ 50 - 31
psiphon/remoteServerList_test.go

@@ -22,6 +22,7 @@ package psiphon
 import (
 	"bytes"
 	"crypto/md5"
+	"encoding/base64"
 	"encoding/hex"
 	"fmt"
 	"io"
@@ -146,7 +147,8 @@ func TestObfuscatedRemoteServerLists(t *testing.T) {
 			map[time.Time]string{
 				epoch: string(encodedServerEntry),
 			},
-		})
+		},
+		nil)
 	if err != nil {
 		t.Fatalf("error paving OSL files: %s", err)
 	}
@@ -181,38 +183,55 @@ func TestObfuscatedRemoteServerLists(t *testing.T) {
 	// run mock remote server list host
 	//
 
-	remoteServerListHostAddress := net.JoinHostPort(serverIPaddress, "8081")
+	// Exercise using multiple download URLs
+	remoteServerListHostAddresses := []string{
+		net.JoinHostPort(serverIPaddress, "8081"),
+		net.JoinHostPort(serverIPaddress, "8082"),
+	}
 
 	// The common remote server list fetches will 404
-	remoteServerListURL := fmt.Sprintf("http://%s/server_list_compressed", remoteServerListHostAddress)
+	remoteServerListURL := fmt.Sprintf("http://%s/server_list_compressed", remoteServerListHostAddresses[0])
 	remoteServerListDownloadFilename := filepath.Join(testDataDirName, "server_list_compressed")
 
-	obfuscatedServerListRootURL := fmt.Sprintf("http://%s/", remoteServerListHostAddress)
-	obfuscatedServerListDownloadDirectory := testDataDirName
+	obfuscatedServerListRootURLsJSONConfig := "["
+	obfuscatedServerListRootURLs := make([]string, len(remoteServerListHostAddresses))
+	for i := 0; i < len(remoteServerListHostAddresses); i++ {
 
-	go func() {
-		startTime := time.Now()
-		serveMux := http.NewServeMux()
-		for _, paveFile := range paveFiles {
-			file := paveFile
-			serveMux.HandleFunc("/"+file.Name, func(w http.ResponseWriter, req *http.Request) {
-				md5sum := md5.Sum(file.Contents)
-				w.Header().Add("Content-Type", "application/octet-stream")
-				w.Header().Add("ETag", hex.EncodeToString(md5sum[:]))
-				http.ServeContent(w, req, file.Name, startTime, bytes.NewReader(file.Contents))
-			})
-		}
-		httpServer := &http.Server{
-			Addr:    remoteServerListHostAddress,
-			Handler: serveMux,
-		}
-		err := httpServer.ListenAndServe()
-		if err != nil {
-			// TODO: wrong goroutine for t.FatalNow()
-			t.Fatalf("error running remote server list host: %s", err)
+		obfuscatedServerListRootURLs[i] = fmt.Sprintf("http://%s/", remoteServerListHostAddresses[i])
 
+		obfuscatedServerListRootURLsJSONConfig += fmt.Sprintf(
+			"{\"URL\" : \"%s\"}", base64.StdEncoding.EncodeToString([]byte(obfuscatedServerListRootURLs[i])))
+		if i == len(remoteServerListHostAddresses)-1 {
+			obfuscatedServerListRootURLsJSONConfig += "]"
+		} else {
+			obfuscatedServerListRootURLsJSONConfig += ","
 		}
-	}()
+
+		go func(remoteServerListHostAddress string) {
+			startTime := time.Now()
+			serveMux := http.NewServeMux()
+			for _, paveFile := range paveFiles {
+				file := paveFile
+				serveMux.HandleFunc("/"+file.Name, func(w http.ResponseWriter, req *http.Request) {
+					md5sum := md5.Sum(file.Contents)
+					w.Header().Add("Content-Type", "application/octet-stream")
+					w.Header().Add("ETag", fmt.Sprintf("\"%s\"", hex.EncodeToString(md5sum[:])))
+					http.ServeContent(w, req, file.Name, startTime, bytes.NewReader(file.Contents))
+				})
+			}
+			httpServer := &http.Server{
+				Addr:    remoteServerListHostAddress,
+				Handler: serveMux,
+			}
+			err := httpServer.ListenAndServe()
+			if err != nil {
+				// TODO: wrong goroutine for t.FatalNow()
+				t.Fatalf("error running remote server list host: %s", err)
+			}
+		}(remoteServerListHostAddresses[i])
+	}
+
+	obfuscatedServerListDownloadDirectory := testDataDirName
 
 	//
 	// run Psiphon server
@@ -263,7 +282,7 @@ func TestObfuscatedRemoteServerLists(t *testing.T) {
 					defer waitGroup.Done()
 					io.Copy(remoteConn, localConn)
 				}()
-				if localConn.Req.Target == remoteServerListHostAddress {
+				if common.Contains(remoteServerListHostAddresses, localConn.Req.Target) {
 					io.CopyN(localConn, remoteConn, 500)
 				} else {
 					io.Copy(localConn, remoteConn)
@@ -294,7 +313,7 @@ func TestObfuscatedRemoteServerLists(t *testing.T) {
 		"RemoteServerListSignaturePublicKey" : "%s",
 		"RemoteServerListUrl" : "%s",
 		"RemoteServerListDownloadFilename" : "%s",
-		"ObfuscatedServerListRootURL" : "%s",
+		"ObfuscatedServerListRootURLs" : %s,
 		"ObfuscatedServerListDownloadDirectory" : "%s",
 		"UpstreamProxyUrl" : "%s"
     }`
@@ -304,7 +323,7 @@ func TestObfuscatedRemoteServerLists(t *testing.T) {
 		signingPublicKey,
 		remoteServerListURL,
 		remoteServerListDownloadFilename,
-		obfuscatedServerListRootURL,
+		obfuscatedServerListRootURLsJSONConfig,
 		obfuscatedServerListDownloadDirectory,
 		disruptorProxyURL)
 
@@ -359,11 +378,11 @@ func TestObfuscatedRemoteServerLists(t *testing.T) {
 	}
 
 	for _, paveFile := range paveFiles {
-		u, _ := url.Parse(obfuscatedServerListRootURL)
+		u, _ := url.Parse(obfuscatedServerListRootURLs[0])
 		u.Path = path.Join(u.Path, paveFile.Name)
 		etag, _ := GetUrlETag(u.String())
 		md5sum := md5.Sum(paveFile.Contents)
-		if etag != hex.EncodeToString(md5sum[:]) {
+		if etag != fmt.Sprintf("\"%s\"", hex.EncodeToString(md5sum[:])) {
 			t.Fatalf("unexpected ETag for %s", u)
 		}
 	}

+ 1 - 1
psiphon/server/geoip.go

@@ -145,7 +145,7 @@ func (geoIP *GeoIPService) Lookup(ipAddress string) GeoIPData {
 	}
 
 	// Each database will populate geoIPFields with the values it contains. In the
-	// currnt MaxMind deployment, the City database populates Country and City and
+	// current MaxMind deployment, the City database populates Country and City and
 	// the separate ISP database populates ISP.
 	for _, database := range geoIP.databases {
 		database.ReloadableFile.RLock()

+ 16 - 2
psiphon/server/server_test.go

@@ -580,12 +580,26 @@ func makeTunneledWebRequest(t *testing.T, localHTTPProxyPort int) error {
 
 func makeTunneledNTPRequest(t *testing.T, localSOCKSProxyPort int, udpgwServerAddress string) error {
 
-	testHostname := "pool.ntp.org"
 	timeout := 20 * time.Second
+	var err error
+
+	for _, testHostname := range []string{"time.google.com", "time.nist.gov", "pool.ntp.org"} {
+		err = makeTunneledNTPRequestAttempt(t, testHostname, timeout, localSOCKSProxyPort, udpgwServerAddress)
+		if err == nil {
+			break
+		}
+		t.Logf("makeTunneledNTPRequestAttempt failed: %s", err)
+	}
+
+	return err
+}
+
+func makeTunneledNTPRequestAttempt(
+	t *testing.T, testHostname string, timeout time.Duration, localSOCKSProxyPort int, udpgwServerAddress string) error {
 
 	localUDPProxyAddress, err := net.ResolveUDPAddr("udp", "127.0.0.1:7301")
 	if err != nil {
-		t.Fatalf("ResolveUDPAddr failed: %s", err)
+		return fmt.Errorf("ResolveUDPAddr failed: %s", err)
 	}
 
 	// Note: this proxy is intended for this test only -- it only accepts a single connection,

+ 1 - 0
psiphon/serverApi.go

@@ -525,6 +525,7 @@ func (serverContext *ServerContext) doUntunneledStatusRequest(
 		dialConfig,
 		certificate,
 		url,
+		false,
 		timeout)
 	if err != nil {
 		return common.ContextError(err)

+ 1 - 0
psiphon/tunnel.go

@@ -629,6 +629,7 @@ func dialSsh(
 		PendingConns:                  pendingConns,
 		DeviceBinder:                  config.DeviceBinder,
 		DnsServerGetter:               config.DnsServerGetter,
+		IPv6Synthesizer:               config.IPv6Synthesizer,
 		UseIndistinguishableTLS:       config.UseIndistinguishableTLS,
 		TrustedCACertificatesFilename: config.TrustedCACertificatesFilename,
 		DeviceRegion:                  config.DeviceRegion,

+ 8 - 1
psiphon/upgradeDownload.go

@@ -55,10 +55,14 @@ import (
 // upgrade is still pending install by the outer client.
 func DownloadUpgrade(
 	config *Config,
+	attempt int,
 	handshakeVersion string,
 	tunnel *Tunnel,
 	untunneledDialConfig *DialConfig) error {
 
+	// Note: this downloader doesn't use ETags since many client binaries, with
+	// different embedded values, exist for a single version.
+
 	// Check if complete file already downloaded
 
 	if _, err := os.Stat(config.UpgradeDownloadFilename); err == nil {
@@ -68,11 +72,14 @@ func DownloadUpgrade(
 
 	// Select tunneled or untunneled configuration
 
+	downloadURL, _, skipVerify := selectDownloadURL(attempt, config.UpgradeDownloadURLs)
+
 	httpClient, requestUrl, err := MakeDownloadHttpClient(
 		config,
 		tunnel,
 		untunneledDialConfig,
-		config.UpgradeDownloadUrl,
+		downloadURL,
+		skipVerify,
 		DOWNLOAD_UPGRADE_TIMEOUT)
 
 	// If no handshake version is supplied, make an initial HEAD request