func main() { var configFilename string flag.StringVar(&configFilename, "config", "", "OSL configuration file") var scheme int flag.IntVar(&scheme, "scheme", 0, "scheme to pave") var oslOffset int flag.IntVar(&oslOffset, "offset", 0, "OSL offset") var oslCount int flag.IntVar(&oslCount, "count", 1, "OSL count") var signingKeyPairFilename string flag.StringVar(&signingKeyPairFilename, "key", "", "signing public key pair") var destinationDirectory string flag.StringVar(&destinationDirectory, "output", "", "destination directory for output files") flag.Parse() configJSON, err := ioutil.ReadFile(configFilename) if err != nil { fmt.Printf("failed loading configuration file: %s\n", err) os.Exit(1) } config, err := osl.LoadConfig(configJSON) if err != nil { fmt.Printf("failed processing configuration file: %s\n", err) os.Exit(1) } if scheme < 0 || scheme >= len(config.Schemes) { fmt.Printf("failed: invalid scheme\n") os.Exit(1) } keyPairPEM, err := ioutil.ReadFile(signingKeyPairFilename) if err != nil { fmt.Printf("failed loading signing public key pair file: %s\n", err) os.Exit(1) } // Password "none" from psi_ops: // https://bitbucket.org/psiphon/psiphon-circumvention-system/src/ef4f3d4893bd5259ef24f0cb4525cbbbb0854cf9/Automation/psi_ops.py?at=default&fileviewer=file-view-default#psi_ops.py-297 block, _ := pem.Decode(keyPairPEM) decryptedKeyPairPEM, err := x509.DecryptPEMBlock(block, []byte("none")) if err != nil { fmt.Printf("failed decrypting signing public key pair file: %s\n", err) os.Exit(1) } rsaKey, err := x509.ParsePKCS1PrivateKey(decryptedKeyPairPEM) if err != nil { fmt.Printf("failed parsing signing public key pair file: %s\n", err) os.Exit(1) } publicKeyBytes, err := x509.MarshalPKIXPublicKey(rsaKey.Public()) if err != nil { fmt.Printf("failed marshaling signing public key: %s\n", err) os.Exit(1) } privateKeyBytes := x509.MarshalPKCS1PrivateKey(rsaKey) signingPublicKey := base64.StdEncoding.EncodeToString(publicKeyBytes) signingPrivateKey := base64.StdEncoding.EncodeToString(privateKeyBytes) slokTimePeriodsPerOSL := 1 for _, keySplit := range config.Schemes[scheme].SeedPeriodKeySplits { slokTimePeriodsPerOSL *= keySplit.Total } oslTimePeriod := time.Duration(config.Schemes[0].SeedPeriodNanoseconds * int64(slokTimePeriodsPerOSL)) for _, propagationChannelID := range config.Schemes[0].PropagationChannelIDs { paveServerEntries := make([]map[time.Time]string, len(config.Schemes)) paveServerEntries[0] = make(map[time.Time]string) epoch, _ := time.Parse(time.RFC3339, config.Schemes[0].Epoch) for i := oslOffset; i < oslOffset+oslCount; i++ { paveServerEntries[0][epoch.Add(time.Duration(i)*oslTimePeriod)] = "" } paveFiles, err := config.Pave( epoch.Add(time.Duration(oslCount)*oslTimePeriod), propagationChannelID, signingPublicKey, signingPrivateKey, paveServerEntries) if err != nil { fmt.Printf("failed paving: %s\n", err) os.Exit(1) } directory := filepath.Join(destinationDirectory, propagationChannelID) err = os.MkdirAll(directory, 0755) if err != nil { fmt.Printf("failed creating output directory: %s\n", err) os.Exit(1) } for _, paveFile := range paveFiles { filename := filepath.Join(directory, paveFile.Name) err = ioutil.WriteFile(filename, paveFile.Contents, 0755) if err != nil { fmt.Printf("error writing output file: %s\n", err) os.Exit(1) } } } }
func TestObfuscatedRemoteServerLists(t *testing.T) { testDataDirName, err := ioutil.TempDir("", "psiphon-remote-server-list-test") if err != nil { t.Fatalf("TempDir failed: %s", err) } defer os.RemoveAll(testDataDirName) // // create a server // serverIPaddress := "" for _, interfaceName := range []string{"eth0", "en0"} { serverIPaddress, err = common.GetInterfaceIPAddress(interfaceName) if err == nil { break } } if err != nil { t.Fatalf("error getting server IP address: %s", err) } serverConfigJSON, _, encodedServerEntry, err := server.GenerateConfig( &server.GenerateConfigParams{ ServerIPAddress: serverIPaddress, EnableSSHAPIRequests: true, WebServerPort: 8001, TunnelProtocolPorts: map[string]int{"OSSH": 4001}, }) if err != nil { t.Fatalf("error generating server config: %s", err) } // // pave OSLs // oslConfigJSONTemplate := ` { "Schemes" : [ { "Epoch" : "%s", "Regions" : [], "PropagationChannelIDs" : ["%s"], "MasterKey" : "vwab2WY3eNyMBpyFVPtsivMxF4MOpNHM/T7rHJIXctg=", "SeedSpecs" : [ { "ID" : "KuP2V6gLcROIFzb/27fUVu4SxtEfm2omUoISlrWv1mA=", "UpstreamSubnets" : ["0.0.0.0/0"], "Targets" : { "BytesRead" : 1, "BytesWritten" : 1, "PortForwardDurationNanoseconds" : 1 } } ], "SeedSpecThreshold" : 1, "SeedPeriodNanoseconds" : %d, "SeedPeriodKeySplits": [ { "Total": 1, "Threshold": 1 } ] } ] }` now := time.Now().UTC() seedPeriod := 24 * time.Hour epoch := now.Truncate(seedPeriod) epochStr := epoch.Format(time.RFC3339Nano) propagationChannelID, _ := common.MakeRandomStringHex(8) oslConfigJSON := fmt.Sprintf( oslConfigJSONTemplate, epochStr, propagationChannelID, seedPeriod) oslConfig, err := osl.LoadConfig([]byte(oslConfigJSON)) if err != nil { t.Fatalf("error loading OSL config: %s", err) } signingPublicKey, signingPrivateKey, err := common.GenerateAuthenticatedDataPackageKeys() if err != nil { t.Fatalf("error generating package keys: %s", err) } paveFiles, err := oslConfig.Pave( epoch, propagationChannelID, signingPublicKey, signingPrivateKey, []map[time.Time]string{ map[time.Time]string{ epoch: string(encodedServerEntry), }, }) if err != nil { t.Fatalf("error paving OSL files: %s", err) } // // mock seeding SLOKs // singleton = dataStore{} os.Remove(filepath.Join(testDataDirName, DATA_STORE_FILENAME)) err = InitDataStore(&Config{DataStoreDirectory: testDataDirName}) if err != nil { t.Fatalf("error initializing client datastore: %s", err) } if CountServerEntries("", "") > 0 { t.Fatalf("unexpected server entries") } seedState := oslConfig.NewClientSeedState("", propagationChannelID, nil) seedPortForward := seedState.NewClientSeedPortForward(net.ParseIP("0.0.0.0")) seedPortForward.UpdateProgress(1, 1, 1) payload := seedState.GetSeedPayload() if len(payload.SLOKs) != 1 { t.Fatalf("expected 1 SLOKs, got %d", len(payload.SLOKs)) } SetSLOK(payload.SLOKs[0].ID, payload.SLOKs[0].Key) // // run mock remote server list host // remoteServerListHostAddress := net.JoinHostPort(serverIPaddress, "8081") // The common remote server list fetches will 404 remoteServerListURL := fmt.Sprintf("http://%s/server_list_compressed", remoteServerListHostAddress) remoteServerListDownloadFilename := filepath.Join(testDataDirName, "server_list_compressed") obfuscatedServerListRootURL := fmt.Sprintf("http://%s/", remoteServerListHostAddress) obfuscatedServerListDownloadDirectory := testDataDirName 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) } }() // // run Psiphon server // go func() { err := server.RunServices(serverConfigJSON) if err != nil { // TODO: wrong goroutine for t.FatalNow() t.Fatalf("error running server: %s", err) } }() // // disrupt remote server list downloads // disruptorProxyAddress := "127.0.0.1:2162" disruptorProxyURL := "socks4a://" + disruptorProxyAddress go func() { listener, err := socks.ListenSocks("tcp", disruptorProxyAddress) if err != nil { fmt.Errorf("disruptor proxy listen error: %s", err) return } for { localConn, err := listener.AcceptSocks() if err != nil { fmt.Errorf("disruptor proxy accept error: %s", err) return } go func() { remoteConn, err := net.Dial("tcp", localConn.Req.Target) if err != nil { fmt.Errorf("disruptor proxy dial error: %s", err) return } err = localConn.Grant(&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: 0}) if err != nil { fmt.Errorf("disruptor proxy grant error: %s", err) return } waitGroup := new(sync.WaitGroup) waitGroup.Add(1) go func() { defer waitGroup.Done() io.Copy(remoteConn, localConn) }() if localConn.Req.Target == remoteServerListHostAddress { io.CopyN(localConn, remoteConn, 500) } else { io.Copy(localConn, remoteConn) } localConn.Close() remoteConn.Close() waitGroup.Wait() }() } }() // // connect to Psiphon server with Psiphon client // SetEmitDiagnosticNotices(true) // Note: calling LoadConfig ensures all *int config fields are initialized clientConfigJSONTemplate := ` { "ClientPlatform" : "", "ClientVersion" : "0", "SponsorId" : "0", "PropagationChannelId" : "0", "ConnectionPoolSize" : 1, "EstablishTunnelPausePeriodSeconds" : 1, "FetchRemoteServerListRetryPeriodSeconds" : 1, "RemoteServerListSignaturePublicKey" : "%s", "RemoteServerListUrl" : "%s", "RemoteServerListDownloadFilename" : "%s", "ObfuscatedServerListRootURL" : "%s", "ObfuscatedServerListDownloadDirectory" : "%s", "UpstreamProxyUrl" : "%s" }` clientConfigJSON := fmt.Sprintf( clientConfigJSONTemplate, signingPublicKey, remoteServerListURL, remoteServerListDownloadFilename, obfuscatedServerListRootURL, obfuscatedServerListDownloadDirectory, disruptorProxyURL) clientConfig, _ := LoadConfig([]byte(clientConfigJSON)) controller, err := NewController(clientConfig) if err != nil { t.Fatalf("error creating client controller: %s", err) } tunnelEstablished := make(chan struct{}, 1) SetNoticeOutput(NewNoticeReceiver( func(notice []byte) { noticeType, payload, err := GetNotice(notice) if err != nil { return } printNotice := false switch noticeType { case "Tunnels": printNotice = true count := int(payload["count"].(float64)) if count == 1 { tunnelEstablished <- *new(struct{}) } case "RemoteServerListResourceDownloadedBytes": // TODO: check for resumed download for each URL //url := payload["url"].(string) printNotice = true case "RemoteServerListResourceDownloaded": printNotice = true } if printNotice { fmt.Printf("%s\n", string(notice)) } })) go func() { controller.Run(make(chan struct{})) }() establishTimeout := time.NewTimer(30 * time.Second) select { case <-tunnelEstablished: case <-establishTimeout.C: t.Fatalf("tunnel establish timeout exceeded") } for _, paveFile := range paveFiles { u, _ := url.Parse(obfuscatedServerListRootURL) u.Path = path.Join(u.Path, paveFile.Name) etag, _ := GetUrlETag(u.String()) md5sum := md5.Sum(paveFile.Contents) if etag != hex.EncodeToString(md5sum[:]) { t.Fatalf("unexpected ETag for %s", u) } } }