github.com/psiphon-Labs/psiphon-tunnel-core@v2.0.28+incompatible/ClientLibrary/clientlib/clientlib.go (about) 1 /* 2 * Copyright (c) 2018, 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 clientlib 21 22 import ( 23 "context" 24 "encoding/json" 25 std_errors "errors" 26 "fmt" 27 "path/filepath" 28 "sync" 29 30 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon" 31 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors" 32 ) 33 34 // Parameters provide an easier way to modify the tunnel config at runtime. 35 type Parameters struct { 36 // Used as the directory for the datastore, remote server list, and obfuscasted 37 // server list. 38 // Empty string means the default will be used (current working directory). 39 // nil means the values in the config file will be used. 40 // Optional, but strongly recommended. 41 DataRootDirectory *string 42 43 // Overrides config.ClientPlatform. See config.go for details. 44 // nil means the value in the config file will be used. 45 // Optional, but strongly recommended. 46 ClientPlatform *string 47 48 // Overrides config.NetworkID. For details see: 49 // https://godoc.org/github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon#NetworkIDGetter 50 // nil means the value in the config file will be used. (If not set in the config, 51 // an error will result.) 52 // Empty string will produce an error. 53 // Optional, but strongly recommended. 54 NetworkID *string 55 56 // Overrides config.EstablishTunnelTimeoutSeconds. See config.go for details. 57 // nil means the EstablishTunnelTimeoutSeconds value in the config file will be used. 58 // If there's no such value in the config file, the default will be used. 59 // Zero means there will be no timeout. 60 // Optional. 61 EstablishTunnelTimeoutSeconds *int 62 63 // EmitDiagnosticNoticesToFile indicates whether to use the rotating log file 64 // facility to record diagnostic notices instead of sending diagnostic 65 // notices to noticeReceiver. Has no effect unless the tunnel 66 // config.EmitDiagnosticNotices flag is set. 67 EmitDiagnosticNoticesToFiles bool 68 } 69 70 // PsiphonTunnel is the tunnel object. It can be used for stopping the tunnel and 71 // retrieving proxy ports. 72 type PsiphonTunnel struct { 73 embeddedServerListWaitGroup sync.WaitGroup 74 controllerWaitGroup sync.WaitGroup 75 stopController context.CancelFunc 76 77 // The port on which the HTTP proxy is running 78 HTTPProxyPort int 79 // The port on which the SOCKS proxy is running 80 SOCKSProxyPort int 81 } 82 83 // ParametersDelta allows for fine-grained modification of parameters.Parameters. 84 // NOTE: Ordinary users of this library should never need this. 85 type ParametersDelta map[string]interface{} 86 87 // NoticeEvent represents the notices emitted by tunnel core. It will be passed to 88 // noticeReceiver, if supplied. 89 // NOTE: Ordinary users of this library should never need this. 90 type NoticeEvent struct { 91 Data map[string]interface{} `json:"data"` 92 Type string `json:"noticeType"` 93 Timestamp string `json:"timestamp"` 94 } 95 96 // ErrTimeout is returned when the tunnel establishment attempt fails due to timeout 97 var ErrTimeout = std_errors.New("clientlib: tunnel establishment timeout") 98 99 // StartTunnel establishes a Psiphon tunnel. It returns an error if the establishment 100 // was not successful. If the returned error is nil, the returned tunnel can be used 101 // to find out the proxy ports and subsequently stop the tunnel. 102 // 103 // ctx may be cancelable, if the caller wants to be able to interrupt the establishment 104 // attempt, or context.Background(). 105 // 106 // configJSON will be passed to psiphon.LoadConfig to configure the tunnel. Required. 107 // 108 // embeddedServerEntryList is the encoded embedded server entry list. It is optional. 109 // 110 // params are config values that typically need to be overridden at runtime. 111 // 112 // paramsDelta contains changes that will be applied to the Parameters. 113 // NOTE: Ordinary users of this library should never need this and should pass nil. 114 // 115 // noticeReceiver, if non-nil, will be called for each notice emitted by tunnel core. 116 // NOTE: Ordinary users of this library should never need this and should pass nil. 117 func StartTunnel( 118 ctx context.Context, 119 configJSON []byte, 120 embeddedServerEntryList string, 121 params Parameters, 122 paramsDelta ParametersDelta, 123 noticeReceiver func(NoticeEvent)) (retTunnel *PsiphonTunnel, retErr error) { 124 125 config, err := psiphon.LoadConfig(configJSON) 126 if err != nil { 127 return nil, errors.TraceMsg(err, "failed to load config file") 128 } 129 130 // Use params.DataRootDirectory to set related config values. 131 if params.DataRootDirectory != nil { 132 config.DataRootDirectory = *params.DataRootDirectory 133 134 // Migrate old fields 135 config.MigrateDataStoreDirectory = *params.DataRootDirectory 136 config.MigrateObfuscatedServerListDownloadDirectory = *params.DataRootDirectory 137 config.MigrateRemoteServerListDownloadFilename = filepath.Join(*params.DataRootDirectory, "server_list_compressed") 138 } 139 140 if params.NetworkID != nil { 141 config.NetworkID = *params.NetworkID 142 } 143 144 if params.ClientPlatform != nil { 145 config.ClientPlatform = *params.ClientPlatform 146 } // else use the value in config 147 148 if params.EstablishTunnelTimeoutSeconds != nil { 149 config.EstablishTunnelTimeoutSeconds = params.EstablishTunnelTimeoutSeconds 150 } // else use the value in config 151 152 if config.UseNoticeFiles == nil && config.EmitDiagnosticNotices && params.EmitDiagnosticNoticesToFiles { 153 config.UseNoticeFiles = &psiphon.UseNoticeFiles{ 154 RotatingFileSize: 0, 155 RotatingSyncFrequency: 0, 156 } 157 } // else use the value in the config 158 159 // config.Commit must be called before calling config.SetParameters 160 // or attempting to connect. 161 err = config.Commit(true) 162 if err != nil { 163 return nil, errors.TraceMsg(err, "config.Commit failed") 164 } 165 166 // If supplied, apply the parameters delta 167 if len(paramsDelta) > 0 { 168 err = config.SetParameters("", false, paramsDelta) 169 if err != nil { 170 return nil, errors.TraceMsg( 171 err, fmt.Sprintf("SetParameters failed for delta: %v", paramsDelta)) 172 } 173 } 174 175 // Will receive a value when the tunnel has successfully connected. 176 connected := make(chan struct{}, 1) 177 // Will receive a value if an error occurs during the connection sequence. 178 errored := make(chan error, 1) 179 180 // Create the tunnel object 181 tunnel := new(PsiphonTunnel) 182 183 // Set up notice handling 184 psiphon.SetNoticeWriter(psiphon.NewNoticeReceiver( 185 func(notice []byte) { 186 var event NoticeEvent 187 err := json.Unmarshal(notice, &event) 188 if err != nil { 189 // This is unexpected and probably indicates something fatal has occurred. 190 // We'll interpret it as a connection error and abort. 191 err = errors.TraceMsg(err, "failed to unmarshal notice JSON") 192 select { 193 case errored <- err: 194 default: 195 } 196 return 197 } 198 199 if event.Type == "ListeningHttpProxyPort" { 200 port := event.Data["port"].(float64) 201 tunnel.HTTPProxyPort = int(port) 202 } else if event.Type == "ListeningSocksProxyPort" { 203 port := event.Data["port"].(float64) 204 tunnel.SOCKSProxyPort = int(port) 205 } else if event.Type == "EstablishTunnelTimeout" { 206 select { 207 case errored <- ErrTimeout: 208 default: 209 } 210 } else if event.Type == "Tunnels" { 211 count := event.Data["count"].(float64) 212 if count > 0 { 213 select { 214 case connected <- struct{}{}: 215 default: 216 } 217 } 218 } 219 220 // Some users of this package may need to add special processing of notices. 221 // If the caller has requested it, we'll pass on the notices. 222 if noticeReceiver != nil { 223 noticeReceiver(event) 224 } 225 })) 226 227 err = psiphon.OpenDataStore(config) 228 if err != nil { 229 return nil, errors.TraceMsg(err, "failed to open data store") 230 } 231 // Make sure we close the datastore in case of error 232 defer func() { 233 if retErr != nil { 234 tunnel.controllerWaitGroup.Wait() 235 tunnel.embeddedServerListWaitGroup.Wait() 236 psiphon.CloseDataStore() 237 } 238 }() 239 240 // Create a cancelable context that will be used for stopping the tunnel 241 var controllerCtx context.Context 242 controllerCtx, tunnel.stopController = context.WithCancel(ctx) 243 244 // If specified, the embedded server list is loaded and stored. When there 245 // are no server candidates at all, we wait for this import to complete 246 // before starting the Psiphon controller. Otherwise, we import while 247 // concurrently starting the controller to minimize delay before attempting 248 // to connect to existing candidate servers. 249 // 250 // If the import fails, an error notice is emitted, but the controller is 251 // still started: either existing candidate servers may suffice, or the 252 // remote server list fetch may obtain candidate servers. 253 // 254 // The import will be interrupted if it's still running when the controller 255 // is stopped. 256 tunnel.embeddedServerListWaitGroup.Add(1) 257 go func() { 258 defer tunnel.embeddedServerListWaitGroup.Done() 259 260 err := psiphon.ImportEmbeddedServerEntries( 261 controllerCtx, 262 config, 263 "", 264 embeddedServerEntryList) 265 if err != nil { 266 psiphon.NoticeError("error importing embedded server entry list: %s", err) 267 return 268 } 269 }() 270 if !psiphon.HasServerEntries() { 271 psiphon.NoticeInfo("awaiting embedded server entry list import") 272 tunnel.embeddedServerListWaitGroup.Wait() 273 } 274 275 // Create the Psiphon controller 276 controller, err := psiphon.NewController(config) 277 if err != nil { 278 tunnel.stopController() 279 tunnel.embeddedServerListWaitGroup.Wait() 280 return nil, errors.TraceMsg(err, "psiphon.NewController failed") 281 } 282 283 // Begin tunnel connection 284 tunnel.controllerWaitGroup.Add(1) 285 go func() { 286 defer tunnel.controllerWaitGroup.Done() 287 288 // Start the tunnel. Only returns on error (or internal timeout). 289 controller.Run(controllerCtx) 290 291 // controller.Run does not exit until the goroutine that posts 292 // EstablishTunnelTimeout has terminated; so, if there was a 293 // EstablishTunnelTimeout event, ErrTimeout is guaranteed to be sent to 294 // errord before this next error and will be the StartTunnel return value. 295 296 var err error 297 switch ctx.Err() { 298 case context.DeadlineExceeded: 299 err = ErrTimeout 300 case context.Canceled: 301 err = errors.TraceNew("StartTunnel canceled") 302 default: 303 err = errors.TraceNew("controller.Run exited unexpectedly") 304 } 305 select { 306 case errored <- err: 307 default: 308 } 309 }() 310 311 // Wait for an active tunnel or error 312 select { 313 case <-connected: 314 return tunnel, nil 315 case err := <-errored: 316 tunnel.Stop() 317 if err != ErrTimeout { 318 err = errors.TraceMsg(err, "tunnel start produced error") 319 } 320 return nil, err 321 } 322 } 323 324 // Stop stops/disconnects/shuts down the tunnel. It is safe to call when not connected. 325 // Not safe to call concurrently with Start. 326 func (tunnel *PsiphonTunnel) Stop() { 327 if tunnel.stopController == nil { 328 return 329 } 330 tunnel.stopController() 331 tunnel.controllerWaitGroup.Wait() 332 tunnel.embeddedServerListWaitGroup.Wait() 333 psiphon.CloseDataStore() 334 }