1package engine 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "io/ioutil" 8 "net/http" 9 "net/url" 10 "os" 11 "sync" 12 13 "github.com/ooni/probe-cli/v3/internal/engine/atomicx" 14 "github.com/ooni/probe-cli/v3/internal/engine/geolocate" 15 "github.com/ooni/probe-cli/v3/internal/engine/internal/platform" 16 "github.com/ooni/probe-cli/v3/internal/engine/internal/sessionresolver" 17 "github.com/ooni/probe-cli/v3/internal/engine/kvstore" 18 "github.com/ooni/probe-cli/v3/internal/engine/model" 19 "github.com/ooni/probe-cli/v3/internal/engine/netx" 20 "github.com/ooni/probe-cli/v3/internal/engine/netx/bytecounter" 21 "github.com/ooni/probe-cli/v3/internal/engine/probeservices" 22 "github.com/ooni/probe-cli/v3/internal/engine/tunnel" 23 "github.com/ooni/probe-cli/v3/internal/version" 24) 25 26// SessionConfig contains the Session config 27type SessionConfig struct { 28 AvailableProbeServices []model.Service 29 KVStore KVStore 30 Logger model.Logger 31 ProxyURL *url.URL 32 SoftwareName string 33 SoftwareVersion string 34 TempDir string 35 TorArgs []string 36 TorBinary string 37 38 // TunnelDir is the directory where we should store 39 // the state of persistent tunnels. This field is 40 // optional _unless_ you want to use tunnels. In such 41 // case, starting a tunnel will fail because there 42 // is no directory where to store state. 43 TunnelDir string 44} 45 46// Session is a measurement session. It contains shared information 47// required to run a measurement session, and it controls the lifecycle 48// of such resources. It is not possible to reuse a Session. You MUST 49// NOT attempt to use a Session again after Session.Close. 50type Session struct { 51 availableProbeServices []model.Service 52 availableTestHelpers map[string][]model.Service 53 byteCounter *bytecounter.Counter 54 httpDefaultTransport netx.HTTPRoundTripper 55 kvStore model.KeyValueStore 56 location *geolocate.Results 57 logger model.Logger 58 proxyURL *url.URL 59 queryProbeServicesCount *atomicx.Int64 60 resolver *sessionresolver.Resolver 61 selectedProbeServiceHook func(*model.Service) 62 selectedProbeService *model.Service 63 softwareName string 64 softwareVersion string 65 tempDir string 66 67 // closeOnce allows us to call Close just once. 68 closeOnce sync.Once 69 70 // mu provides mutual exclusion. 71 mu sync.Mutex 72 73 // testLookupLocationContext is a an optional hook for testing 74 // allowing us to mock LookupLocationContext. 75 testLookupLocationContext func(ctx context.Context) (*geolocate.Results, error) 76 77 // testMaybeLookupBackendsContext is an optional hook for testing 78 // allowing us to mock MaybeLookupBackendsContext. 79 testMaybeLookupBackendsContext func(ctx context.Context) error 80 81 // testMaybeLookupLocationContext is an optional hook for testing 82 // allowing us to mock MaybeLookupLocationContext. 83 testMaybeLookupLocationContext func(ctx context.Context) error 84 85 // testNewProbeServicesClientForCheckIn is an optional hook for testing 86 // allowing us to mock NewProbeServicesClient when calling CheckIn. 87 testNewProbeServicesClientForCheckIn func(ctx context.Context) ( 88 sessionProbeServicesClientForCheckIn, error) 89 90 // torArgs contains the optional arguments for tor that we may need 91 // to pass to urlgetter when it uses a tor tunnel. 92 torArgs []string 93 94 // torBinary contains the optional path to the tor binary that we 95 // may need to pass to urlgetter when it uses a tor tunnel. 96 torBinary string 97 98 // tunnel is the optional tunnel that we may be using. It is created 99 // by NewSession and it is cleaned up by Close. 100 tunnel tunnel.Tunnel 101} 102 103// sessionProbeServicesClientForCheckIn returns the probe services 104// client that we should be using for performing the check-in. 105type sessionProbeServicesClientForCheckIn interface { 106 CheckIn(ctx context.Context, config model.CheckInConfig) (*model.CheckInInfo, error) 107} 108 109// NewSession creates a new session. This factory function will 110// execute the following steps: 111// 112// 1. Make sure the config is sane, apply reasonable defaults 113// where possible, otherwise return an error. 114// 115// 2. Create a temporary directory. 116// 117// 3. Create an instance of the session. 118// 119// 4. If the user requested for a proxy that entails a tunnel (at the 120// moment of writing this note, either psiphon or tor), then start the 121// requested tunnel and configure it as our proxy. 122// 123// 5. Create a compound resolver for the session that will attempt 124// to use a bunch of DoT/DoH servers before falling back to the system 125// resolver if nothing else works (see the sessionresolver pkg). This 126// sessionresolver will be using the configured proxy, if any. 127// 128// 6. Create the default HTTP transport that we should be using when 129// we communicate with the OONI backends. This transport will be 130// using the configured proxy, if any. 131// 132// If any of these steps fails, then we cannot create a measurement 133// session and we return an error. 134func NewSession(ctx context.Context, config SessionConfig) (*Session, error) { 135 if config.Logger == nil { 136 return nil, errors.New("Logger is empty") 137 } 138 if config.SoftwareName == "" { 139 return nil, errors.New("SoftwareName is empty") 140 } 141 if config.SoftwareVersion == "" { 142 return nil, errors.New("SoftwareVersion is empty") 143 } 144 if config.KVStore == nil { 145 config.KVStore = kvstore.NewMemoryKeyValueStore() 146 } 147 // Implementation note: if config.TempDir is empty, then Go will 148 // use the temporary directory on the current system. This should 149 // work on Desktop. We tested that it did also work on iOS, but 150 // we have also seen on 2020-06-10 that it does not work on Android. 151 tempDir, err := ioutil.TempDir(config.TempDir, "ooniengine") 152 if err != nil { 153 return nil, err 154 } 155 sess := &Session{ 156 availableProbeServices: config.AvailableProbeServices, 157 byteCounter: bytecounter.New(), 158 kvStore: config.KVStore, 159 logger: config.Logger, 160 queryProbeServicesCount: atomicx.NewInt64(), 161 softwareName: config.SoftwareName, 162 softwareVersion: config.SoftwareVersion, 163 tempDir: tempDir, 164 torArgs: config.TorArgs, 165 torBinary: config.TorBinary, 166 } 167 proxyURL := config.ProxyURL 168 if proxyURL != nil { 169 switch proxyURL.Scheme { 170 case "psiphon", "tor", "fake": 171 config.Logger.Infof( 172 "starting '%s' tunnel; please be patient...", proxyURL.Scheme) 173 tunnel, err := tunnel.Start(ctx, &tunnel.Config{ 174 Logger: config.Logger, 175 Name: proxyURL.Scheme, 176 Session: &sessionTunnelEarlySession{}, 177 TorArgs: config.TorArgs, 178 TorBinary: config.TorBinary, 179 TunnelDir: config.TunnelDir, 180 }) 181 if err != nil { 182 return nil, err 183 } 184 config.Logger.Infof("tunnel '%s' running...", proxyURL.Scheme) 185 sess.tunnel = tunnel 186 proxyURL = tunnel.SOCKS5ProxyURL() 187 } 188 } 189 sess.proxyURL = proxyURL 190 httpConfig := netx.Config{ 191 ByteCounter: sess.byteCounter, 192 BogonIsError: true, 193 Logger: sess.logger, 194 ProxyURL: proxyURL, 195 } 196 sess.resolver = &sessionresolver.Resolver{ 197 ByteCounter: sess.byteCounter, 198 KVStore: config.KVStore, 199 Logger: sess.logger, 200 ProxyURL: proxyURL, 201 } 202 httpConfig.FullResolver = sess.resolver 203 sess.httpDefaultTransport = netx.NewHTTPTransport(httpConfig) 204 return sess, nil 205} 206 207// KibiBytesReceived accounts for the KibiBytes received by the HTTP clients 208// managed by this session so far, including experiments. 209func (s *Session) KibiBytesReceived() float64 { 210 return s.byteCounter.KibiBytesReceived() 211} 212 213// KibiBytesSent is like KibiBytesReceived but for the bytes sent. 214func (s *Session) KibiBytesSent() float64 { 215 return s.byteCounter.KibiBytesSent() 216} 217 218// CheckIn calls the check-in API. The input arguments MUST NOT 219// be nil. Before querying the API, this function will ensure 220// that the config structure does not contain any field that 221// SHOULD be initialized and is not initialized. Whenever there 222// is a field that is not initialized, we will attempt to set 223// a reasonable default value for such a field. This list describes 224// the current defaults we'll choose: 225// 226// - Platform: if empty, set to Session.Platform(); 227// 228// - ProbeASN: if empty, set to Session.ProbeASNString(); 229// 230// - ProbeCC: if empty, set to Session.ProbeCC(); 231// 232// - RunType: if empty, set to "timed"; 233// 234// - SoftwareName: if empty, set to Session.SoftwareName(); 235// 236// - SoftwareVersion: if empty, set to Session.SoftwareVersion(); 237// 238// - WebConnectivity.CategoryCodes: if nil, we will allocate 239// an empty array (the API does not like nil). 240// 241// Because we MAY need to know the current ASN and CC, this 242// function MAY call MaybeLookupLocationContext. 243// 244// The return value is either the check-in response or an error. 245func (s *Session) CheckIn( 246 ctx context.Context, config *model.CheckInConfig) (*model.CheckInInfo, error) { 247 if err := s.maybeLookupLocationContext(ctx); err != nil { 248 return nil, err 249 } 250 client, err := s.newProbeServicesClientForCheckIn(ctx) 251 if err != nil { 252 return nil, err 253 } 254 if config.Platform == "" { 255 config.Platform = s.Platform() 256 } 257 if config.ProbeASN == "" { 258 config.ProbeASN = s.ProbeASNString() 259 } 260 if config.ProbeCC == "" { 261 config.ProbeCC = s.ProbeCC() 262 } 263 if config.RunType == "" { 264 config.RunType = "timed" // most conservative choice 265 } 266 if config.SoftwareName == "" { 267 config.SoftwareName = s.SoftwareName() 268 } 269 if config.SoftwareVersion == "" { 270 config.SoftwareVersion = s.SoftwareVersion() 271 } 272 if config.WebConnectivity.CategoryCodes == nil { 273 config.WebConnectivity.CategoryCodes = []string{} 274 } 275 return client.CheckIn(ctx, *config) 276} 277 278// maybeLookupLocationContext is a wrapper for MaybeLookupLocationContext that calls 279// the configurable testMaybeLookupLocationContext mock, if configured, and the 280// real MaybeLookupLocationContext API otherwise. 281func (s *Session) maybeLookupLocationContext(ctx context.Context) error { 282 if s.testMaybeLookupLocationContext != nil { 283 return s.testMaybeLookupLocationContext(ctx) 284 } 285 return s.MaybeLookupLocationContext(ctx) 286} 287 288// newProbeServicesClientForCheckIn is a wrapper for NewProbeServicesClientForCheckIn 289// that calls the configurable testNewProbeServicesClientForCheckIn mock, if 290// configured, and the real NewProbeServicesClient API otherwise. 291func (s *Session) newProbeServicesClientForCheckIn( 292 ctx context.Context) (sessionProbeServicesClientForCheckIn, error) { 293 if s.testNewProbeServicesClientForCheckIn != nil { 294 return s.testNewProbeServicesClientForCheckIn(ctx) 295 } 296 client, err := s.NewProbeServicesClient(ctx) 297 if err != nil { 298 return nil, err 299 } 300 return client, nil 301} 302 303// Close ensures that we close all the idle connections that the HTTP clients 304// we are currently using may have created. It will also remove the temp dir 305// that contains data from this session. Not calling this function may likely 306// cause memory leaks in your application because of open idle connections, 307// as well as excessive usage of disk space. 308func (s *Session) Close() error { 309 s.closeOnce.Do(s.doClose) 310 return nil 311} 312 313// doClose implements Close. This function is called just once. 314func (s *Session) doClose() { 315 s.httpDefaultTransport.CloseIdleConnections() 316 s.resolver.CloseIdleConnections() 317 s.logger.Infof("%s", s.resolver.Stats()) 318 if s.tunnel != nil { 319 s.tunnel.Stop() 320 } 321 _ = os.RemoveAll(s.tempDir) 322} 323 324// GetTestHelpersByName returns the available test helpers that 325// use the specified name, or false if there's none. 326func (s *Session) GetTestHelpersByName(name string) ([]model.Service, bool) { 327 defer s.mu.Unlock() 328 s.mu.Lock() 329 services, ok := s.availableTestHelpers[name] 330 return services, ok 331} 332 333// DefaultHTTPClient returns the session's default HTTP client. 334func (s *Session) DefaultHTTPClient() *http.Client { 335 return &http.Client{Transport: s.httpDefaultTransport} 336} 337 338// FetchTorTargets fetches tor targets from the API. 339func (s *Session) FetchTorTargets( 340 ctx context.Context, cc string) (map[string]model.TorTarget, error) { 341 clnt, err := s.NewOrchestraClient(ctx) 342 if err != nil { 343 return nil, err 344 } 345 return clnt.FetchTorTargets(ctx, cc) 346} 347 348// FetchURLList fetches the URL list from the API. 349func (s *Session) FetchURLList( 350 ctx context.Context, config model.URLListConfig) ([]model.URLInfo, error) { 351 clnt, err := s.NewOrchestraClient(ctx) 352 if err != nil { 353 return nil, err 354 } 355 return clnt.FetchURLList(ctx, config) 356} 357 358// KeyValueStore returns the configured key-value store. 359func (s *Session) KeyValueStore() model.KeyValueStore { 360 return s.kvStore 361} 362 363// Logger returns the logger used by the session. 364func (s *Session) Logger() model.Logger { 365 return s.logger 366} 367 368// MaybeLookupLocation is a caching location lookup call. 369func (s *Session) MaybeLookupLocation() error { 370 return s.MaybeLookupLocationContext(context.Background()) 371} 372 373// MaybeLookupBackends is a caching OONI backends lookup call. 374func (s *Session) MaybeLookupBackends() error { 375 return s.MaybeLookupBackendsContext(context.Background()) 376} 377 378// ErrAlreadyUsingProxy indicates that we cannot create a tunnel with 379// a specific name because we already configured a proxy. 380var ErrAlreadyUsingProxy = errors.New( 381 "session: cannot create a new tunnel of this kind: we are already using a proxy", 382) 383 384// NewExperimentBuilder returns a new experiment builder 385// for the experiment with the given name, or an error if 386// there's no such experiment with the given name 387func (s *Session) NewExperimentBuilder(name string) (*ExperimentBuilder, error) { 388 return newExperimentBuilder(s, name) 389} 390 391// NewProbeServicesClient creates a new client for talking with the 392// OONI probe services. This function will benchmark the available 393// probe services, and select the fastest. In case all probe services 394// seem to be down, we try again applying circumvention tactics. 395// This function will fail IMMEDIATELY if given a cancelled context. 396func (s *Session) NewProbeServicesClient(ctx context.Context) (*probeservices.Client, error) { 397 if ctx.Err() != nil { 398 return nil, ctx.Err() // helps with testing 399 } 400 if err := s.maybeLookupBackendsContext(ctx); err != nil { 401 return nil, err 402 } 403 if err := s.maybeLookupLocationContext(ctx); err != nil { 404 return nil, err 405 } 406 if s.selectedProbeServiceHook != nil { 407 s.selectedProbeServiceHook(s.selectedProbeService) 408 } 409 return probeservices.NewClient(s, *s.selectedProbeService) 410} 411 412// NewSubmitter creates a new submitter instance. 413func (s *Session) NewSubmitter(ctx context.Context) (Submitter, error) { 414 psc, err := s.NewProbeServicesClient(ctx) 415 if err != nil { 416 return nil, err 417 } 418 return probeservices.NewSubmitter(psc, s.Logger()), nil 419} 420 421// NewOrchestraClient creates a new orchestra client. This client is registered 422// and logged in with the OONI orchestra. An error is returned on failure. 423// 424// This function is DEPRECATED. New code SHOULD NOT use it. It will eventually 425// be made private or entirely removed from the codebase. 426func (s *Session) NewOrchestraClient(ctx context.Context) (*probeservices.Client, error) { 427 clnt, err := s.NewProbeServicesClient(ctx) 428 if err != nil { 429 return nil, err 430 } 431 return s.initOrchestraClient(ctx, clnt, clnt.MaybeLogin) 432} 433 434// Platform returns the current platform. The platform is one of: 435// 436// - android 437// - ios 438// - linux 439// - macos 440// - windows 441// - unknown 442// 443// When running on the iOS simulator, the returned platform is 444// macos rather than ios if CGO is disabled. This is a known issue, 445// that however should have a very limited impact. 446func (s *Session) Platform() string { 447 return platform.Name() 448} 449 450// ProbeASNString returns the probe ASN as a string. 451func (s *Session) ProbeASNString() string { 452 return fmt.Sprintf("AS%d", s.ProbeASN()) 453} 454 455// ProbeASN returns the probe ASN as an integer. 456func (s *Session) ProbeASN() uint { 457 defer s.mu.Unlock() 458 s.mu.Lock() 459 asn := geolocate.DefaultProbeASN 460 if s.location != nil { 461 asn = s.location.ASN 462 } 463 return asn 464} 465 466// ProbeCC returns the probe CC. 467func (s *Session) ProbeCC() string { 468 defer s.mu.Unlock() 469 s.mu.Lock() 470 cc := geolocate.DefaultProbeCC 471 if s.location != nil { 472 cc = s.location.CountryCode 473 } 474 return cc 475} 476 477// ProbeNetworkName returns the probe network name. 478func (s *Session) ProbeNetworkName() string { 479 defer s.mu.Unlock() 480 s.mu.Lock() 481 nn := geolocate.DefaultProbeNetworkName 482 if s.location != nil { 483 nn = s.location.NetworkName 484 } 485 return nn 486} 487 488// ProbeIP returns the probe IP. 489func (s *Session) ProbeIP() string { 490 defer s.mu.Unlock() 491 s.mu.Lock() 492 ip := geolocate.DefaultProbeIP 493 if s.location != nil { 494 ip = s.location.ProbeIP 495 } 496 return ip 497} 498 499// ProxyURL returns the Proxy URL, or nil if not set 500func (s *Session) ProxyURL() *url.URL { 501 return s.proxyURL 502} 503 504// ResolverASNString returns the resolver ASN as a string 505func (s *Session) ResolverASNString() string { 506 return fmt.Sprintf("AS%d", s.ResolverASN()) 507} 508 509// ResolverASN returns the resolver ASN 510func (s *Session) ResolverASN() uint { 511 defer s.mu.Unlock() 512 s.mu.Lock() 513 asn := geolocate.DefaultResolverASN 514 if s.location != nil { 515 asn = s.location.ResolverASN 516 } 517 return asn 518} 519 520// ResolverIP returns the resolver IP 521func (s *Session) ResolverIP() string { 522 defer s.mu.Unlock() 523 s.mu.Lock() 524 ip := geolocate.DefaultResolverIP 525 if s.location != nil { 526 ip = s.location.ResolverIP 527 } 528 return ip 529} 530 531// ResolverNetworkName returns the resolver network name. 532func (s *Session) ResolverNetworkName() string { 533 defer s.mu.Unlock() 534 s.mu.Lock() 535 nn := geolocate.DefaultResolverNetworkName 536 if s.location != nil { 537 nn = s.location.ResolverNetworkName 538 } 539 return nn 540} 541 542// SoftwareName returns the application name. 543func (s *Session) SoftwareName() string { 544 return s.softwareName 545} 546 547// SoftwareVersion returns the application version. 548func (s *Session) SoftwareVersion() string { 549 return s.softwareVersion 550} 551 552// TempDir returns the temporary directory. 553func (s *Session) TempDir() string { 554 return s.tempDir 555} 556 557// TorArgs returns the configured extra args for the tor binary. If not set 558// we will not pass in any extra arg. Applies to `-OTunnel=tor` mainly. 559func (s *Session) TorArgs() []string { 560 return s.torArgs 561} 562 563// TorBinary returns the configured path to the tor binary. If not set 564// we will attempt to use "tor". Applies to `-OTunnel=tor` mainly. 565func (s *Session) TorBinary() string { 566 return s.torBinary 567} 568 569// UserAgent constructs the user agent to be used in this session. 570func (s *Session) UserAgent() (useragent string) { 571 useragent += s.softwareName + "/" + s.softwareVersion 572 useragent += " ooniprobe-engine/" + version.Version 573 return 574} 575 576// getAvailableProbeServicesUnlocked returns the available probe 577// services. This function WILL NOT acquire the mu mutex, therefore, 578// you MUST ensure you are using it from a locked context. 579func (s *Session) getAvailableProbeServicesUnlocked() []model.Service { 580 if len(s.availableProbeServices) > 0 { 581 return s.availableProbeServices 582 } 583 return probeservices.Default() 584} 585 586func (s *Session) initOrchestraClient( 587 ctx context.Context, clnt *probeservices.Client, 588 maybeLogin func(ctx context.Context) error, 589) (*probeservices.Client, error) { 590 // The original implementation has as its only use case that we 591 // were registering and logging in for sending an update regarding 592 // the probe whereabouts. Yet here in probe-engine, the orchestra 593 // is currently only used to fetch inputs. For this purpose, we don't 594 // need to communicate any specific information. The code that will 595 // perform an update used to be responsible of doing that. Now, we 596 // are not using orchestra for this purpose anymore. 597 meta := probeservices.Metadata{ 598 Platform: "miniooni", 599 ProbeASN: "AS0", 600 ProbeCC: "ZZ", 601 SoftwareName: "miniooni", 602 SoftwareVersion: "0.1.0-dev", 603 SupportedTests: []string{"web_connectivity"}, 604 } 605 if err := clnt.MaybeRegister(ctx, meta); err != nil { 606 return nil, err 607 } 608 if err := maybeLogin(ctx); err != nil { 609 return nil, err 610 } 611 return clnt, nil 612} 613 614// ErrAllProbeServicesFailed indicates all probe services failed. 615var ErrAllProbeServicesFailed = errors.New("all available probe services failed") 616 617// maybeLookupBackendsContext uses testMaybeLookupBackendsContext if 618// not nil, otherwise it calls MaybeLookupBackendsContext. 619func (s *Session) maybeLookupBackendsContext(ctx context.Context) error { 620 if s.testMaybeLookupBackendsContext != nil { 621 return s.testMaybeLookupBackendsContext(ctx) 622 } 623 return s.MaybeLookupBackendsContext(ctx) 624} 625 626// MaybeLookupBackendsContext is like MaybeLookupBackends but with context. 627func (s *Session) MaybeLookupBackendsContext(ctx context.Context) error { 628 defer s.mu.Unlock() 629 s.mu.Lock() 630 if s.selectedProbeService != nil { 631 return nil 632 } 633 s.queryProbeServicesCount.Add(1) 634 candidates := probeservices.TryAll(ctx, s, s.getAvailableProbeServicesUnlocked()) 635 selected := probeservices.SelectBest(candidates) 636 if selected == nil { 637 return ErrAllProbeServicesFailed 638 } 639 s.logger.Infof("session: using probe services: %+v", selected.Endpoint) 640 s.selectedProbeService = &selected.Endpoint 641 s.availableTestHelpers = selected.TestHelpers 642 return nil 643} 644 645// LookupLocationContext performs a location lookup. If you want memoisation 646// of the results, you should use MaybeLookupLocationContext. 647func (s *Session) LookupLocationContext(ctx context.Context) (*geolocate.Results, error) { 648 task := geolocate.Must(geolocate.NewTask(geolocate.Config{ 649 Logger: s.Logger(), 650 Resolver: s.resolver, 651 UserAgent: s.UserAgent(), 652 })) 653 return task.Run(ctx) 654} 655 656// lookupLocationContext calls testLookupLocationContext if set and 657// otherwise calls LookupLocationContext. 658func (s *Session) lookupLocationContext(ctx context.Context) (*geolocate.Results, error) { 659 if s.testLookupLocationContext != nil { 660 return s.testLookupLocationContext(ctx) 661 } 662 return s.LookupLocationContext(ctx) 663} 664 665// MaybeLookupLocationContext is like MaybeLookupLocation but with a context 666// that can be used to interrupt this long running operation. This function 667// will fail IMMEDIATELY if given a cancelled context. 668func (s *Session) MaybeLookupLocationContext(ctx context.Context) error { 669 if ctx.Err() != nil { 670 return ctx.Err() // helps with testing 671 } 672 defer s.mu.Unlock() 673 s.mu.Lock() 674 if s.location == nil { 675 location, err := s.lookupLocationContext(ctx) 676 if err != nil { 677 return err 678 } 679 s.location = location 680 } 681 return nil 682} 683 684var _ model.ExperimentSession = &Session{} 685