Updates #3488 Signed-off-by: David Anderson <danderson@tailscale.com>main
parent
d5a7eabcd0
commit
96f008cf87
@ -0,0 +1,242 @@ |
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package controlhttp implements the Tailscale 2021 control protocol
|
||||
// base transport over HTTP.
|
||||
//
|
||||
// This tunnels the protocol in control/controlbase over HTTP with a
|
||||
// variety of compatibility fallbacks for handling picky or deep
|
||||
// inspecting proxies.
|
||||
//
|
||||
// In the happy path, a client makes a single cleartext HTTP request
|
||||
// to the server, the server responds with 101 Switching Protocols,
|
||||
// and the control base protocol takes place over plain TCP.
|
||||
//
|
||||
// In the compatibility path, the client does the above over HTTPS,
|
||||
// resulting in double encryption (once for the control transport, and
|
||||
// once for the outer TLS layer).
|
||||
package controlhttp |
||||
|
||||
import ( |
||||
"context" |
||||
"crypto/tls" |
||||
"encoding/base64" |
||||
"errors" |
||||
"fmt" |
||||
"io" |
||||
"log" |
||||
"net" |
||||
"net/http" |
||||
"net/http/httptrace" |
||||
"net/url" |
||||
|
||||
"tailscale.com/control/controlbase" |
||||
"tailscale.com/net/dnscache" |
||||
"tailscale.com/net/dnsfallback" |
||||
"tailscale.com/net/netns" |
||||
"tailscale.com/net/tlsdial" |
||||
"tailscale.com/net/tshttpproxy" |
||||
"tailscale.com/types/key" |
||||
) |
||||
|
||||
// upgradeHeader is the value of the Upgrade HTTP header used to
|
||||
// indicate the Tailscale control protocol.
|
||||
const ( |
||||
upgradeHeaderValue = "tailscale-control-protocol" |
||||
handshakeHeaderName = "X-Tailscale-Handshake" |
||||
) |
||||
|
||||
// Dial connects to the HTTP server at addr, requests to switch to the
|
||||
// Tailscale control protocol, and returns an established control
|
||||
// protocol connection.
|
||||
//
|
||||
// If Dial fails to connect using addr, it also tries to tunnel over
|
||||
// TLS to <addr's host>:443 as a compatibility fallback.
|
||||
func Dial(ctx context.Context, addr string, machineKey key.MachinePrivate, controlKey key.MachinePublic) (*controlbase.Conn, error) { |
||||
host, port, err := net.SplitHostPort(addr) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
a := &dialParams{ |
||||
ctx: ctx, |
||||
host: host, |
||||
httpPort: port, |
||||
httpsPort: "443", |
||||
machineKey: machineKey, |
||||
controlKey: controlKey, |
||||
proxyFunc: tshttpproxy.ProxyFromEnvironment, |
||||
} |
||||
return a.dial() |
||||
} |
||||
|
||||
type dialParams struct { |
||||
ctx context.Context |
||||
host string |
||||
httpPort string |
||||
httpsPort string |
||||
machineKey key.MachinePrivate |
||||
controlKey key.MachinePublic |
||||
proxyFunc func(*http.Request) (*url.URL, error) // or nil
|
||||
|
||||
// For tests only
|
||||
insecureTLS bool |
||||
} |
||||
|
||||
func (a *dialParams) dial() (*controlbase.Conn, error) { |
||||
init, cont, err := controlbase.ClientDeferred(a.machineKey, a.controlKey) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
u := &url.URL{ |
||||
Scheme: "http", |
||||
Host: net.JoinHostPort(a.host, a.httpPort), |
||||
Path: "/switch", |
||||
} |
||||
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 |
||||
} |
||||
|
||||
// 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) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
conn, tlsErr := a.tryURL(u, init) |
||||
if tlsErr == nil { |
||||
ret, err := cont(a.ctx, conn) |
||||
if err != nil { |
||||
conn.Close() |
||||
return nil, err |
||||
} |
||||
return ret, nil |
||||
} |
||||
|
||||
return nil, fmt.Errorf("all connection attempts failed (HTTP: %v, HTTPS: %v)", httpErr, tlsErr) |
||||
} |
||||
|
||||
func (a *dialParams) tryURL(u *url.URL, init []byte) (net.Conn, error) { |
||||
dns := &dnscache.Resolver{ |
||||
Forward: dnscache.Get().Forward, |
||||
LookupIPFallback: dnsfallback.Lookup, |
||||
UseLastGood: true, |
||||
} |
||||
dialer := netns.NewDialer(log.Printf) |
||||
tr := http.DefaultTransport.(*http.Transport).Clone() |
||||
defer tr.CloseIdleConnections() |
||||
tr.Proxy = a.proxyFunc |
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr) |
||||
tr.DialContext = dnscache.Dialer(dialer.DialContext, dns) |
||||
// Disable HTTP2, since h2 can't do protocol switching.
|
||||
tr.TLSClientConfig.NextProtos = []string{} |
||||
tr.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{} |
||||
tr.TLSClientConfig = tlsdial.Config(a.host, tr.TLSClientConfig) |
||||
if a.insecureTLS { |
||||
tr.TLSClientConfig.InsecureSkipVerify = true |
||||
tr.TLSClientConfig.VerifyConnection = nil |
||||
} |
||||
tr.DialTLSContext = dnscache.TLSDialer(dialer.DialContext, dns, tr.TLSClientConfig) |
||||
tr.DisableCompression = true |
||||
|
||||
// (mis)use httptrace to extract the underlying net.Conn from the
|
||||
// transport. We make exactly 1 request using this transport, so
|
||||
// there will be exactly 1 GotConn call. Additionally, the
|
||||
// transport handles 101 Switching Protocols correctly, such that
|
||||
// the Conn will not be reused or kept alive by the transport once
|
||||
// the response has been handed back from RoundTrip.
|
||||
//
|
||||
// In theory, the machinery of net/http should make it such that
|
||||
// the trace callback happens-before we get the response, but
|
||||
// there's no promise of that. So, to make sure, we use a buffered
|
||||
// channel as a synchronization step to avoid data races.
|
||||
//
|
||||
// Note that even though we're able to extract a net.Conn via this
|
||||
// mechanism, we must still keep using the eventual resp.Body to
|
||||
// read from, because it includes a buffer we can't get rid of. If
|
||||
// the server never sends any data after sending the HTTP
|
||||
// response, we could get away with it, but violating this
|
||||
// assumption leads to very mysterious transport errors (lockups,
|
||||
// unexpected EOFs...), and we're bound to forget someday and
|
||||
// introduce a protocol optimization at a higher level that starts
|
||||
// eagerly transmitting from the server.
|
||||
connCh := make(chan net.Conn, 1) |
||||
trace := httptrace.ClientTrace{ |
||||
GotConn: func(info httptrace.GotConnInfo) { |
||||
connCh <- info.Conn |
||||
}, |
||||
} |
||||
ctx := httptrace.WithClientTrace(a.ctx, &trace) |
||||
req := &http.Request{ |
||||
Method: "POST", |
||||
URL: u, |
||||
Header: http.Header{ |
||||
"Upgrade": []string{upgradeHeaderValue}, |
||||
"Connection": []string{"upgrade"}, |
||||
handshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)}, |
||||
}, |
||||
} |
||||
req = req.WithContext(ctx) |
||||
|
||||
resp, err := tr.RoundTrip(req) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if resp.StatusCode != http.StatusSwitchingProtocols { |
||||
return nil, fmt.Errorf("unexpected HTTP response: %s", resp.Status) |
||||
} |
||||
|
||||
// From here on, the underlying net.Conn is ours to use, but there
|
||||
// is still a read buffer attached to it within resp.Body. So, we
|
||||
// must direct I/O through resp.Body, but we can still use the
|
||||
// underlying net.Conn for stuff like deadlines.
|
||||
var switchedConn net.Conn |
||||
select { |
||||
case switchedConn = <-connCh: |
||||
default: |
||||
} |
||||
if switchedConn == nil { |
||||
resp.Body.Close() |
||||
return nil, fmt.Errorf("httptrace didn't provide a connection") |
||||
} |
||||
|
||||
if next := resp.Header.Get("Upgrade"); next != upgradeHeaderValue { |
||||
resp.Body.Close() |
||||
return nil, fmt.Errorf("server switched to unexpected protocol %q", next) |
||||
} |
||||
|
||||
rwc, ok := resp.Body.(io.ReadWriteCloser) |
||||
if !ok { |
||||
resp.Body.Close() |
||||
return nil, errors.New("http Transport did not provide a writable body") |
||||
} |
||||
|
||||
return &wrappedConn{switchedConn, rwc}, nil |
||||
} |
||||
|
||||
type wrappedConn struct { |
||||
net.Conn |
||||
rwc io.ReadWriteCloser |
||||
} |
||||
|
||||
func (w *wrappedConn) Read(bs []byte) (int, error) { |
||||
return w.rwc.Read(bs) |
||||
} |
||||
|
||||
func (w *wrappedConn) Write(bs []byte) (int, error) { |
||||
return w.rwc.Write(bs) |
||||
} |
||||
|
||||
func (w *wrappedConn) Close() error { |
||||
return w.rwc.Close() |
||||
} |
||||
@ -0,0 +1,398 @@ |
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package controlhttp |
||||
|
||||
import ( |
||||
"context" |
||||
"crypto/tls" |
||||
"fmt" |
||||
"io" |
||||
"log" |
||||
"net" |
||||
"net/http" |
||||
"net/http/httputil" |
||||
"net/url" |
||||
"strconv" |
||||
"sync" |
||||
"testing" |
||||
|
||||
"tailscale.com/control/controlbase" |
||||
"tailscale.com/net/socks5" |
||||
"tailscale.com/types/key" |
||||
) |
||||
|
||||
func TestControlHTTP(t *testing.T) { |
||||
tests := []struct { |
||||
name string |
||||
proxy proxy |
||||
}{ |
||||
// direct connection
|
||||
{ |
||||
name: "no_proxy", |
||||
proxy: nil, |
||||
}, |
||||
// SOCKS5
|
||||
{ |
||||
name: "socks5", |
||||
proxy: &socksProxy{}, |
||||
}, |
||||
// HTTP->HTTP
|
||||
{ |
||||
name: "http_to_http", |
||||
proxy: &httpProxy{ |
||||
useTLS: false, |
||||
allowConnect: false, |
||||
allowHTTP: true, |
||||
}, |
||||
}, |
||||
// HTTP->HTTPS
|
||||
{ |
||||
name: "http_to_https", |
||||
proxy: &httpProxy{ |
||||
useTLS: false, |
||||
allowConnect: true, |
||||
allowHTTP: false, |
||||
}, |
||||
}, |
||||
// HTTP->any (will pick HTTP)
|
||||
{ |
||||
name: "http_to_any", |
||||
proxy: &httpProxy{ |
||||
useTLS: false, |
||||
allowConnect: true, |
||||
allowHTTP: true, |
||||
}, |
||||
}, |
||||
// HTTPS->HTTP
|
||||
{ |
||||
name: "https_to_http", |
||||
proxy: &httpProxy{ |
||||
useTLS: true, |
||||
allowConnect: false, |
||||
allowHTTP: true, |
||||
}, |
||||
}, |
||||
// HTTPS->HTTPS
|
||||
{ |
||||
name: "https_to_https", |
||||
proxy: &httpProxy{ |
||||
useTLS: true, |
||||
allowConnect: true, |
||||
allowHTTP: false, |
||||
}, |
||||
}, |
||||
// HTTPS->any (will pick HTTP)
|
||||
{ |
||||
name: "https_to_any", |
||||
proxy: &httpProxy{ |
||||
useTLS: true, |
||||
allowConnect: true, |
||||
allowHTTP: true, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
for _, test := range tests { |
||||
t.Run(test.name, func(t *testing.T) { |
||||
testControlHTTP(t, test.proxy) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func testControlHTTP(t *testing.T, proxy proxy) { |
||||
client, server := key.NewMachine(), key.NewMachine() |
||||
|
||||
sch := make(chan serverResult, 1) |
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
||||
conn, err := AcceptHTTP(context.Background(), w, r, server) |
||||
if err != nil { |
||||
log.Print(err) |
||||
} |
||||
res := serverResult{ |
||||
err: err, |
||||
} |
||||
if conn != nil { |
||||
res.clientAddr = conn.RemoteAddr().String() |
||||
res.version = conn.ProtocolVersion() |
||||
res.peer = conn.Peer() |
||||
res.conn = conn |
||||
} |
||||
sch <- res |
||||
}) |
||||
|
||||
httpLn, err := net.Listen("tcp", "127.0.0.1:0") |
||||
if err != nil { |
||||
t.Fatalf("HTTP listen: %v", err) |
||||
} |
||||
httpsLn, err := net.Listen("tcp", "127.0.0.1:0") |
||||
if err != nil { |
||||
t.Fatalf("HTTPS listen: %v", err) |
||||
} |
||||
|
||||
httpServer := &http.Server{Handler: handler} |
||||
go httpServer.Serve(httpLn) |
||||
defer httpServer.Close() |
||||
|
||||
httpsServer := &http.Server{ |
||||
Handler: handler, |
||||
TLSConfig: tlsConfig(t), |
||||
} |
||||
go httpsServer.ServeTLS(httpsLn, "", "") |
||||
defer httpsServer.Close() |
||||
|
||||
//ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
//defer cancel()
|
||||
|
||||
a := dialParams{ |
||||
ctx: context.Background(), //ctx,
|
||||
host: "localhost", |
||||
httpPort: strconv.Itoa(httpLn.Addr().(*net.TCPAddr).Port), |
||||
httpsPort: strconv.Itoa(httpsLn.Addr().(*net.TCPAddr).Port), |
||||
machineKey: client, |
||||
controlKey: server.Public(), |
||||
insecureTLS: true, |
||||
} |
||||
|
||||
if proxy != nil { |
||||
proxyEnv := proxy.Start(t) |
||||
defer proxy.Close() |
||||
proxyURL, err := url.Parse(proxyEnv) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
a.proxyFunc = func(*http.Request) (*url.URL, error) { |
||||
return proxyURL, nil |
||||
} |
||||
} else { |
||||
a.proxyFunc = func(*http.Request) (*url.URL, error) { |
||||
return nil, nil |
||||
} |
||||
} |
||||
|
||||
conn, err := a.dial() |
||||
if err != nil { |
||||
t.Fatalf("dialing controlhttp: %v", err) |
||||
} |
||||
defer conn.Close() |
||||
si := <-sch |
||||
if si.conn != nil { |
||||
defer si.conn.Close() |
||||
} |
||||
if si.err != nil { |
||||
t.Fatalf("controlhttp server got error: %v", err) |
||||
} |
||||
if clientVersion := conn.ProtocolVersion(); si.version != clientVersion { |
||||
t.Fatalf("client and server don't agree on protocol version: %d vs %d", clientVersion, si.version) |
||||
} |
||||
if si.peer != client.Public() { |
||||
t.Fatalf("server got peer pubkey %s, want %s", si.peer, client.Public()) |
||||
} |
||||
if spub := conn.Peer(); spub != server.Public() { |
||||
t.Fatalf("client got peer pubkey %s, want %s", spub, server.Public()) |
||||
} |
||||
if proxy != nil && !proxy.ConnIsFromProxy(si.clientAddr) { |
||||
t.Fatalf("client connected from %s, which isn't the proxy", si.clientAddr) |
||||
} |
||||
} |
||||
|
||||
type serverResult struct { |
||||
err error |
||||
clientAddr string |
||||
version int |
||||
peer key.MachinePublic |
||||
conn *controlbase.Conn |
||||
} |
||||
|
||||
type proxy interface { |
||||
Start(*testing.T) string |
||||
Close() |
||||
ConnIsFromProxy(string) bool |
||||
} |
||||
|
||||
type socksProxy struct { |
||||
sync.Mutex |
||||
proxy socks5.Server |
||||
ln net.Listener |
||||
clientConnAddrs map[string]bool // addrs of the local end of outgoing conns from proxy
|
||||
} |
||||
|
||||
func (s *socksProxy) Start(t *testing.T) (url string) { |
||||
t.Helper() |
||||
s.Lock() |
||||
defer s.Unlock() |
||||
ln, err := net.Listen("tcp", "127.0.0.1:0") |
||||
if err != nil { |
||||
t.Fatalf("listening for SOCKS server: %v", err) |
||||
} |
||||
s.ln = ln |
||||
s.clientConnAddrs = map[string]bool{} |
||||
s.proxy.Logf = t.Logf |
||||
s.proxy.Dialer = s.dialAndRecord |
||||
go s.proxy.Serve(ln) |
||||
return fmt.Sprintf("socks5://%s", ln.Addr().String()) |
||||
} |
||||
|
||||
func (s *socksProxy) Close() { |
||||
s.Lock() |
||||
defer s.Unlock() |
||||
s.ln.Close() |
||||
} |
||||
|
||||
func (s *socksProxy) dialAndRecord(ctx context.Context, network, addr string) (net.Conn, error) { |
||||
var d net.Dialer |
||||
conn, err := d.DialContext(ctx, network, addr) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
s.Lock() |
||||
defer s.Unlock() |
||||
s.clientConnAddrs[conn.LocalAddr().String()] = true |
||||
return conn, nil |
||||
} |
||||
|
||||
func (s *socksProxy) ConnIsFromProxy(addr string) bool { |
||||
s.Lock() |
||||
defer s.Unlock() |
||||
return s.clientConnAddrs[addr] |
||||
} |
||||
|
||||
type httpProxy struct { |
||||
useTLS bool // take incoming connections over TLS
|
||||
allowConnect bool // allow CONNECT for TLS
|
||||
allowHTTP bool // allow plain HTTP proxying
|
||||
|
||||
sync.Mutex |
||||
ln net.Listener |
||||
rp httputil.ReverseProxy |
||||
s http.Server |
||||
clientConnAddrs map[string]bool // addrs of the local end of outgoing conns from proxy
|
||||
} |
||||
|
||||
func (h *httpProxy) Start(t *testing.T) (url string) { |
||||
t.Helper() |
||||
h.Lock() |
||||
defer h.Unlock() |
||||
ln, err := net.Listen("tcp", "127.0.0.1:0") |
||||
if err != nil { |
||||
t.Fatalf("listening for HTTP proxy: %v", err) |
||||
} |
||||
h.ln = ln |
||||
h.rp = httputil.ReverseProxy{ |
||||
Director: func(*http.Request) {}, |
||||
Transport: &http.Transport{ |
||||
DialContext: h.dialAndRecord, |
||||
TLSClientConfig: &tls.Config{ |
||||
InsecureSkipVerify: true, |
||||
}, |
||||
TLSNextProto: map[string]func(string, *tls.Conn) http.RoundTripper{}, |
||||
}, |
||||
} |
||||
h.clientConnAddrs = map[string]bool{} |
||||
h.s.Handler = h |
||||
if h.useTLS { |
||||
h.s.TLSConfig = tlsConfig(t) |
||||
go h.s.ServeTLS(h.ln, "", "") |
||||
return fmt.Sprintf("https://%s", ln.Addr().String()) |
||||
} else { |
||||
go h.s.Serve(h.ln) |
||||
return fmt.Sprintf("http://%s", ln.Addr().String()) |
||||
} |
||||
} |
||||
|
||||
func (h *httpProxy) Close() { |
||||
h.Lock() |
||||
defer h.Unlock() |
||||
h.s.Close() |
||||
} |
||||
|
||||
func (h *httpProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
||||
if r.Method != "CONNECT" { |
||||
if !h.allowHTTP { |
||||
http.Error(w, "http proxy not allowed", 500) |
||||
return |
||||
} |
||||
h.rp.ServeHTTP(w, r) |
||||
return |
||||
} |
||||
|
||||
if !h.allowConnect { |
||||
http.Error(w, "connect not allowed", 500) |
||||
return |
||||
} |
||||
|
||||
dst := r.RequestURI |
||||
c, err := h.dialAndRecord(context.Background(), "tcp", dst) |
||||
if err != nil { |
||||
http.Error(w, err.Error(), 500) |
||||
return |
||||
} |
||||
defer c.Close() |
||||
|
||||
cc, ccbuf, err := w.(http.Hijacker).Hijack() |
||||
if err != nil { |
||||
http.Error(w, err.Error(), 500) |
||||
return |
||||
} |
||||
defer cc.Close() |
||||
|
||||
io.WriteString(cc, "HTTP/1.1 200 OK\r\n\r\n") |
||||
|
||||
errc := make(chan error, 1) |
||||
go func() { |
||||
_, err := io.Copy(cc, c) |
||||
errc <- err |
||||
}() |
||||
go func() { |
||||
_, err := io.Copy(c, ccbuf) |
||||
errc <- err |
||||
}() |
||||
<-errc |
||||
} |
||||
|
||||
func (h *httpProxy) dialAndRecord(ctx context.Context, network, addr string) (net.Conn, error) { |
||||
var d net.Dialer |
||||
conn, err := d.DialContext(ctx, network, addr) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
h.Lock() |
||||
defer h.Unlock() |
||||
h.clientConnAddrs[conn.LocalAddr().String()] = true |
||||
return conn, nil |
||||
} |
||||
|
||||
func (h *httpProxy) ConnIsFromProxy(addr string) bool { |
||||
h.Lock() |
||||
defer h.Unlock() |
||||
return h.clientConnAddrs[addr] |
||||
} |
||||
|
||||
func tlsConfig(t *testing.T) *tls.Config { |
||||
// Cert and key taken from the example code in the crypto/tls
|
||||
// package.
|
||||
certPem := []byte(`-----BEGIN CERTIFICATE----- |
||||
MIIBhTCCASugAwIBAgIQIRi6zePL6mKjOipn+dNuaTAKBggqhkjOPQQDAjASMRAw |
||||
DgYDVQQKEwdBY21lIENvMB4XDTE3MTAyMDE5NDMwNloXDTE4MTAyMDE5NDMwNlow |
||||
EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD0d |
||||
7VNhbWvZLWPuj/RtHFjvtJBEwOkhbN/BnnE8rnZR8+sbwnc/KhCk3FhnpHZnQz7B |
||||
5aETbbIgmuvewdjvSBSjYzBhMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggr |
||||
BgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1UdEQQiMCCCDmxvY2FsaG9zdDo1 |
||||
NDUzgg4xMjcuMC4wLjE6NTQ1MzAKBggqhkjOPQQDAgNIADBFAiEA2zpJEPQyz6/l |
||||
Wf86aX6PepsntZv2GYlA5UpabfT2EZICICpJ5h/iI+i341gBmLiAFQOyTDT+/wQc |
||||
6MF9+Yw1Yy0t |
||||
-----END CERTIFICATE-----`) |
||||
keyPem := []byte(`-----BEGIN EC PRIVATE KEY----- |
||||
MHcCAQEEIIrYSSNQFaA2Hwf1duRSxKtLYX5CB04fSeQ6tF1aY/PuoAoGCCqGSM49 |
||||
AwEHoUQDQgAEPR3tU2Fta9ktY+6P9G0cWO+0kETA6SFs38GecTyudlHz6xvCdz8q |
||||
EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA== |
||||
-----END EC PRIVATE KEY-----`) |
||||
cert, err := tls.X509KeyPair(certPem, keyPem) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
return &tls.Config{ |
||||
Certificates: []tls.Certificate{cert}, |
||||
} |
||||
} |
||||
@ -0,0 +1,95 @@ |
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package controlhttp |
||||
|
||||
import ( |
||||
"bufio" |
||||
"context" |
||||
"encoding/base64" |
||||
"errors" |
||||
"fmt" |
||||
"net" |
||||
"net/http" |
||||
|
||||
"tailscale.com/control/controlbase" |
||||
"tailscale.com/types/key" |
||||
) |
||||
|
||||
// AcceptHTTP upgrades the HTTP request given by w and r into a
|
||||
// Tailscale control protocol base transport connection.
|
||||
//
|
||||
// AcceptHTTP always writes an HTTP response to w. The caller must not
|
||||
// attempt their own response after calling AcceptHTTP.
|
||||
func AcceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, private key.MachinePrivate) (*controlbase.Conn, error) { |
||||
next := r.Header.Get("Upgrade") |
||||
if next == "" { |
||||
http.Error(w, "missing next protocol", http.StatusBadRequest) |
||||
return nil, errors.New("no next protocol in HTTP request") |
||||
} |
||||
if next != upgradeHeaderValue { |
||||
http.Error(w, "unknown next protocol", http.StatusBadRequest) |
||||
return nil, fmt.Errorf("client requested unhandled next protocol %q", next) |
||||
} |
||||
|
||||
initB64 := r.Header.Get(handshakeHeaderName) |
||||
if initB64 == "" { |
||||
http.Error(w, "missing Tailscale handshake header", http.StatusBadRequest) |
||||
return nil, errors.New("no tailscale handshake header in HTTP request") |
||||
} |
||||
init, err := base64.StdEncoding.DecodeString(initB64) |
||||
if err != nil { |
||||
http.Error(w, "invalid tailscale handshake header", http.StatusBadRequest) |
||||
return nil, fmt.Errorf("decoding base64 handshake header: %v", err) |
||||
} |
||||
|
||||
hijacker, ok := w.(http.Hijacker) |
||||
if !ok { |
||||
http.Error(w, "make request over HTTP/1", http.StatusBadRequest) |
||||
return nil, errors.New("can't hijack client connection") |
||||
} |
||||
|
||||
w.Header().Set("Upgrade", upgradeHeaderValue) |
||||
w.Header().Set("Connection", "upgrade") |
||||
w.WriteHeader(http.StatusSwitchingProtocols) |
||||
|
||||
conn, brw, err := hijacker.Hijack() |
||||
if err != nil { |
||||
return nil, fmt.Errorf("hijacking client connection: %w", err) |
||||
} |
||||
if err := brw.Flush(); err != nil { |
||||
conn.Close() |
||||
return nil, fmt.Errorf("flushing hijacked HTTP buffer: %w", err) |
||||
} |
||||
if brw.Reader.Buffered() > 0 { |
||||
conn = &drainBufConn{conn, brw.Reader} |
||||
} |
||||
|
||||
nc, err := controlbase.Server(ctx, conn, private, init) |
||||
if err != nil { |
||||
conn.Close() |
||||
return nil, fmt.Errorf("noise handshake failed: %w", err) |
||||
} |
||||
|
||||
return nc, nil |
||||
} |
||||
|
||||
// drainBufConn is a net.Conn with an initial bunch of bytes in a
|
||||
// bufio.Reader. Read drains the bufio.Reader until empty, then passes
|
||||
// through subsequent reads to the Conn directly.
|
||||
type drainBufConn struct { |
||||
net.Conn |
||||
r *bufio.Reader |
||||
} |
||||
|
||||
func (b *drainBufConn) Read(bs []byte) (int, error) { |
||||
if b.r == nil { |
||||
return b.Conn.Read(bs) |
||||
} |
||||
n, err := b.r.Read(bs) |
||||
if b.r.Buffered() == 0 { |
||||
b.r = nil |
||||
} |
||||
return n, err |
||||
} |
||||
Loading…
Reference in new issue