github.com/astaguna/popon-core@v0.0.0-20231019235610-96e42d76a5ff/psiphon/serverApi.go (about) 1 /* 2 * Copyright (c) 2015, 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 "bytes" 24 "context" 25 "encoding/base64" 26 "encoding/hex" 27 "encoding/json" 28 "fmt" 29 "io" 30 "io/ioutil" 31 "net" 32 "net/http" 33 "net/url" 34 "strconv" 35 "strings" 36 "time" 37 38 "github.com/astaguna/popon-core/psiphon/common" 39 "github.com/astaguna/popon-core/psiphon/common/buildinfo" 40 "github.com/astaguna/popon-core/psiphon/common/errors" 41 "github.com/astaguna/popon-core/psiphon/common/fragmentor" 42 "github.com/astaguna/popon-core/psiphon/common/parameters" 43 "github.com/astaguna/popon-core/psiphon/common/prng" 44 "github.com/astaguna/popon-core/psiphon/common/protocol" 45 "github.com/astaguna/popon-core/psiphon/common/tactics" 46 "github.com/astaguna/popon-core/psiphon/transferstats" 47 ) 48 49 // ServerContext is a utility struct which holds all of the data associated 50 // with a Psiphon server connection. In addition to the established tunnel, this 51 // includes data and transport mechanisms for Psiphon API requests. Legacy servers 52 // offer the Psiphon API through a web service; newer servers offer the Psiphon 53 // API through SSH requests made directly through the tunnel's SSH client. 54 type ServerContext struct { 55 tunnel *Tunnel 56 psiphonHttpsClient *http.Client 57 statsRegexps *transferstats.Regexps 58 clientUpgradeVersion string 59 serverHandshakeTimestamp string 60 paddingPRNG *prng.PRNG 61 } 62 63 // MakeSessionId creates a new session ID. The same session ID is used across 64 // multi-tunnel controller runs, where each tunnel has its own ServerContext 65 // instance. 66 // In server-side stats, we now consider a "session" to be the lifetime of the 67 // Controller (e.g., the user's commanded start and stop) and we measure this 68 // duration as well as the duration of each tunnel within the session. 69 func MakeSessionId() (string, error) { 70 randomId, err := common.MakeSecureRandomBytes(protocol.PSIPHON_API_CLIENT_SESSION_ID_LENGTH) 71 if err != nil { 72 return "", errors.Trace(err) 73 } 74 return hex.EncodeToString(randomId), nil 75 } 76 77 // NewServerContext makes the tunneled handshake request to the Psiphon server 78 // and returns a ServerContext struct for use with subsequent Psiphon server API 79 // requests (e.g., periodic connected and status requests). 80 func NewServerContext(tunnel *Tunnel) (*ServerContext, error) { 81 82 // For legacy servers, set up psiphonHttpsClient for 83 // accessing the Psiphon API via the web service. 84 var psiphonHttpsClient *http.Client 85 if !tunnel.dialParams.ServerEntry.SupportsSSHAPIRequests() || 86 tunnel.config.TargetApiProtocol == protocol.PSIPHON_WEB_API_PROTOCOL { 87 88 var err error 89 psiphonHttpsClient, err = makePsiphonHttpsClient(tunnel) 90 if err != nil { 91 return nil, errors.Trace(err) 92 } 93 } 94 95 serverContext := &ServerContext{ 96 tunnel: tunnel, 97 psiphonHttpsClient: psiphonHttpsClient, 98 paddingPRNG: prng.NewPRNGWithSeed(tunnel.dialParams.APIRequestPaddingSeed), 99 } 100 101 ignoreRegexps := tunnel.config.GetParameters().Get().Bool( 102 parameters.IgnoreHandshakeStatsRegexps) 103 104 err := serverContext.doHandshakeRequest(ignoreRegexps) 105 if err != nil { 106 return nil, errors.Trace(err) 107 } 108 109 return serverContext, nil 110 } 111 112 // doHandshakeRequest performs the "handshake" API request. The handshake 113 // returns upgrade info, newly discovered server entries -- which are 114 // stored -- and sponsor info (home pages, stat regexes). 115 func (serverContext *ServerContext) doHandshakeRequest( 116 ignoreStatsRegexps bool) error { 117 118 params := serverContext.getBaseAPIParameters(baseParametersAll) 119 120 // The server will return a signed copy of its own server entry when the 121 // client specifies this 'missing_server_entry_signature' parameter. 122 // 123 // The purpose of this mechanism is to rapidly upgrade client local storage 124 // from unsigned to signed server entries, and to ensure that the client has 125 // a signed server entry for its currently connected server as required for 126 // the client-to-client exchange feature. 127 // 128 // The server entry will be included in handshakeResponse.EncodedServerList, 129 // along side discovery servers. 130 requestedMissingSignature := false 131 if !serverContext.tunnel.dialParams.ServerEntry.HasSignature() { 132 requestedMissingSignature = true 133 params["missing_server_entry_signature"] = 134 serverContext.tunnel.dialParams.ServerEntry.Tag 135 } 136 137 doTactics := !serverContext.tunnel.config.DisableTactics 138 139 networkID := "" 140 if doTactics { 141 142 // Limitation: it is assumed that the network ID obtained here is the 143 // one that is active when the handshake request is received by the 144 // server. However, it is remotely possible to switch networks 145 // immediately after invoking the GetNetworkID callback and initiating 146 // the handshake, if the tunnel protocol is meek. 147 // 148 // The response handling code below calls GetNetworkID again and ignores 149 // any tactics payload if the network ID is not the same. While this 150 // doesn't detect all cases of changing networks, it reduces the already 151 // narrow window. 152 153 networkID = serverContext.tunnel.config.GetNetworkID() 154 155 err := tactics.SetTacticsAPIParameters( 156 GetTacticsStorer(serverContext.tunnel.config), 157 networkID, 158 params) 159 if err != nil { 160 return errors.Trace(err) 161 } 162 } 163 164 // When split tunnel mode is enabled, indicate this to the server. When 165 // indicated, the server will perform split tunnel classifications on TCP 166 // port forwards and reject, with a distinct response, port forwards which 167 // the client should connect to directly, untunneled. 168 if serverContext.tunnel.config.SplitTunnelOwnRegion { 169 params["split_tunnel"] = "1" 170 } 171 172 // While regular split tunnel mode makes untunneled connections to 173 // destinations in the client's own country, selected split tunnel mode 174 // allows the client to specify a list of untunneled countries. Either or 175 // both modes may be enabled. 176 if len(serverContext.tunnel.config.SplitTunnelRegions) > 0 { 177 params["split_tunnel_regions"] = serverContext.tunnel.config.SplitTunnelRegions 178 } 179 180 var response []byte 181 if serverContext.psiphonHttpsClient == nil { 182 183 params[protocol.PSIPHON_API_HANDSHAKE_AUTHORIZATIONS] = 184 serverContext.tunnel.config.GetAuthorizations() 185 186 request, err := serverContext.makeSSHAPIRequestPayload(params) 187 if err != nil { 188 return errors.Trace(err) 189 } 190 191 response, err = serverContext.tunnel.SendAPIRequest( 192 protocol.PSIPHON_API_HANDSHAKE_REQUEST_NAME, request) 193 if err != nil { 194 return errors.Trace(err) 195 } 196 197 } else { 198 199 // Legacy web service API request 200 201 responseBody, err := serverContext.doGetRequest( 202 makeRequestUrl(serverContext.tunnel, "", "handshake", params)) 203 if err != nil { 204 return errors.Trace(err) 205 } 206 // Skip legacy format lines and just parse the JSON config line 207 configLinePrefix := []byte("Config: ") 208 for _, line := range bytes.Split(responseBody, []byte("\n")) { 209 if bytes.HasPrefix(line, configLinePrefix) { 210 response = line[len(configLinePrefix):] 211 break 212 } 213 } 214 if len(response) == 0 { 215 return errors.TraceNew("no config line found") 216 } 217 } 218 219 // Legacy fields: 220 // - 'preemptive_reconnect_lifetime_milliseconds' is unused and ignored 221 // - 'ssh_session_id' is ignored; client session ID is used instead 222 223 var handshakeResponse protocol.HandshakeResponse 224 225 // Initialize these fields to distinguish between psiphond omitting values in 226 // the response and the zero value, which means unlimited rate. 227 handshakeResponse.UpstreamBytesPerSecond = -1 228 handshakeResponse.DownstreamBytesPerSecond = -1 229 230 err := json.Unmarshal(response, &handshakeResponse) 231 if err != nil { 232 return errors.Trace(err) 233 } 234 235 if serverContext.tunnel.config.EmitClientAddress { 236 NoticeClientAddress(handshakeResponse.ClientAddress) 237 } 238 239 NoticeClientRegion(handshakeResponse.ClientRegion) 240 241 // Emit a SplitTunnelRegions notice indicating active split tunnel region. 242 // For SplitTunnelOwnRegion, the handshake ClientRegion is the split 243 // tunnel region and this region is always listed first. 244 245 splitTunnelRegions := []string{} 246 if serverContext.tunnel.config.SplitTunnelOwnRegion { 247 splitTunnelRegions = []string{handshakeResponse.ClientRegion} 248 } 249 for _, region := range serverContext.tunnel.config.SplitTunnelRegions { 250 if !serverContext.tunnel.config.SplitTunnelOwnRegion || 251 region != handshakeResponse.ClientRegion { 252 253 splitTunnelRegions = append(splitTunnelRegions, region) 254 } 255 } 256 if len(splitTunnelRegions) > 0 { 257 NoticeSplitTunnelRegions(splitTunnelRegions) 258 } 259 260 var serverEntries []protocol.ServerEntryFields 261 262 // Store discovered server entries 263 // We use the server's time, as it's available here, for the server entry 264 // timestamp since this is more reliable than the client time. 265 for _, encodedServerEntry := range handshakeResponse.EncodedServerList { 266 267 serverEntryFields, err := protocol.DecodeServerEntryFields( 268 encodedServerEntry, 269 common.TruncateTimestampToHour(handshakeResponse.ServerTimestamp), 270 protocol.SERVER_ENTRY_SOURCE_DISCOVERY) 271 if err != nil { 272 return errors.Trace(err) 273 } 274 275 // Retain the original timestamp and source in the requestedMissingSignature 276 // case, as this server entry was not discovered here. 277 // 278 // Limitation: there is a transient edge case where 279 // requestedMissingSignature will be set for a discovery server entry that 280 // _is_ also discovered here. 281 if requestedMissingSignature && 282 serverEntryFields.GetIPAddress() == serverContext.tunnel.dialParams.ServerEntry.IpAddress { 283 284 serverEntryFields.SetLocalTimestamp(serverContext.tunnel.dialParams.ServerEntry.LocalTimestamp) 285 serverEntryFields.SetLocalSource(serverContext.tunnel.dialParams.ServerEntry.LocalSource) 286 } 287 288 err = protocol.ValidateServerEntryFields(serverEntryFields) 289 if err != nil { 290 // Skip this entry and continue with the next one 291 NoticeWarning("invalid handshake server entry: %s", err) 292 continue 293 } 294 295 serverEntries = append(serverEntries, serverEntryFields) 296 } 297 298 err = StoreServerEntries( 299 serverContext.tunnel.config, 300 serverEntries, 301 true) 302 if err != nil { 303 return errors.Trace(err) 304 } 305 306 NoticeHomepages(handshakeResponse.Homepages) 307 308 serverContext.clientUpgradeVersion = handshakeResponse.UpgradeClientVersion 309 if handshakeResponse.UpgradeClientVersion != "" { 310 NoticeClientUpgradeAvailable(handshakeResponse.UpgradeClientVersion) 311 } else { 312 NoticeClientIsLatestVersion("") 313 } 314 315 if !ignoreStatsRegexps { 316 317 // The handshake returns page_view_regexes and https_request_regexes. 318 // page_view_regexes is obsolete and not used. https_request_regexes, which 319 // are actually host/domain name regexes, are used for host/domain name 320 // bytes transferred metrics: tunneled traffic TLS SNI server names and HTTP 321 // Host header host names are matched against these regexes to select flows 322 // for bytes transferred counting. 323 324 var regexpsNotices []string 325 serverContext.statsRegexps, regexpsNotices = transferstats.MakeRegexps( 326 handshakeResponse.HttpsRequestRegexes) 327 328 for _, notice := range regexpsNotices { 329 NoticeWarning(notice) 330 } 331 } 332 333 diagnosticID := serverContext.tunnel.dialParams.ServerEntry.GetDiagnosticID() 334 335 serverContext.serverHandshakeTimestamp = handshakeResponse.ServerTimestamp 336 NoticeServerTimestamp(diagnosticID, serverContext.serverHandshakeTimestamp) 337 338 NoticeActiveAuthorizationIDs(diagnosticID, handshakeResponse.ActiveAuthorizationIDs) 339 340 NoticeTrafficRateLimits( 341 diagnosticID, 342 handshakeResponse.UpstreamBytesPerSecond, 343 handshakeResponse.DownstreamBytesPerSecond) 344 345 if doTactics && handshakeResponse.TacticsPayload != nil && 346 networkID == serverContext.tunnel.config.GetNetworkID() { 347 348 var payload *tactics.Payload 349 err := json.Unmarshal(handshakeResponse.TacticsPayload, &payload) 350 if err != nil { 351 return errors.Trace(err) 352 } 353 354 // handshakeResponse.TacticsPayload may be "null", and payload 355 // will successfully unmarshal as nil. As a result, the previous 356 // handshakeResponse.TacticsPayload != nil test is insufficient. 357 if payload != nil { 358 359 tacticsRecord, err := tactics.HandleTacticsPayload( 360 GetTacticsStorer(serverContext.tunnel.config), 361 networkID, 362 payload) 363 if err != nil { 364 return errors.Trace(err) 365 } 366 367 if tacticsRecord != nil && 368 prng.FlipWeightedCoin(tacticsRecord.Tactics.Probability) { 369 370 err := serverContext.tunnel.config.SetParameters( 371 tacticsRecord.Tag, true, tacticsRecord.Tactics.Parameters) 372 if err != nil { 373 NoticeInfo("apply handshake tactics failed: %s", err) 374 } 375 // The error will be due to invalid tactics values 376 // from the server. When SetParameters fails, all 377 // previous tactics values are left in place. 378 } 379 } 380 } 381 382 return nil 383 } 384 385 // DoConnectedRequest performs the "connected" API request. This request is 386 // used for statistics, including unique user counting; reporting the full 387 // tunnel establishment duration including the handshake request; and updated 388 // fragmentor metrics. 389 // 390 // Users are not assigned identifiers. Instead, daily unique users are 391 // calculated by having clients submit their last connected timestamp 392 // (truncated to an hour, as a privacy measure). As client clocks are 393 // unreliable, the server returns new last_connected values for the client to 394 // store and send next time it connects. 395 func (serverContext *ServerContext) DoConnectedRequest() error { 396 397 // Limitation: as currently implemented, the last_connected exchange isn't a 398 // distributed, atomic operation. When clients send the connected request, 399 // the server may receive the request, count a unique user based on the 400 // client's last_connected, and then the tunnel fails before the client 401 // receives the response, so the client will not update its last_connected 402 // value and submit the same one again, resulting in an inflated unique user 403 // count. 404 // 405 // The SetInFlightConnectedRequest mechanism mitigates one class of connected 406 // request interruption, a commanded shutdown in the middle of a connected 407 // request, by allowing some time for the request to complete before 408 // terminating the tunnel. 409 // 410 // TODO: consider extending the connected request protocol with additional 411 // "acknowledgment" messages so that the server does not commit its unique 412 // user count until after the client has acknowledged receipt and durable 413 // storage of the new last_connected value. 414 415 requestDone := make(chan struct{}) 416 defer close(requestDone) 417 418 if !serverContext.tunnel.SetInFlightConnectedRequest(requestDone) { 419 return errors.TraceNew("tunnel is closing") 420 } 421 defer serverContext.tunnel.SetInFlightConnectedRequest(nil) 422 423 params := serverContext.getBaseAPIParameters( 424 baseParametersOnlyUpstreamFragmentorDialParameters) 425 426 lastConnected, err := getLastConnected() 427 if err != nil { 428 return errors.Trace(err) 429 } 430 431 params["last_connected"] = lastConnected 432 433 // serverContext.tunnel.establishDuration is nanoseconds; report milliseconds 434 params["establishment_duration"] = 435 fmt.Sprintf("%d", serverContext.tunnel.establishDuration/time.Millisecond) 436 437 var response []byte 438 if serverContext.psiphonHttpsClient == nil { 439 440 request, err := serverContext.makeSSHAPIRequestPayload(params) 441 if err != nil { 442 return errors.Trace(err) 443 } 444 445 response, err = serverContext.tunnel.SendAPIRequest( 446 protocol.PSIPHON_API_CONNECTED_REQUEST_NAME, request) 447 if err != nil { 448 return errors.Trace(err) 449 } 450 451 } else { 452 453 // Legacy web service API request 454 455 response, err = serverContext.doGetRequest( 456 makeRequestUrl(serverContext.tunnel, "", "connected", params)) 457 if err != nil { 458 return errors.Trace(err) 459 } 460 } 461 462 var connectedResponse protocol.ConnectedResponse 463 err = json.Unmarshal(response, &connectedResponse) 464 if err != nil { 465 return errors.Trace(err) 466 } 467 468 err = SetKeyValue( 469 datastoreLastConnectedKey, connectedResponse.ConnectedTimestamp) 470 if err != nil { 471 return errors.Trace(err) 472 } 473 474 return nil 475 } 476 477 func getLastConnected() (string, error) { 478 lastConnected, err := GetKeyValue(datastoreLastConnectedKey) 479 if err != nil { 480 return "", errors.Trace(err) 481 } 482 if lastConnected == "" { 483 lastConnected = "None" 484 } 485 return lastConnected, nil 486 } 487 488 // StatsRegexps gets the Regexps used for the statistics for this tunnel. 489 func (serverContext *ServerContext) StatsRegexps() *transferstats.Regexps { 490 return serverContext.statsRegexps 491 } 492 493 // DoStatusRequest makes a "status" API request to the server, sending session stats. 494 func (serverContext *ServerContext) DoStatusRequest(tunnel *Tunnel) error { 495 496 params := serverContext.getBaseAPIParameters(baseParametersNoDialParameters) 497 498 // Note: ensure putBackStatusRequestPayload is called, to replace 499 // payload for future attempt, in all failure cases. 500 501 statusPayload, statusPayloadInfo, err := makeStatusRequestPayload( 502 serverContext.tunnel.config, 503 tunnel.dialParams.ServerEntry.IpAddress) 504 if err != nil { 505 return errors.Trace(err) 506 } 507 508 // Skip the request when there's no payload to send. 509 510 if len(statusPayload) == 0 { 511 return nil 512 } 513 514 var response []byte 515 516 if serverContext.psiphonHttpsClient == nil { 517 518 rawMessage := json.RawMessage(statusPayload) 519 params["statusData"] = &rawMessage 520 521 var request []byte 522 request, err = serverContext.makeSSHAPIRequestPayload(params) 523 524 if err == nil { 525 response, err = serverContext.tunnel.SendAPIRequest( 526 protocol.PSIPHON_API_STATUS_REQUEST_NAME, request) 527 } 528 529 } else { 530 531 // Legacy web service API request 532 response, err = serverContext.doPostRequest( 533 makeRequestUrl(serverContext.tunnel, "", "status", params), 534 "application/json", 535 bytes.NewReader(statusPayload)) 536 } 537 538 if err != nil { 539 540 // Resend the transfer stats and tunnel stats later 541 // Note: potential duplicate reports if the server received and processed 542 // the request but the client failed to receive the response. 543 putBackStatusRequestPayload(statusPayloadInfo) 544 545 return errors.Trace(err) 546 } 547 548 confirmStatusRequestPayload(statusPayloadInfo) 549 550 var statusResponse protocol.StatusResponse 551 err = json.Unmarshal(response, &statusResponse) 552 if err != nil { 553 return errors.Trace(err) 554 } 555 556 for _, serverEntryTag := range statusResponse.InvalidServerEntryTags { 557 PruneServerEntry(serverContext.tunnel.config, serverEntryTag) 558 } 559 560 return nil 561 } 562 563 // statusRequestPayloadInfo is a temporary structure for data used to 564 // either "clear" or "put back" status request payload data depending 565 // on whether or not the request succeeded. 566 type statusRequestPayloadInfo struct { 567 serverId string 568 transferStats *transferstats.AccumulatedStats 569 persistentStats map[string][][]byte 570 } 571 572 func makeStatusRequestPayload( 573 config *Config, 574 serverId string) ([]byte, *statusRequestPayloadInfo, error) { 575 576 transferStats := transferstats.TakeOutStatsForServer(serverId) 577 hostBytes := transferStats.GetStatsForStatusRequest() 578 579 persistentStats, err := TakeOutUnreportedPersistentStats(config) 580 if err != nil { 581 NoticeWarning( 582 "TakeOutUnreportedPersistentStats failed: %s", errors.Trace(err)) 583 persistentStats = nil 584 // Proceed with transferStats only 585 } 586 587 if len(hostBytes) == 0 && len(persistentStats) == 0 { 588 // There is no payload to send. 589 return nil, nil, nil 590 } 591 592 payloadInfo := &statusRequestPayloadInfo{ 593 serverId, transferStats, persistentStats} 594 595 payload := make(map[string]interface{}) 596 597 payload["host_bytes"] = hostBytes 598 599 // We're not recording these fields, but legacy servers require them. 600 payload["bytes_transferred"] = 0 601 payload["page_views"] = make([]string, 0) 602 payload["https_requests"] = make([]string, 0) 603 604 persistentStatPayloadNames := make(map[string]string) 605 persistentStatPayloadNames[datastorePersistentStatTypeRemoteServerList] = "remote_server_list_stats" 606 persistentStatPayloadNames[datastorePersistentStatTypeFailedTunnel] = "failed_tunnel_stats" 607 608 for statType, stats := range persistentStats { 609 610 // Persistent stats records are already in JSON format 611 jsonStats := make([]json.RawMessage, len(stats)) 612 for i, stat := range stats { 613 jsonStats[i] = json.RawMessage(stat) 614 } 615 payload[persistentStatPayloadNames[statType]] = jsonStats 616 } 617 618 jsonPayload, err := json.Marshal(payload) 619 if err != nil { 620 621 // Send the transfer stats and tunnel stats later 622 putBackStatusRequestPayload(payloadInfo) 623 624 return nil, nil, errors.Trace(err) 625 } 626 627 return jsonPayload, payloadInfo, nil 628 } 629 630 func putBackStatusRequestPayload(payloadInfo *statusRequestPayloadInfo) { 631 transferstats.PutBackStatsForServer( 632 payloadInfo.serverId, payloadInfo.transferStats) 633 err := PutBackUnreportedPersistentStats(payloadInfo.persistentStats) 634 if err != nil { 635 // These persistent stats records won't be resent until after a 636 // datastore re-initialization. 637 NoticeWarning( 638 "PutBackUnreportedPersistentStats failed: %s", errors.Trace(err)) 639 } 640 } 641 642 func confirmStatusRequestPayload(payloadInfo *statusRequestPayloadInfo) { 643 err := ClearReportedPersistentStats(payloadInfo.persistentStats) 644 if err != nil { 645 // These persistent stats records may be resent. 646 NoticeWarning( 647 "ClearReportedPersistentStats failed: %s", errors.Trace(err)) 648 } 649 } 650 651 // RecordRemoteServerListStat records a completed common or OSL remote server 652 // list resource download. 653 // 654 // The RSL download event could occur when the client is unable to immediately 655 // send a status request to a server, so these records are stored in the 656 // persistent datastore and reported via subsequent status requests sent to 657 // any Psiphon server. 658 // 659 // Note that some common event field values may change between the stat 660 // recording and reporting, including client geolocation and host_id. 661 // 662 // The bytes/duration fields reflect the size and download time for the _last 663 // chunk only_ in the case of a resumed download. The purpose of these fields 664 // is to calculate rough data transfer rates. Both bytes and duration are 665 // included in the log, to allow for filtering out of small transfers which 666 // may not produce accurate rate numbers. 667 // 668 // Multiple "status" requests may be in flight at once (due to multi-tunnel, 669 // asynchronous final status retry, and aggressive status requests for 670 // pre-registered tunnels), To avoid duplicate reporting, persistent stats 671 // records are "taken-out" by a status request and then "put back" in case the 672 // request fails. 673 // 674 // Duplicate reporting may also occur when a server receives and processes a 675 // status request but the client fails to receive the response. 676 func RecordRemoteServerListStat( 677 config *Config, 678 tunneled bool, 679 url string, 680 etag string, 681 bytes int64, 682 duration time.Duration, 683 authenticated bool, 684 additionalParameters common.APIParameters) error { 685 686 if !config.GetParameters().Get().WeightedCoinFlip( 687 parameters.RecordRemoteServerListPersistentStatsProbability) { 688 return nil 689 } 690 691 params := make(common.APIParameters) 692 693 params["session_id"] = config.SessionID 694 params["propagation_channel_id"] = config.PropagationChannelId 695 params["sponsor_id"] = config.GetSponsorID() 696 params["client_version"] = config.ClientVersion 697 params["client_platform"] = config.ClientPlatform 698 params["client_build_rev"] = buildinfo.GetBuildInfo().BuildRev 699 if config.DeviceRegion != "" { 700 params["device_region"] = config.DeviceRegion 701 } 702 703 params["client_download_timestamp"] = common.TruncateTimestampToHour(common.GetCurrentTimestamp()) 704 tunneledStr := "0" 705 if tunneled { 706 tunneledStr = "1" 707 } 708 params["tunneled"] = tunneledStr 709 params["url"] = url 710 params["etag"] = etag 711 params["bytes"] = fmt.Sprintf("%d", bytes) 712 713 // duration is nanoseconds; report milliseconds 714 params["duration"] = fmt.Sprintf("%d", duration/time.Millisecond) 715 716 authenticatedStr := "0" 717 if authenticated { 718 authenticatedStr = "1" 719 } 720 params["authenticated"] = authenticatedStr 721 722 for k, v := range additionalParameters { 723 params[k] = v 724 } 725 726 remoteServerListStatJson, err := json.Marshal(params) 727 if err != nil { 728 return errors.Trace(err) 729 } 730 731 return StorePersistentStat( 732 config, datastorePersistentStatTypeRemoteServerList, remoteServerListStatJson) 733 } 734 735 // RecordFailedTunnelStat records metrics for a failed tunnel dial, including 736 // dial parameters and error condition (tunnelErr). No record is created when 737 // tunnelErr is nil. 738 // 739 // This uses the same reporting facility, with the same caveats, as 740 // RecordRemoteServerListStat. 741 func RecordFailedTunnelStat( 742 config *Config, 743 dialParams *DialParameters, 744 livenessTestMetrics *livenessTestMetrics, 745 bytesUp int64, 746 bytesDown int64, 747 tunnelErr error) error { 748 749 probability := config.GetParameters().Get().Float( 750 parameters.RecordFailedTunnelPersistentStatsProbability) 751 752 if !prng.FlipWeightedCoin(probability) { 753 return nil 754 } 755 756 // Callers should not call RecordFailedTunnelStat with a nil tunnelErr, as 757 // this is not a useful stat and it results in a nil pointer dereference. 758 // This check catches potential bug cases. An example edge case, now 759 // fixed, is deferred error handlers, such as the ones in in 760 // dialTunnel/tunnel.Activate, which may be invoked in the case of a 761 // panic, which can occur before any error value is returned. 762 if tunnelErr == nil { 763 return errors.TraceNew("no error") 764 } 765 766 lastConnected, err := getLastConnected() 767 if err != nil { 768 return errors.Trace(err) 769 } 770 771 params := getBaseAPIParameters(baseParametersAll, config, dialParams) 772 773 delete(params, "server_secret") 774 params["server_entry_tag"] = dialParams.ServerEntry.Tag 775 params["last_connected"] = lastConnected 776 params["client_failed_timestamp"] = common.TruncateTimestampToHour(common.GetCurrentTimestamp()) 777 if livenessTestMetrics != nil { 778 params["liveness_test_upstream_bytes"] = strconv.Itoa(livenessTestMetrics.UpstreamBytes) 779 params["liveness_test_sent_upstream_bytes"] = strconv.Itoa(livenessTestMetrics.SentUpstreamBytes) 780 params["liveness_test_downstream_bytes"] = strconv.Itoa(livenessTestMetrics.DownstreamBytes) 781 params["liveness_test_received_downstream_bytes"] = strconv.Itoa(livenessTestMetrics.ReceivedDownstreamBytes) 782 } 783 if bytesUp >= 0 { 784 params["bytes_up"] = fmt.Sprintf("%d", bytesUp) 785 } 786 if bytesDown >= 0 { 787 params["bytes_down"] = fmt.Sprintf("%d", bytesDown) 788 } 789 790 // Log RecordFailedTunnelPersistentStatsProbability to indicate the 791 // proportion of failed tunnel events being recorded at the time of 792 // this log event. 793 params["record_probability"] = fmt.Sprintf("%f", probability) 794 795 // Ensure direct server IPs are not exposed in logs. The "net" package, and 796 // possibly other 3rd party packages, will include destination addresses in 797 // I/O error messages. 798 tunnelError := common.RedactIPAddressesString(tunnelErr.Error()) 799 params["tunnel_error"] = tunnelError 800 801 failedTunnelStatJson, err := json.Marshal(params) 802 if err != nil { 803 return errors.Trace(err) 804 } 805 806 return StorePersistentStat( 807 config, datastorePersistentStatTypeFailedTunnel, failedTunnelStatJson) 808 } 809 810 // doGetRequest makes a tunneled HTTPS request and returns the response body. 811 func (serverContext *ServerContext) doGetRequest( 812 requestUrl string) (responseBody []byte, err error) { 813 814 request, err := http.NewRequest("GET", requestUrl, nil) 815 if err != nil { 816 return nil, errors.Trace(err) 817 } 818 819 request.Header.Set("User-Agent", MakePsiphonUserAgent(serverContext.tunnel.config)) 820 821 response, err := serverContext.psiphonHttpsClient.Do(request) 822 if err == nil && response.StatusCode != http.StatusOK { 823 response.Body.Close() 824 err = fmt.Errorf("HTTP GET request failed with response code: %d", response.StatusCode) 825 } 826 if err != nil { 827 // Trim this error since it may include long URLs 828 return nil, errors.Trace(TrimError(err)) 829 } 830 defer response.Body.Close() 831 body, err := ioutil.ReadAll(response.Body) 832 if err != nil { 833 return nil, errors.Trace(err) 834 } 835 return body, nil 836 } 837 838 // doPostRequest makes a tunneled HTTPS POST request. 839 func (serverContext *ServerContext) doPostRequest( 840 requestUrl string, bodyType string, body io.Reader) (responseBody []byte, err error) { 841 842 request, err := http.NewRequest("POST", requestUrl, body) 843 if err != nil { 844 return nil, errors.Trace(err) 845 } 846 847 request.Header.Set("User-Agent", MakePsiphonUserAgent(serverContext.tunnel.config)) 848 request.Header.Set("Content-Type", bodyType) 849 850 response, err := serverContext.psiphonHttpsClient.Do(request) 851 if err == nil && response.StatusCode != http.StatusOK { 852 response.Body.Close() 853 err = fmt.Errorf("HTTP POST request failed with response code: %d", response.StatusCode) 854 } 855 if err != nil { 856 // Trim this error since it may include long URLs 857 return nil, errors.Trace(TrimError(err)) 858 } 859 defer response.Body.Close() 860 responseBody, err = ioutil.ReadAll(response.Body) 861 if err != nil { 862 return nil, errors.Trace(err) 863 } 864 return responseBody, nil 865 } 866 867 // makeSSHAPIRequestPayload makes a JSON payload for an SSH API request. 868 func (serverContext *ServerContext) makeSSHAPIRequestPayload( 869 params common.APIParameters) ([]byte, error) { 870 jsonPayload, err := json.Marshal(params) 871 if err != nil { 872 return nil, errors.Trace(err) 873 } 874 return jsonPayload, nil 875 } 876 877 type baseParametersFilter int 878 879 const ( 880 baseParametersAll baseParametersFilter = iota 881 baseParametersOnlyUpstreamFragmentorDialParameters 882 baseParametersNoDialParameters 883 ) 884 885 func (serverContext *ServerContext) getBaseAPIParameters( 886 filter baseParametersFilter) common.APIParameters { 887 888 params := getBaseAPIParameters( 889 filter, 890 serverContext.tunnel.config, 891 serverContext.tunnel.dialParams) 892 893 // Add a random amount of padding to defend against API call traffic size 894 // fingerprints. The "pad_response" field instructs the server to pad its 895 // response accordingly. 896 897 p := serverContext.tunnel.config.GetParameters().Get() 898 minUpstreamPadding := p.Int(parameters.APIRequestUpstreamPaddingMinBytes) 899 maxUpstreamPadding := p.Int(parameters.APIRequestUpstreamPaddingMaxBytes) 900 minDownstreamPadding := p.Int(parameters.APIRequestDownstreamPaddingMinBytes) 901 maxDownstreamPadding := p.Int(parameters.APIRequestDownstreamPaddingMaxBytes) 902 903 if maxUpstreamPadding > 0 { 904 size := serverContext.paddingPRNG.Range(minUpstreamPadding, maxUpstreamPadding) 905 params["padding"] = strings.Repeat(" ", size) 906 } 907 908 if maxDownstreamPadding > 0 { 909 size := serverContext.paddingPRNG.Range(minDownstreamPadding, maxDownstreamPadding) 910 params["pad_response"] = strconv.Itoa(size) 911 } 912 913 return params 914 } 915 916 // getBaseAPIParameters returns all the common API parameters that are 917 // included with each Psiphon API request. These common parameters are used 918 // for metrics. 919 func getBaseAPIParameters( 920 filter baseParametersFilter, 921 config *Config, 922 dialParams *DialParameters) common.APIParameters { 923 924 params := make(common.APIParameters) 925 926 params["session_id"] = config.SessionID 927 params["client_session_id"] = config.SessionID 928 params["server_secret"] = dialParams.ServerEntry.WebServerSecret 929 params["propagation_channel_id"] = config.PropagationChannelId 930 params["sponsor_id"] = config.GetSponsorID() 931 params["client_version"] = config.ClientVersion 932 params["client_platform"] = config.ClientPlatform 933 params["client_features"] = config.clientFeatures 934 params["client_build_rev"] = buildinfo.GetBuildInfo().BuildRev 935 936 // Blank parameters must be omitted. 937 938 if config.DeviceRegion != "" { 939 params["device_region"] = config.DeviceRegion 940 } 941 942 if filter == baseParametersAll { 943 944 params["relay_protocol"] = dialParams.TunnelProtocol 945 params["network_type"] = dialParams.GetNetworkType() 946 947 if dialParams.BPFProgramName != "" { 948 params["client_bpf"] = dialParams.BPFProgramName 949 } 950 951 if dialParams.SelectedSSHClientVersion { 952 params["ssh_client_version"] = dialParams.SSHClientVersion 953 } 954 955 if dialParams.UpstreamProxyType != "" { 956 params["upstream_proxy_type"] = dialParams.UpstreamProxyType 957 } 958 959 if dialParams.UpstreamProxyCustomHeaderNames != nil { 960 params["upstream_proxy_custom_header_names"] = dialParams.UpstreamProxyCustomHeaderNames 961 } 962 963 if dialParams.FrontingProviderID != "" { 964 params["fronting_provider_id"] = dialParams.FrontingProviderID 965 } 966 967 if dialParams.MeekDialAddress != "" { 968 params["meek_dial_address"] = dialParams.MeekDialAddress 969 } 970 971 if protocol.TunnelProtocolUsesFrontedMeek(dialParams.TunnelProtocol) { 972 meekResolvedIPAddress := dialParams.MeekResolvedIPAddress.Load().(string) 973 if meekResolvedIPAddress != "" { 974 params["meek_resolved_ip_address"] = meekResolvedIPAddress 975 } 976 } 977 978 if dialParams.MeekSNIServerName != "" { 979 params["meek_sni_server_name"] = dialParams.MeekSNIServerName 980 } 981 982 if dialParams.MeekHostHeader != "" { 983 params["meek_host_header"] = dialParams.MeekHostHeader 984 } 985 986 // MeekTransformedHostName is meaningful when meek is used, which is when 987 // MeekDialAddress != "" 988 if dialParams.MeekDialAddress != "" { 989 transformedHostName := "0" 990 if dialParams.MeekTransformedHostName { 991 transformedHostName = "1" 992 } 993 params["meek_transformed_host_name"] = transformedHostName 994 } 995 996 if dialParams.TLSOSSHSNIServerName != "" { 997 params["tls_ossh_sni_server_name"] = dialParams.TLSOSSHSNIServerName 998 } 999 1000 if dialParams.TLSOSSHTransformedSNIServerName { 1001 params["tls_ossh_transformed_host_name"] = "1" 1002 } 1003 1004 if dialParams.SelectedUserAgent { 1005 params["user_agent"] = dialParams.UserAgent 1006 } 1007 1008 if dialParams.SelectedTLSProfile { 1009 params["tls_profile"] = dialParams.TLSProfile 1010 params["tls_version"] = dialParams.GetTLSVersionForMetrics() 1011 } 1012 1013 if dialParams.ServerEntry.Region != "" { 1014 params["server_entry_region"] = dialParams.ServerEntry.Region 1015 } 1016 1017 if dialParams.ServerEntry.LocalSource != "" { 1018 params["server_entry_source"] = dialParams.ServerEntry.LocalSource 1019 } 1020 1021 // As with last_connected, this timestamp stat, which may be a precise 1022 // handshake request server timestamp, is truncated to hour granularity to 1023 // avoid introducing a reconstructable cross-session user trace into server 1024 // logs. 1025 localServerEntryTimestamp := common.TruncateTimestampToHour( 1026 dialParams.ServerEntry.LocalTimestamp) 1027 if localServerEntryTimestamp != "" { 1028 params["server_entry_timestamp"] = localServerEntryTimestamp 1029 } 1030 1031 params[tactics.APPLIED_TACTICS_TAG_PARAMETER_NAME] = 1032 config.GetParameters().Get().Tag() 1033 1034 if dialParams.DialPortNumber != "" { 1035 params["dial_port_number"] = dialParams.DialPortNumber 1036 } 1037 1038 if dialParams.QUICVersion != "" { 1039 params["quic_version"] = dialParams.QUICVersion 1040 } 1041 1042 if dialParams.QUICDialSNIAddress != "" { 1043 params["quic_dial_sni_address"] = dialParams.QUICDialSNIAddress 1044 } 1045 1046 if dialParams.QUICDisablePathMTUDiscovery { 1047 params["quic_disable_client_path_mtu_discovery"] = "1" 1048 } 1049 1050 isReplay := "0" 1051 if dialParams.IsReplay { 1052 isReplay = "1" 1053 } 1054 params["is_replay"] = isReplay 1055 1056 if config.EgressRegion != "" { 1057 params["egress_region"] = config.EgressRegion 1058 } 1059 1060 // dialParams.DialDuration is nanoseconds; report milliseconds 1061 params["dial_duration"] = fmt.Sprintf("%d", dialParams.DialDuration/time.Millisecond) 1062 1063 params["candidate_number"] = strconv.Itoa(dialParams.CandidateNumber) 1064 1065 params["established_tunnels_count"] = strconv.Itoa(dialParams.EstablishedTunnelsCount) 1066 1067 if dialParams.NetworkLatencyMultiplier != 0.0 { 1068 params["network_latency_multiplier"] = 1069 fmt.Sprintf("%f", dialParams.NetworkLatencyMultiplier) 1070 } 1071 1072 if dialParams.ConjureTransport != "" { 1073 params["conjure_transport"] = dialParams.ConjureTransport 1074 } 1075 1076 if dialParams.ResolveParameters != nil { 1077 1078 if dialParams.ResolveParameters.PreresolvedIPAddress != "" { 1079 params["dns_preresolved"] = dialParams.ResolveParameters.PreresolvedIPAddress 1080 1081 } else { 1082 1083 // Log enough information to distinguish several successful or 1084 // failed circumvention cases of interest, including preferring 1085 // alternate servers and/or using DNS protocol transforms, and 1086 // appropriate for both handshake and failed_tunnel logging: 1087 // 1088 // - The initial attempt made by Resolver.ResolveIP, 1089 // preferring an alternate DNS server and/or using a 1090 // protocol transform succeeds (dns_result = 0, the initial 1091 // attempt, 0, got the first result). 1092 // 1093 // - A second attempt may be used, still preferring an 1094 // alternate DNS server but no longer using the protocol 1095 // transform, which presumably failed (dns_result = 1, the 1096 // second attempt, 1, got the first result). 1097 // 1098 // - Subsequent attempts will use the system DNS server and no 1099 // protocol transforms (dns_result > 2). 1100 // 1101 // Due to the design of Resolver.ResolveIP, the notion 1102 // of "success" is approximate; for example a successful 1103 // response may arrive after a subsequent attempt succeeds, 1104 // simply due to slow network conditions. It's also possible 1105 // that, for a given attemp, only one of the two concurrent 1106 // requests (A and AAAA) succeeded. 1107 // 1108 // Note that ResolveParameters.GetFirstAttemptWithAnswer 1109 // semantics assume that dialParams.ResolveParameters wasn't 1110 // used by or modified by any other dial. 1111 1112 if dialParams.ResolveParameters.PreferAlternateDNSServer { 1113 params["dns_preferred"] = dialParams.ResolveParameters.AlternateDNSServer 1114 } 1115 1116 if dialParams.ResolveParameters.ProtocolTransformName != "" { 1117 params["dns_transform"] = dialParams.ResolveParameters.ProtocolTransformName 1118 } 1119 1120 params["dns_attempt"] = strconv.Itoa( 1121 dialParams.ResolveParameters.GetFirstAttemptWithAnswer()) 1122 } 1123 } 1124 1125 if dialParams.HTTPTransformerParameters != nil { 1126 if dialParams.HTTPTransformerParameters.ProtocolTransformSpec != nil { 1127 params["http_transform"] = dialParams.HTTPTransformerParameters.ProtocolTransformName 1128 } 1129 } 1130 1131 if dialParams.OSSHObfuscatorSeedTransformerParameters != nil { 1132 if dialParams.OSSHObfuscatorSeedTransformerParameters.TransformSpec != nil { 1133 params["seed_transform"] = dialParams.OSSHObfuscatorSeedTransformerParameters.TransformName 1134 } 1135 } 1136 1137 if dialParams.ObfuscatedQUICNonceTransformerParameters != nil { 1138 if dialParams.ObfuscatedQUICNonceTransformerParameters.TransformSpec != nil { 1139 params["seed_transform"] = dialParams.ObfuscatedQUICNonceTransformerParameters.TransformName 1140 } 1141 } 1142 1143 if dialParams.OSSHPrefixSpec != nil { 1144 if dialParams.OSSHPrefixSpec.Spec != nil { 1145 params["ossh_prefix"] = dialParams.OSSHPrefixSpec.Name 1146 } 1147 } 1148 1149 if dialParams.DialConnMetrics != nil { 1150 metrics := dialParams.DialConnMetrics.GetMetrics() 1151 for name, value := range metrics { 1152 params[name] = fmt.Sprintf("%v", value) 1153 } 1154 } 1155 1156 if dialParams.ObfuscatedSSHConnMetrics != nil { 1157 metrics := dialParams.ObfuscatedSSHConnMetrics.GetMetrics() 1158 for name, value := range metrics { 1159 params[name] = fmt.Sprintf("%v", value) 1160 } 1161 } 1162 1163 } else if filter == baseParametersOnlyUpstreamFragmentorDialParameters { 1164 1165 if dialParams.DialConnMetrics != nil { 1166 names := fragmentor.GetUpstreamMetricsNames() 1167 metrics := dialParams.DialConnMetrics.GetMetrics() 1168 for name, value := range metrics { 1169 if common.Contains(names, name) { 1170 params[name] = fmt.Sprintf("%v", value) 1171 } 1172 } 1173 } 1174 } 1175 1176 return params 1177 } 1178 1179 // makeRequestUrl makes a URL for a web service API request. 1180 func makeRequestUrl(tunnel *Tunnel, port, path string, params common.APIParameters) string { 1181 var requestUrl bytes.Buffer 1182 1183 if port == "" { 1184 port = tunnel.dialParams.ServerEntry.WebServerPort 1185 } 1186 1187 requestUrl.WriteString("https://") 1188 requestUrl.WriteString(tunnel.dialParams.ServerEntry.IpAddress) 1189 requestUrl.WriteString(":") 1190 requestUrl.WriteString(port) 1191 requestUrl.WriteString("/") 1192 requestUrl.WriteString(path) 1193 1194 if len(params) > 0 { 1195 1196 queryParams := url.Values{} 1197 1198 for name, value := range params { 1199 1200 // Note: this logic skips the tactics.SPEED_TEST_SAMPLES_PARAMETER_NAME 1201 // parameter, which has a different type. This parameter is not recognized 1202 // by legacy servers. 1203 1204 switch v := value.(type) { 1205 case string: 1206 queryParams.Set(name, v) 1207 case []string: 1208 // String array param encoded as JSON 1209 jsonValue, err := json.Marshal(v) 1210 if err != nil { 1211 break 1212 } 1213 queryParams.Set(name, string(jsonValue)) 1214 } 1215 } 1216 1217 requestUrl.WriteString("?") 1218 requestUrl.WriteString(queryParams.Encode()) 1219 } 1220 1221 return requestUrl.String() 1222 } 1223 1224 // makePsiphonHttpsClient creates a Psiphon HTTPS client that tunnels web service API 1225 // requests and which validates the web server using the Psiphon server entry web server 1226 // certificate. 1227 func makePsiphonHttpsClient(tunnel *Tunnel) (httpsClient *http.Client, err error) { 1228 1229 certificate, err := DecodeCertificate( 1230 tunnel.dialParams.ServerEntry.WebServerCertificate) 1231 if err != nil { 1232 return nil, errors.Trace(err) 1233 } 1234 1235 tunneledDialer := func(_ context.Context, _, addr string) (net.Conn, error) { 1236 // This case bypasses tunnel.Dial, to avoid its check that the tunnel is 1237 // already active (it won't be pre-handshake). This bypass won't handle the 1238 // server rejecting the port forward due to split tunnel classification, but 1239 // we know that the server won't classify the web API destination as 1240 // untunneled. 1241 return tunnel.sshClient.Dial("tcp", addr) 1242 } 1243 1244 // Note: as with SSH API requests, there no dial context here. SSH port forward dials 1245 // cannot be interrupted directly. Closing the tunnel will interrupt both the dial and 1246 // the request. While it's possible to add a timeout here, we leave it with no explicit 1247 // timeout which is the same as SSH API requests: if the tunnel has stalled then SSH keep 1248 // alives will cause the tunnel to close. 1249 1250 dialer := NewCustomTLSDialer( 1251 &CustomTLSConfig{ 1252 Parameters: tunnel.config.GetParameters(), 1253 Dial: tunneledDialer, 1254 VerifyLegacyCertificate: certificate, 1255 }) 1256 1257 transport := &http.Transport{ 1258 DialTLS: func(network, addr string) (net.Conn, error) { 1259 return dialer(context.Background(), network, addr) 1260 }, 1261 Dial: func(network, addr string) (net.Conn, error) { 1262 return nil, errors.TraceNew("HTTP not supported") 1263 }, 1264 } 1265 1266 return &http.Client{ 1267 Transport: transport, 1268 }, nil 1269 } 1270 1271 func HandleServerRequest( 1272 tunnelOwner TunnelOwner, tunnel *Tunnel, name string, payload []byte) error { 1273 1274 switch name { 1275 case protocol.PSIPHON_API_OSL_REQUEST_NAME: 1276 return HandleOSLRequest(tunnelOwner, tunnel, payload) 1277 case protocol.PSIPHON_API_ALERT_REQUEST_NAME: 1278 return HandleAlertRequest(tunnelOwner, tunnel, payload) 1279 } 1280 1281 return errors.Tracef("invalid request name: %s", name) 1282 } 1283 1284 func HandleOSLRequest( 1285 tunnelOwner TunnelOwner, tunnel *Tunnel, payload []byte) error { 1286 1287 var oslRequest protocol.OSLRequest 1288 err := json.Unmarshal(payload, &oslRequest) 1289 if err != nil { 1290 return errors.Trace(err) 1291 } 1292 1293 if oslRequest.ClearLocalSLOKs { 1294 DeleteSLOKs() 1295 } 1296 1297 seededNewSLOK := false 1298 1299 for _, slok := range oslRequest.SeedPayload.SLOKs { 1300 duplicate, err := SetSLOK(slok.ID, slok.Key) 1301 if err != nil { 1302 // TODO: return error to trigger retry? 1303 NoticeWarning("SetSLOK failed: %s", errors.Trace(err)) 1304 } else if !duplicate { 1305 seededNewSLOK = true 1306 } 1307 1308 if tunnel.config.EmitSLOKs { 1309 NoticeSLOKSeeded(base64.StdEncoding.EncodeToString(slok.ID), duplicate) 1310 } 1311 } 1312 1313 if seededNewSLOK { 1314 tunnelOwner.SignalSeededNewSLOK() 1315 } 1316 1317 return nil 1318 } 1319 1320 func HandleAlertRequest( 1321 tunnelOwner TunnelOwner, tunnel *Tunnel, payload []byte) error { 1322 1323 var alertRequest protocol.AlertRequest 1324 err := json.Unmarshal(payload, &alertRequest) 1325 if err != nil { 1326 return errors.Trace(err) 1327 } 1328 1329 if tunnel.config.EmitServerAlerts { 1330 NoticeServerAlert(alertRequest) 1331 } 1332 1333 return nil 1334 }