github.com/psiphon-labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/exchange_test.go (about) 1 /* 2 * Copyright (c) 2019, Psiphon Inc. 3 * All rights reserved. 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package psiphon 21 22 import ( 23 "encoding/base64" 24 "fmt" 25 "io/ioutil" 26 "os" 27 "testing" 28 29 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common" 30 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng" 31 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol" 32 ) 33 34 func TestServerEntryExchange(t *testing.T) { 35 36 // Prepare an empty database 37 38 testDataDirName, err := ioutil.TempDir("", "psiphon-exchange-test") 39 if err != nil { 40 t.Fatalf("TempDir failed: %s", err) 41 } 42 defer os.RemoveAll(testDataDirName) 43 44 SetNoticeWriter(ioutil.Discard) 45 46 // Generate signing and exchange key material 47 48 obfuscationKeyBytes, err := common.MakeSecureRandomBytes(32) 49 if err != nil { 50 t.Fatalf("MakeRandomBytes failed: %s", err) 51 } 52 53 obfuscationKey := base64.StdEncoding.EncodeToString(obfuscationKeyBytes) 54 55 publicKey, privateKey, err := protocol.NewServerEntrySignatureKeyPair() 56 if err != nil { 57 t.Fatalf("NewServerEntrySignatureKeyPair failed: %s", err) 58 } 59 60 // Initialize config required for datastore operation 61 62 networkID := prng.HexString(8) 63 64 configJSONTemplate := ` 65 { 66 "SponsorId" : "0", 67 "PropagationChannelId" : "0", 68 "ServerEntrySignaturePublicKey" : "%s", 69 "ExchangeObfuscationKey" : "%s", 70 "NetworkID" : "%s" 71 }` 72 73 configJSON := fmt.Sprintf( 74 configJSONTemplate, 75 publicKey, 76 obfuscationKey, 77 networkID) 78 79 config, err := LoadConfig([]byte(configJSON)) 80 if err != nil { 81 t.Fatalf("LoadConfig failed: %s", err) 82 } 83 84 config.DataRootDirectory = testDataDirName 85 86 err = config.Commit(false) 87 if err != nil { 88 t.Fatalf("Commit failed: %s", err) 89 } 90 91 resolver := NewResolver(config, true) 92 defer resolver.Stop() 93 config.SetResolver(resolver) 94 95 err = OpenDataStore(config) 96 if err != nil { 97 t.Fatalf("OpenDataStore failed: %s", err) 98 } 99 defer CloseDataStore() 100 101 // Generate server entries to test different cases 102 // 103 // Note: invalid signature cases are exercised in 104 // protocol.TestServerEntryListSignatures 105 106 makeServerEntryFields := func(IPAddress string) protocol.ServerEntryFields { 107 n := 16 108 fields := make(protocol.ServerEntryFields) 109 fields["ipAddress"] = IPAddress 110 fields["sshPort"] = 22 111 fields["sshUsername"] = prng.HexString(n) 112 fields["sshPassword"] = prng.HexString(n) 113 fields["sshHostKey"] = prng.HexString(n) 114 fields["sshObfuscatedPort"] = 23 115 fields["sshObfuscatedQUICPort"] = 24 116 fields["sshObfuscatedKey"] = prng.HexString(n) 117 fields["capabilities"] = []string{"SSH", "OSSH", "QUIC", "ssh-api-requests"} 118 fields["region"] = "US" 119 fields["configurationVersion"] = 1 120 return fields 121 } 122 123 serverEntry0 := makeServerEntryFields("192.168.1.1") 124 tunnelProtocol0 := "SSH" 125 126 serverEntry1 := makeServerEntryFields("192.168.1.2") 127 err = serverEntry1.AddSignature(publicKey, privateKey) 128 if err != nil { 129 t.Fatalf("AddSignature failed: %s", err) 130 } 131 tunnelProtocol1 := "OSSH" 132 133 serverEntry2 := makeServerEntryFields("192.168.1.3") 134 err = serverEntry2.AddSignature(publicKey, privateKey) 135 if err != nil { 136 t.Fatalf("AddSignature failed: %s", err) 137 } 138 tunnelProtocol2 := "QUIC-OSSH" 139 140 serverEntry3 := makeServerEntryFields("192.168.1.4") 141 err = serverEntry3.AddSignature(publicKey, privateKey) 142 if err != nil { 143 t.Fatalf("AddSignature failed: %s", err) 144 } 145 tunnelProtocol3 := "" 146 147 // paveServerEntry stores a server entry in the datastore with source 148 // EMBEDDED, promotes the server entry to the affinity/export candidate 149 // position, and generates and stores associated dial parameters when 150 // specified. This creates potential candidates for export. 151 // 152 // When tunnelProtocol is "", no dial parameters are created. 153 154 paveServerEntry := func( 155 fields protocol.ServerEntryFields, tunnelProtocol string) { 156 157 fields.SetLocalSource(protocol.SERVER_ENTRY_SOURCE_EMBEDDED) 158 fields.SetLocalTimestamp( 159 common.TruncateTimestampToHour(common.GetCurrentTimestamp())) 160 161 err = StoreServerEntry(fields, true) 162 if err != nil { 163 t.Fatalf("StoreServerEntry failed: %s", err) 164 } 165 166 err = PromoteServerEntry(config, fields["ipAddress"].(string)) 167 if err != nil { 168 t.Fatalf("PromoteServerEntry failed: %s", err) 169 } 170 171 if tunnelProtocol != "" { 172 173 serverEntry, err := fields.GetServerEntry() 174 if err != nil { 175 t.Fatalf("ServerEntryFields.GetServerEntry failed: %s", err) 176 } 177 178 canReplay := func(serverEntry *protocol.ServerEntry, replayProtocol string) bool { 179 return true 180 } 181 182 selectProtocol := func(serverEntry *protocol.ServerEntry) (string, bool) { 183 return tunnelProtocol, true 184 } 185 186 dialParams, err := MakeDialParameters( 187 config, 188 nil, 189 canReplay, 190 selectProtocol, 191 serverEntry, 192 false, 193 0, 194 0) 195 if err != nil { 196 t.Fatalf("MakeDialParameters failed: %s", err) 197 } 198 199 err = SetDialParameters(serverEntry.IpAddress, networkID, dialParams) 200 if err != nil { 201 t.Fatalf("SetDialParameters failed: %s", err) 202 } 203 } 204 } 205 206 // checkFirstServerEntry checks that the current affinity server entry has 207 // the expected ID (IP address), and that any associated, stored dial 208 // parameters are in the expected exchanged state. This is used to verify 209 // that an import has succeed and set the datastore correctly. 210 211 checkFirstServerEntry := func( 212 fields protocol.ServerEntryFields, tunnelProtocol string, isExchanged bool) { 213 214 _, iterator, err := NewServerEntryIterator(config) 215 if err != nil { 216 t.Fatalf("NewServerEntryIterator failed: %s", err) 217 } 218 defer iterator.Close() 219 220 serverEntry, err := iterator.Next() 221 if err != nil { 222 t.Fatalf("ServerEntryIterator.Next failed: %s", err) 223 } 224 if serverEntry == nil { 225 t.Fatalf("unexpected nil server entry") 226 } 227 228 if serverEntry.IpAddress != fields["ipAddress"] { 229 t.Fatalf("unexpected server entry IP address") 230 } 231 232 if isExchanged { 233 if serverEntry.LocalSource != protocol.SERVER_ENTRY_SOURCE_EXCHANGED { 234 t.Fatalf("unexpected non-exchanged server entry source") 235 } 236 } else { 237 if serverEntry.LocalSource == protocol.SERVER_ENTRY_SOURCE_EXCHANGED { 238 t.Fatalf("unexpected exchanged server entry source") 239 } 240 } 241 242 dialParams, err := GetDialParameters(config, serverEntry.IpAddress, networkID) 243 if err != nil { 244 t.Fatalf("GetDialParameters failed: %s", err) 245 } 246 247 if tunnelProtocol == "" { 248 if dialParams != nil { 249 t.Fatalf("unexpected non-nil dial parameters") 250 } 251 } else if isExchanged { 252 if !dialParams.IsExchanged { 253 t.Fatalf("unexpected non-exchanged dial parameters") 254 } 255 if dialParams.TunnelProtocol != tunnelProtocol { 256 t.Fatalf("unexpected exchanged dial parameters tunnel protocol") 257 } 258 } else { 259 if dialParams.IsExchanged { 260 t.Fatalf("unexpected exchanged dial parameters") 261 } 262 if dialParams.TunnelProtocol != tunnelProtocol { 263 t.Fatalf("unexpected dial parameters tunnel protocol") 264 } 265 } 266 } 267 268 // Test: pave only an unsigned server entry; export should fail 269 270 paveServerEntry(serverEntry0, tunnelProtocol0) 271 272 payload := ExportExchangePayload(config) 273 if payload != "" { 274 t.Fatalf("ExportExchangePayload unexpectedly succeeded") 275 } 276 277 // Test: pave two signed server entries; serverEntry2 is the affinity server 278 // entry and should be the exported server entry 279 280 paveServerEntry(serverEntry1, tunnelProtocol1) 281 paveServerEntry(serverEntry2, tunnelProtocol2) 282 283 payload = ExportExchangePayload(config) 284 if payload == "" { 285 t.Fatalf("ExportExchangePayload failed") 286 } 287 288 // Test: import; serverEntry2 should be imported 289 290 // Before importing the exported payload, move serverEntry1 to the affinity 291 // position. After the import, we expect serverEntry2 to be at the affinity 292 // position and its dial parameters to be IsExchanged and and have the 293 // exchanged tunnel protocol. 294 295 err = PromoteServerEntry(config, serverEntry1["ipAddress"].(string)) 296 if err != nil { 297 t.Fatalf("PromoteServerEntry failed: %s", err) 298 } 299 300 checkFirstServerEntry(serverEntry1, tunnelProtocol1, false) 301 302 if !ImportExchangePayload(config, payload) { 303 t.Fatalf("ImportExchangePayload failed") 304 } 305 306 checkFirstServerEntry(serverEntry2, tunnelProtocol2, true) 307 308 // Test: nil exchanged dial parameters case 309 310 paveServerEntry(serverEntry3, tunnelProtocol3) 311 312 payload = ExportExchangePayload(config) 313 if payload == "" { 314 t.Fatalf("ExportExchangePayload failed") 315 } 316 317 err = PromoteServerEntry(config, serverEntry1["ipAddress"].(string)) 318 if err != nil { 319 t.Fatalf("PromoteServerEntry failed: %s", err) 320 } 321 322 checkFirstServerEntry(serverEntry1, tunnelProtocol1, false) 323 324 if !ImportExchangePayload(config, payload) { 325 t.Fatalf("ImportExchangePayload failed") 326 } 327 328 checkFirstServerEntry(serverEntry3, tunnelProtocol3, true) 329 }