client.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. // Copyright (c) Tailscale Inc & contributors
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build !js
  4. // Package controlhttp implements the Tailscale 2021 control protocol
  5. // base transport over HTTP.
  6. //
  7. // This tunnels the protocol in control/controlbase over HTTP with a
  8. // variety of compatibility fallbacks for handling picky or deep
  9. // inspecting proxies.
  10. //
  11. // In the happy path, a client makes a single cleartext HTTP request
  12. // to the server, the server responds with 101 Switching Protocols,
  13. // and the control base protocol takes place over plain TCP.
  14. //
  15. // In the compatibility path, the client does the above over HTTPS,
  16. // resulting in double encryption (once for the control transport, and
  17. // once for the outer TLS layer).
  18. package controlhttp
  19. import (
  20. "cmp"
  21. "context"
  22. "crypto/tls"
  23. "encoding/base64"
  24. "errors"
  25. "fmt"
  26. "io"
  27. "net"
  28. "net/http"
  29. "net/http/httptrace"
  30. "net/netip"
  31. "net/url"
  32. "runtime"
  33. "sync/atomic"
  34. "time"
  35. "tailscale.com/control/controlbase"
  36. "tailscale.com/control/controlhttp/controlhttpcommon"
  37. "tailscale.com/envknob"
  38. "tailscale.com/feature"
  39. "tailscale.com/feature/buildfeatures"
  40. "tailscale.com/health"
  41. "tailscale.com/net/dnscache"
  42. "tailscale.com/net/dnsfallback"
  43. "tailscale.com/net/netutil"
  44. "tailscale.com/net/netx"
  45. "tailscale.com/net/sockstats"
  46. "tailscale.com/net/tlsdial"
  47. "tailscale.com/syncs"
  48. "tailscale.com/tailcfg"
  49. "tailscale.com/tstime"
  50. )
  51. var stdDialer net.Dialer
  52. // Dial connects to the HTTP server at this Dialer's Host:HTTPPort, requests to
  53. // switch to the Tailscale control protocol, and returns an established control
  54. // protocol connection.
  55. //
  56. // If Dial fails to connect using HTTP, it also tries to tunnel over TLS to the
  57. // Dialer's Host:HTTPSPort as a compatibility fallback.
  58. //
  59. // The provided ctx is only used for the initial connection, until
  60. // Dial returns. It does not affect the connection once established.
  61. func (a *Dialer) Dial(ctx context.Context) (*ClientConn, error) {
  62. if a.Hostname == "" {
  63. return nil, errors.New("required Dialer.Hostname empty")
  64. }
  65. return a.dial(ctx)
  66. }
  67. func (a *Dialer) logf(format string, args ...any) {
  68. if a.Logf != nil {
  69. a.Logf(format, args...)
  70. }
  71. }
  72. func (a *Dialer) getProxyFunc() func(*http.Request) (*url.URL, error) {
  73. if a.proxyFunc != nil {
  74. return a.proxyFunc
  75. }
  76. return feature.HookProxyFromEnvironment.GetOrNil()
  77. }
  78. // httpsFallbackDelay is how long we'll wait for a.HTTPPort to work before
  79. // starting to try a.HTTPSPort.
  80. func (a *Dialer) httpsFallbackDelay() time.Duration {
  81. if v := a.testFallbackDelay; v != 0 {
  82. return v
  83. }
  84. return 500 * time.Millisecond
  85. }
  86. var _ = envknob.RegisterBool("TS_USE_CONTROL_DIAL_PLAN") // to record at init time whether it's in use
  87. func (a *Dialer) dial(ctx context.Context) (*ClientConn, error) {
  88. a.logPort80Failure.Store(true)
  89. // If we don't have a dial plan, just fall back to dialing the single
  90. // host we know about.
  91. useDialPlan := envknob.BoolDefaultTrue("TS_USE_CONTROL_DIAL_PLAN")
  92. if !useDialPlan || a.DialPlan == nil || len(a.DialPlan.Candidates) == 0 {
  93. return a.dialHost(ctx)
  94. }
  95. candidates := a.DialPlan.Candidates
  96. // Create a context to be canceled as we return, so once we get a good connection,
  97. // we can drop all the other ones.
  98. ctx, cancel := context.WithCancel(ctx)
  99. defer cancel()
  100. // Now, for each candidate, kick off a dial in parallel.
  101. type dialResult struct {
  102. conn *ClientConn
  103. err error
  104. }
  105. resultsCh := make(chan dialResult) // unbuffered, never closed
  106. dialCand := func(cand tailcfg.ControlIPCandidate) (*ClientConn, error) {
  107. if cand.ACEHost != "" {
  108. a.logf("[v2] controlhttp: waited %.2f seconds, dialing %q via ACE %s (%s)", cand.DialStartDelaySec, a.Hostname, cand.ACEHost, cmp.Or(cand.IP.String(), "dns"))
  109. } else {
  110. a.logf("[v2] controlhttp: waited %.2f seconds, dialing %q @ %s", cand.DialStartDelaySec, a.Hostname, cand.IP.String())
  111. }
  112. ctx, cancel := context.WithTimeout(ctx, time.Duration(cand.DialTimeoutSec*float64(time.Second)))
  113. defer cancel()
  114. return a.dialHostOpt(ctx, cand.IP, cand.ACEHost)
  115. }
  116. for _, cand := range candidates {
  117. timer := time.AfterFunc(time.Duration(cand.DialStartDelaySec*float64(time.Second)), func() {
  118. go func() {
  119. conn, err := dialCand(cand)
  120. select {
  121. case resultsCh <- dialResult{conn, err}:
  122. if err == nil {
  123. a.logf("[v1] controlhttp: succeeded dialing %q @ %v from dial plan", a.Hostname, cmp.Or(cand.ACEHost, cand.IP.String()))
  124. }
  125. case <-ctx.Done():
  126. if conn != nil {
  127. conn.Close()
  128. }
  129. }
  130. }()
  131. })
  132. defer timer.Stop()
  133. }
  134. var errs []error
  135. for {
  136. select {
  137. case res := <-resultsCh:
  138. if res.err == nil {
  139. return res.conn, nil
  140. }
  141. errs = append(errs, res.err)
  142. if len(errs) == len(candidates) {
  143. // If we get here, then we didn't get anywhere with our dial plan; fall back to just using DNS.
  144. a.logf("controlhttp: failed dialing using DialPlan, falling back to DNS; errs=%s", errors.Join(errs...))
  145. return a.dialHost(ctx)
  146. }
  147. case <-ctx.Done():
  148. a.logf("controlhttp: context aborted dialing")
  149. return nil, ctx.Err()
  150. }
  151. }
  152. }
  153. // The TS_FORCE_NOISE_443 envknob forces the controlclient noise dialer to
  154. // always use port 443 HTTPS connections to the controlplane and not try the
  155. // port 80 HTTP fast path.
  156. //
  157. // This is currently (2023-01-17) needed for Docker Desktop's "VPNKit" proxy
  158. // that breaks port 80 for us post-Noise-handshake, causing us to never try port
  159. // 443. Until one of Docker's proxy and/or this package's port 443 fallback is
  160. // fixed, this is a workaround. It might also be useful for future debugging.
  161. var forceNoise443 = envknob.RegisterBool("TS_FORCE_NOISE_443")
  162. // forceNoise443 reports whether the controlclient noise dialer should always
  163. // use HTTPS connections as its underlay connection (double crypto). This can
  164. // be necessary when networks or middle boxes are messing with port 80.
  165. func (d *Dialer) forceNoise443() bool {
  166. if runtime.GOOS == "plan9" {
  167. // For running demos of Plan 9 in a browser with network relays,
  168. // we want to minimize the number of connections we're making.
  169. // The main reason to use port 80 is to avoid double crypto
  170. // costs server-side but the costs are tiny and number of Plan 9
  171. // users doesn't make it worth it. Just disable this and always use
  172. // HTTPS for Plan 9. That also reduces some log spam.
  173. return true
  174. }
  175. if forceNoise443() {
  176. return true
  177. }
  178. if d.HealthTracker.LastNoiseDialWasRecent() {
  179. // If we dialed recently, assume there was a recent failure and fall
  180. // back to HTTPS dials for the subsequent retries.
  181. //
  182. // This heuristic works around networks where port 80 is MITMed and
  183. // appears to work for a bit post-Upgrade but then gets closed,
  184. // such as seen in https://github.com/tailscale/tailscale/issues/13597.
  185. if d.logPort80Failure.CompareAndSwap(true, false) {
  186. d.logf("controlhttp: forcing port 443 dial due to recent noise dial")
  187. }
  188. return true
  189. }
  190. return false
  191. }
  192. func (d *Dialer) clock() tstime.Clock {
  193. if d.Clock != nil {
  194. return d.Clock
  195. }
  196. return tstime.StdClock{}
  197. }
  198. var debugNoiseDial = envknob.RegisterBool("TS_DEBUG_NOISE_DIAL")
  199. // dialHost connects to the configured Dialer.Hostname and upgrades the
  200. // connection into a controlbase.Conn.
  201. func (a *Dialer) dialHost(ctx context.Context) (*ClientConn, error) {
  202. return a.dialHostOpt(ctx,
  203. netip.Addr{}, // no pre-resolved IP
  204. "", // don't use ACE
  205. )
  206. }
  207. // dialHostOpt connects to the configured Dialer.Hostname and upgrades the
  208. // connection into a controlbase.Conn.
  209. //
  210. // If optAddr is valid, then no DNS is used and the connection will be made to the
  211. // provided address.
  212. func (a *Dialer) dialHostOpt(ctx context.Context, optAddr netip.Addr, optACEHost string) (*ClientConn, error) {
  213. // Create one shared context used by both port 80 and port 443 dials.
  214. // If port 80 is still in flight when 443 returns, this deferred cancel
  215. // will stop the port 80 dial.
  216. ctx, cancel := context.WithCancel(ctx)
  217. defer cancel()
  218. ctx = sockstats.WithSockStats(ctx, sockstats.LabelControlClientDialer, a.logf)
  219. // u80 and u443 are the URLs we'll try to hit over HTTP or HTTPS,
  220. // respectively, in order to do the HTTP upgrade to a net.Conn over which
  221. // we'll speak Noise.
  222. u80 := &url.URL{
  223. Scheme: "http",
  224. Host: net.JoinHostPort(a.Hostname, strDef(a.HTTPPort, "80")),
  225. Path: serverUpgradePath,
  226. }
  227. u443 := &url.URL{
  228. Scheme: "https",
  229. Host: net.JoinHostPort(a.Hostname, strDef(a.HTTPSPort, "443")),
  230. Path: serverUpgradePath,
  231. }
  232. if a.HTTPSPort == NoPort || optACEHost != "" {
  233. u443 = nil
  234. }
  235. type tryURLRes struct {
  236. u *url.URL // input (the URL conn+err are for/from)
  237. conn *ClientConn // result (mutually exclusive with err)
  238. err error
  239. }
  240. ch := make(chan tryURLRes) // must be unbuffered
  241. try := func(u *url.URL) {
  242. if debugNoiseDial() {
  243. a.logf("trying noise dial (%v, %v) ...", u, cmp.Or(optACEHost, optAddr.String()))
  244. }
  245. cbConn, err := a.dialURL(ctx, u, optAddr, optACEHost)
  246. if debugNoiseDial() {
  247. a.logf("noise dial (%v, %v) = (%v, %v)", u, cmp.Or(optACEHost, optAddr.String()), cbConn, err)
  248. }
  249. select {
  250. case ch <- tryURLRes{u, cbConn, err}:
  251. case <-ctx.Done():
  252. if cbConn != nil {
  253. cbConn.Close()
  254. }
  255. }
  256. }
  257. forceTLS := a.forceNoise443()
  258. // Start the plaintext HTTP attempt first, unless disabled by the envknob.
  259. if !forceTLS || u443 == nil {
  260. go try(u80)
  261. }
  262. // In case outbound port 80 blocked or MITM'ed poorly, start a backup timer
  263. // to dial port 443 if port 80 doesn't either succeed or fail quickly.
  264. var try443Timer tstime.TimerController
  265. if u443 != nil {
  266. delay := a.httpsFallbackDelay()
  267. if forceTLS {
  268. delay = 0
  269. }
  270. try443Timer = a.clock().AfterFunc(delay, func() { try(u443) })
  271. defer try443Timer.Stop()
  272. }
  273. var err80, err443 error
  274. if forceTLS {
  275. err80 = errors.New("TLS forced: no port 80 dialed")
  276. }
  277. for {
  278. select {
  279. case <-ctx.Done():
  280. return nil, fmt.Errorf("connection attempts aborted by context: %w", ctx.Err())
  281. case res := <-ch:
  282. if res.err == nil {
  283. return res.conn, nil
  284. }
  285. switch res.u {
  286. case u80:
  287. // Connecting over plain HTTP failed; assume it's an HTTP proxy
  288. // being difficult and see if we can get through over HTTPS.
  289. err80 = res.err
  290. // Stop the fallback timer and run it immediately. We don't use
  291. // Timer.Reset(0) here because on AfterFuncs, that can run it
  292. // again.
  293. if try443Timer != nil && try443Timer.Stop() {
  294. go try(u443)
  295. } // else we lost the race and it started already which is what we want
  296. case u443:
  297. err443 = res.err
  298. default:
  299. panic("invalid")
  300. }
  301. if err80 != nil && err443 != nil {
  302. return nil, fmt.Errorf("all connection attempts failed (HTTP: %v, HTTPS: %v)", err80, err443)
  303. }
  304. }
  305. }
  306. }
  307. // dialURL attempts to connect to the given URL.
  308. //
  309. // If optAddr is valid, then no DNS is used and the connection will be made to the
  310. // provided address.
  311. func (a *Dialer) dialURL(ctx context.Context, u *url.URL, optAddr netip.Addr, optACEHost string) (*ClientConn, error) {
  312. init, cont, err := controlbase.ClientDeferred(a.MachineKey, a.ControlKey, a.ProtocolVersion)
  313. if err != nil {
  314. return nil, err
  315. }
  316. netConn, err := a.tryURLUpgrade(ctx, u, optAddr, optACEHost, init)
  317. if err != nil {
  318. return nil, err
  319. }
  320. cbConn, err := cont(ctx, netConn)
  321. if err != nil {
  322. netConn.Close()
  323. return nil, err
  324. }
  325. return &ClientConn{
  326. Conn: cbConn,
  327. }, nil
  328. }
  329. // resolver returns a.DNSCache if non-nil or a new *dnscache.Resolver
  330. // otherwise.
  331. func (a *Dialer) resolver() *dnscache.Resolver {
  332. if a.DNSCache != nil {
  333. return a.DNSCache
  334. }
  335. return &dnscache.Resolver{
  336. Forward: dnscache.Get().Forward,
  337. LookupIPFallback: dnsfallback.MakeLookupFunc(a.logf, a.NetMon),
  338. UseLastGood: true,
  339. Logf: a.Logf, // not a.logf method; we want to propagate nil-ness
  340. }
  341. }
  342. func isLoopback(a net.Addr) bool {
  343. if ta, ok := a.(*net.TCPAddr); ok {
  344. return ta.IP.IsLoopback()
  345. }
  346. return false
  347. }
  348. var macOSScreenTime = health.Register(&health.Warnable{
  349. Code: "macos-screen-time",
  350. Severity: health.SeverityHigh,
  351. Title: "Tailscale blocked by Screen Time",
  352. Text: func(args health.Args) string {
  353. return "macOS Screen Time seems to be blocking Tailscale. Try disabling Screen Time in System Settings > Screen Time > Content & Privacy > Access to Web Content."
  354. },
  355. ImpactsConnectivity: true,
  356. })
  357. var HookMakeACEDialer feature.Hook[func(dialer netx.DialFunc, aceHost string, optIP netip.Addr) netx.DialFunc]
  358. // tryURLUpgrade connects to u, and tries to upgrade it to a net.Conn.
  359. //
  360. // If optAddr is valid, then no DNS is used and the connection will be made to
  361. // the provided address.
  362. //
  363. // Only the provided ctx is used, not a.ctx.
  364. func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, optAddr netip.Addr, optACEHost string, init []byte) (_ net.Conn, retErr error) {
  365. var dns *dnscache.Resolver
  366. // If we were provided an address to dial, then create a resolver that just
  367. // returns that value; otherwise, fall back to DNS.
  368. if optAddr.IsValid() {
  369. dns = &dnscache.Resolver{
  370. SingleHostStaticResult: []netip.Addr{optAddr},
  371. SingleHost: u.Hostname(),
  372. Logf: a.Logf, // not a.logf method; we want to propagate nil-ness
  373. }
  374. } else {
  375. dns = a.resolver()
  376. }
  377. var dialer netx.DialFunc
  378. if a.Dialer != nil {
  379. dialer = a.Dialer
  380. } else {
  381. dialer = stdDialer.DialContext
  382. }
  383. if optACEHost != "" {
  384. if !buildfeatures.HasACE {
  385. return nil, feature.ErrUnavailable
  386. }
  387. f, ok := HookMakeACEDialer.GetOk()
  388. if !ok {
  389. return nil, feature.ErrUnavailable
  390. }
  391. dialer = f(dialer, optACEHost, optAddr)
  392. }
  393. // On macOS, see if Screen Time is blocking things.
  394. if runtime.GOOS == "darwin" {
  395. var proxydIntercepted atomic.Bool // intercepted by macOS webfilterproxyd
  396. origDialer := dialer
  397. dialer = func(ctx context.Context, network, address string) (net.Conn, error) {
  398. c, err := origDialer(ctx, network, address)
  399. if err != nil {
  400. return nil, err
  401. }
  402. if isLoopback(c.LocalAddr()) && isLoopback(c.RemoteAddr()) {
  403. proxydIntercepted.Store(true)
  404. }
  405. return c, nil
  406. }
  407. defer func() {
  408. if retErr != nil && proxydIntercepted.Load() {
  409. a.HealthTracker.SetUnhealthy(macOSScreenTime, nil)
  410. retErr = fmt.Errorf("macOS Screen Time is blocking network access: %w", retErr)
  411. } else {
  412. a.HealthTracker.SetHealthy(macOSScreenTime)
  413. }
  414. }()
  415. }
  416. tr := http.DefaultTransport.(*http.Transport).Clone()
  417. defer tr.CloseIdleConnections()
  418. if optACEHost != "" {
  419. // If using ACE, we don't want to use any HTTP proxy.
  420. // ACE is already a tunnel+proxy.
  421. // TODO(tailscale/corp#32483): use system proxy too?
  422. tr.Proxy = nil
  423. tr.DialContext = dialer
  424. } else {
  425. if buildfeatures.HasUseProxy {
  426. tr.Proxy = a.getProxyFunc()
  427. if set, ok := feature.HookProxySetTransportGetProxyConnectHeader.GetOk(); ok {
  428. set(tr)
  429. }
  430. }
  431. tr.DialContext = dnscache.Dialer(dialer, dns)
  432. }
  433. // Disable HTTP2, since h2 can't do protocol switching.
  434. tr.TLSClientConfig.NextProtos = []string{}
  435. tr.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{}
  436. tr.TLSClientConfig = tlsdial.Config(a.HealthTracker, tr.TLSClientConfig)
  437. if !tr.TLSClientConfig.InsecureSkipVerify {
  438. panic("unexpected") // should be set by tlsdial.Config
  439. }
  440. verify := tr.TLSClientConfig.VerifyConnection
  441. if verify == nil {
  442. panic("unexpected") // should be set by tlsdial.Config
  443. }
  444. // Demote all cert verification errors to log messages. We don't actually
  445. // care about the TLS security (because we just do the Noise crypto atop whatever
  446. // connection we get, including HTTP port 80 plaintext) so this permits
  447. // middleboxes to MITM their users. All they'll see is some Noise.
  448. tr.TLSClientConfig.VerifyConnection = func(cs tls.ConnectionState) error {
  449. if err := verify(cs); err != nil && a.Logf != nil && !a.omitCertErrorLogging {
  450. a.Logf("warning: TLS cert verificication for %q failed: %v", a.Hostname, err)
  451. }
  452. return nil // regardless
  453. }
  454. tr.DialTLSContext = dnscache.TLSDialer(dialer, dns, tr.TLSClientConfig)
  455. tr.DisableCompression = true
  456. // (mis)use httptrace to extract the underlying net.Conn from the
  457. // transport. The transport handles 101 Switching Protocols correctly,
  458. // such that the Conn will not be reused or kept alive by the transport
  459. // once the response has been handed back from RoundTrip.
  460. //
  461. // In theory, the machinery of net/http should make it such that
  462. // the trace callback happens-before we get the response, but
  463. // there's no promise of that. So, to make sure, we use a buffered
  464. // channel as a synchronization step to avoid data races.
  465. //
  466. // Note that even though we're able to extract a net.Conn via this
  467. // mechanism, we must still keep using the eventual resp.Body to
  468. // read from, because it includes a buffer we can't get rid of. If
  469. // the server never sends any data after sending the HTTP
  470. // response, we could get away with it, but violating this
  471. // assumption leads to very mysterious transport errors (lockups,
  472. // unexpected EOFs...), and we're bound to forget someday and
  473. // introduce a protocol optimization at a higher level that starts
  474. // eagerly transmitting from the server.
  475. var lastConn syncs.AtomicValue[net.Conn]
  476. trace := httptrace.ClientTrace{
  477. // Even though we only make a single HTTP request which should
  478. // require a single connection, the context (with the attached
  479. // trace configuration) might be used by our custom dialer to
  480. // make other HTTP requests (e.g. BootstrapDNS). We only care
  481. // about the last connection made, which should be the one to
  482. // the control server.
  483. GotConn: func(info httptrace.GotConnInfo) {
  484. lastConn.Store(info.Conn)
  485. },
  486. }
  487. ctx = httptrace.WithClientTrace(ctx, &trace)
  488. req := &http.Request{
  489. Method: "POST",
  490. URL: u,
  491. Header: http.Header{
  492. "Upgrade": []string{controlhttpcommon.UpgradeHeaderValue},
  493. "Connection": []string{"upgrade"},
  494. controlhttpcommon.HandshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
  495. },
  496. }
  497. req = req.WithContext(ctx)
  498. resp, err := tr.RoundTrip(req)
  499. if err != nil {
  500. return nil, err
  501. }
  502. if resp.StatusCode != http.StatusSwitchingProtocols {
  503. return nil, fmt.Errorf("unexpected HTTP response: %s", resp.Status)
  504. }
  505. // From here on, the underlying net.Conn is ours to use, but there
  506. // is still a read buffer attached to it within resp.Body. So, we
  507. // must direct I/O through resp.Body, but we can still use the
  508. // underlying net.Conn for stuff like deadlines.
  509. switchedConn := lastConn.Load()
  510. if switchedConn == nil {
  511. resp.Body.Close()
  512. return nil, fmt.Errorf("httptrace didn't provide a connection")
  513. }
  514. if next := resp.Header.Get("Upgrade"); next != controlhttpcommon.UpgradeHeaderValue {
  515. resp.Body.Close()
  516. return nil, fmt.Errorf("server switched to unexpected protocol %q", next)
  517. }
  518. rwc, ok := resp.Body.(io.ReadWriteCloser)
  519. if !ok {
  520. resp.Body.Close()
  521. return nil, errors.New("http Transport did not provide a writable body")
  522. }
  523. return netutil.NewAltReadWriteCloserConn(rwc, switchedConn), nil
  524. }