|
|
|
|
@ -30,6 +30,7 @@ import ( |
|
|
|
|
"net/http" |
|
|
|
|
"net/http/httptrace" |
|
|
|
|
"net/url" |
|
|
|
|
"time" |
|
|
|
|
|
|
|
|
|
"tailscale.com/control/controlbase" |
|
|
|
|
"tailscale.com/net/dnscache" |
|
|
|
|
@ -98,48 +99,98 @@ type dialParams struct { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (a *dialParams) dial() (*controlbase.Conn, error) { |
|
|
|
|
init, cont, err := controlbase.ClientDeferred(a.machineKey, a.controlKey, a.version) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
// Create one shared context used by both port 80 and port 443 dials.
|
|
|
|
|
// If port 80 is still in flight when 443 returns, this deferred cancel
|
|
|
|
|
// will stop the port 80 dial.
|
|
|
|
|
ctx, cancel := context.WithCancel(a.ctx) |
|
|
|
|
defer cancel() |
|
|
|
|
|
|
|
|
|
u := &url.URL{ |
|
|
|
|
// u80 and u443 are the URLs we'll try to hit over HTTP or HTTPS,
|
|
|
|
|
// respectively, in order to do the HTTP upgrade to a net.Conn over which
|
|
|
|
|
// we'll speak Noise.
|
|
|
|
|
u80 := &url.URL{ |
|
|
|
|
Scheme: "http", |
|
|
|
|
Host: net.JoinHostPort(a.host, a.httpPort), |
|
|
|
|
Path: serverUpgradePath, |
|
|
|
|
} |
|
|
|
|
conn, httpErr := a.tryURL(u, init) |
|
|
|
|
if httpErr == nil { |
|
|
|
|
ret, err := cont(a.ctx, conn) |
|
|
|
|
if err != nil { |
|
|
|
|
conn.Close() |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
return ret, nil |
|
|
|
|
u443 := &url.URL{ |
|
|
|
|
Scheme: "https", |
|
|
|
|
Host: net.JoinHostPort(a.host, a.httpsPort), |
|
|
|
|
Path: serverUpgradePath, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Connecting over plain HTTP failed, assume it's an HTTP proxy
|
|
|
|
|
// being difficult and see if we can get through over HTTPS.
|
|
|
|
|
u.Scheme = "https" |
|
|
|
|
u.Host = net.JoinHostPort(a.host, a.httpsPort) |
|
|
|
|
init, cont, err = controlbase.ClientDeferred(a.machineKey, a.controlKey, a.version) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
type tryURLRes struct { |
|
|
|
|
u *url.URL |
|
|
|
|
conn net.Conn |
|
|
|
|
cont controlbase.HandshakeContinuation |
|
|
|
|
err error |
|
|
|
|
} |
|
|
|
|
conn, tlsErr := a.tryURL(u, init) |
|
|
|
|
if tlsErr == nil { |
|
|
|
|
ret, err := cont(a.ctx, conn) |
|
|
|
|
if err != nil { |
|
|
|
|
conn.Close() |
|
|
|
|
return nil, err |
|
|
|
|
ch := make(chan tryURLRes) // must be unbuffered
|
|
|
|
|
|
|
|
|
|
try := func(u *url.URL) { |
|
|
|
|
res := tryURLRes{u: u} |
|
|
|
|
var init []byte |
|
|
|
|
init, res.cont, res.err = controlbase.ClientDeferred(a.machineKey, a.controlKey, a.version) |
|
|
|
|
if res.err == nil { |
|
|
|
|
res.conn, res.err = a.tryURL(ctx, u, init) |
|
|
|
|
} |
|
|
|
|
select { |
|
|
|
|
case ch <- res: |
|
|
|
|
case <-ctx.Done(): |
|
|
|
|
if res.conn != nil { |
|
|
|
|
res.conn.Close() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return ret, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("all connection attempts failed (HTTP: %v, HTTPS: %v)", httpErr, tlsErr) |
|
|
|
|
// Start the plaintext HTTP attempt first.
|
|
|
|
|
go try(u80) |
|
|
|
|
|
|
|
|
|
// In case outbound port 80 blocked or MITM'ed poorly, start a backup timer
|
|
|
|
|
// to dial port 443 if port 80 doesn't either succeed or fail quickly.
|
|
|
|
|
try443Timer := time.AfterFunc(500*time.Millisecond, func() { try(u443) }) |
|
|
|
|
defer try443Timer.Stop() |
|
|
|
|
|
|
|
|
|
var err80, err443 error |
|
|
|
|
for { |
|
|
|
|
select { |
|
|
|
|
case <-ctx.Done(): |
|
|
|
|
return nil, fmt.Errorf("connection attempts aborted by context: %w", ctx.Err()) |
|
|
|
|
case res := <-ch: |
|
|
|
|
if res.err == nil { |
|
|
|
|
ret, err := res.cont(ctx, res.conn) |
|
|
|
|
if err != nil { |
|
|
|
|
res.conn.Close() |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
return ret, nil |
|
|
|
|
} |
|
|
|
|
switch res.u { |
|
|
|
|
case u80: |
|
|
|
|
// Connecting over plain HTTP failed; assume it's an HTTP proxy
|
|
|
|
|
// being difficult and see if we can get through over HTTPS.
|
|
|
|
|
err80 = res.err |
|
|
|
|
// Stop the fallback timer and run it immediately. We don't use
|
|
|
|
|
// Timer.Reset(0) here because on AfterFuncs, that can run it
|
|
|
|
|
// again.
|
|
|
|
|
if try443Timer.Stop() { |
|
|
|
|
go try(u443) |
|
|
|
|
} // else we lost the race and it started already which is what we want
|
|
|
|
|
case u443: |
|
|
|
|
err443 = res.err |
|
|
|
|
default: |
|
|
|
|
panic("invalid") |
|
|
|
|
} |
|
|
|
|
if err80 != nil && err443 != nil { |
|
|
|
|
return nil, fmt.Errorf("all connection attempts failed (HTTP: %v, HTTPS: %v)", err80, err443) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (a *dialParams) tryURL(u *url.URL, init []byte) (net.Conn, error) { |
|
|
|
|
// tryURL connects to u, and tries to upgrade it to a net.Conn.
|
|
|
|
|
//
|
|
|
|
|
// Only the provided ctx is used, not a.ctx.
|
|
|
|
|
func (a *dialParams) tryURL(ctx context.Context, u *url.URL, init []byte) (net.Conn, error) { |
|
|
|
|
dns := &dnscache.Resolver{ |
|
|
|
|
Forward: dnscache.Get().Forward, |
|
|
|
|
LookupIPFallback: dnsfallback.Lookup, |
|
|
|
|
@ -189,7 +240,7 @@ func (a *dialParams) tryURL(u *url.URL, init []byte) (net.Conn, error) { |
|
|
|
|
connCh <- info.Conn |
|
|
|
|
}, |
|
|
|
|
} |
|
|
|
|
ctx := httptrace.WithClientTrace(a.ctx, &trace) |
|
|
|
|
ctx = httptrace.WithClientTrace(ctx, &trace) |
|
|
|
|
req := &http.Request{ |
|
|
|
|
Method: "POST", |
|
|
|
|
URL: u, |
|
|
|
|
|