client_test.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package ts2021
  4. import (
  5. "context"
  6. "encoding/binary"
  7. "encoding/json"
  8. "io"
  9. "math"
  10. "net/http"
  11. "net/http/httptrace"
  12. "sync/atomic"
  13. "testing"
  14. "time"
  15. "golang.org/x/net/http2"
  16. "tailscale.com/control/controlhttp/controlhttpserver"
  17. "tailscale.com/net/netmon"
  18. "tailscale.com/net/tsdial"
  19. "tailscale.com/tailcfg"
  20. "tailscale.com/tstest/nettest"
  21. "tailscale.com/types/key"
  22. "tailscale.com/types/logger"
  23. "tailscale.com/util/must"
  24. )
  25. // maxAllowedNoiseVersion is the highest we expect the Tailscale
  26. // capability version to ever get. It's a value close to 2^16, but
  27. // with enough leeway that we get a very early warning that it's time
  28. // to rework the wire protocol to allow larger versions, while still
  29. // giving us headroom to bump this test and fix the build.
  30. //
  31. // Code elsewhere in the client will panic() if the tailcfg capability
  32. // version exceeds 16 bits, so take a failure of this test seriously.
  33. const maxAllowedNoiseVersion = math.MaxUint16 - 5000
  34. func TestNoiseVersion(t *testing.T) {
  35. if tailcfg.CurrentCapabilityVersion > maxAllowedNoiseVersion {
  36. t.Fatalf("tailcfg.CurrentCapabilityVersion is %d, want <=%d", tailcfg.CurrentCapabilityVersion, maxAllowedNoiseVersion)
  37. }
  38. }
  39. type noiseClientTest struct {
  40. sendEarlyPayload bool
  41. }
  42. func TestNoiseClientHTTP2Upgrade(t *testing.T) {
  43. noiseClientTest{}.run(t)
  44. }
  45. func TestNoiseClientHTTP2Upgrade_earlyPayload(t *testing.T) {
  46. noiseClientTest{
  47. sendEarlyPayload: true,
  48. }.run(t)
  49. }
  50. var (
  51. testPrivKey = key.NewMachine()
  52. testServerPub = key.NewMachine().Public()
  53. )
  54. func makeClientWithURL(t *testing.T, url string) *Client {
  55. nc, err := NewClient(ClientOpts{
  56. Logf: t.Logf,
  57. PrivKey: testPrivKey,
  58. ServerPubKey: testServerPub,
  59. ServerURL: url,
  60. Dialer: tsdial.NewDialer(netmon.NewStatic()),
  61. })
  62. if err != nil {
  63. t.Fatal(err)
  64. }
  65. t.Cleanup(func() { nc.Close() })
  66. return nc
  67. }
  68. func TestNoiseClientPortsAreSet(t *testing.T) {
  69. tests := []struct {
  70. name string
  71. url string
  72. wantHTTPS string
  73. wantHTTP string
  74. }{
  75. {
  76. name: "https-url",
  77. url: "https://example.com",
  78. wantHTTPS: "443",
  79. wantHTTP: "80",
  80. },
  81. {
  82. name: "http-url",
  83. url: "http://example.com",
  84. wantHTTPS: "443", // TODO(bradfitz): questionable; change?
  85. wantHTTP: "80",
  86. },
  87. {
  88. name: "https-url-custom-port",
  89. url: "https://example.com:123",
  90. wantHTTPS: "123",
  91. wantHTTP: "",
  92. },
  93. {
  94. name: "http-url-custom-port",
  95. url: "http://example.com:123",
  96. wantHTTPS: "443", // TODO(bradfitz): questionable; change?
  97. wantHTTP: "123",
  98. },
  99. {
  100. name: "http-loopback-no-port",
  101. url: "http://127.0.0.1",
  102. wantHTTPS: "",
  103. wantHTTP: "80",
  104. },
  105. {
  106. name: "http-loopback-custom-port",
  107. url: "http://127.0.0.1:8080",
  108. wantHTTPS: "",
  109. wantHTTP: "8080",
  110. },
  111. {
  112. name: "http-localhost-no-port",
  113. url: "http://localhost",
  114. wantHTTPS: "",
  115. wantHTTP: "80",
  116. },
  117. {
  118. name: "http-localhost-custom-port",
  119. url: "http://localhost:8080",
  120. wantHTTPS: "",
  121. wantHTTP: "8080",
  122. },
  123. {
  124. name: "http-private-ip-no-port",
  125. url: "http://192.168.2.3",
  126. wantHTTPS: "",
  127. wantHTTP: "80",
  128. },
  129. {
  130. name: "http-private-ip-custom-port",
  131. url: "http://192.168.2.3:8080",
  132. wantHTTPS: "",
  133. wantHTTP: "8080",
  134. },
  135. {
  136. name: "http-public-ip",
  137. url: "http://1.2.3.4",
  138. wantHTTPS: "443", // TODO(bradfitz): questionable; change?
  139. wantHTTP: "80",
  140. },
  141. {
  142. name: "http-public-ip-custom-port",
  143. url: "http://1.2.3.4:8080",
  144. wantHTTPS: "443", // TODO(bradfitz): questionable; change?
  145. wantHTTP: "8080",
  146. },
  147. {
  148. name: "https-public-ip",
  149. url: "https://1.2.3.4",
  150. wantHTTPS: "443",
  151. wantHTTP: "80",
  152. },
  153. {
  154. name: "https-public-ip-custom-port",
  155. url: "https://1.2.3.4:8080",
  156. wantHTTPS: "8080",
  157. wantHTTP: "",
  158. },
  159. }
  160. for _, tt := range tests {
  161. t.Run(tt.name, func(t *testing.T) {
  162. nc := makeClientWithURL(t, tt.url)
  163. if nc.httpsPort != tt.wantHTTPS {
  164. t.Errorf("nc.httpsPort = %q; want %q", nc.httpsPort, tt.wantHTTPS)
  165. }
  166. if nc.httpPort != tt.wantHTTP {
  167. t.Errorf("nc.httpPort = %q; want %q", nc.httpPort, tt.wantHTTP)
  168. }
  169. })
  170. }
  171. }
  172. func (tt noiseClientTest) run(t *testing.T) {
  173. serverPrivate := key.NewMachine()
  174. clientPrivate := key.NewMachine()
  175. chalPrivate := key.NewChallenge()
  176. const msg = "Hello, client"
  177. h2 := &http2.Server{}
  178. nw := nettest.GetNetwork(t)
  179. hs := nettest.NewHTTPServer(nw, &Upgrader{
  180. h2srv: h2,
  181. noiseKeyPriv: serverPrivate,
  182. sendEarlyPayload: tt.sendEarlyPayload,
  183. challenge: chalPrivate,
  184. httpBaseConfig: &http.Server{
  185. Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  186. w.Header().Set("Content-Type", "text/plain")
  187. io.WriteString(w, msg)
  188. }),
  189. },
  190. })
  191. defer hs.Close()
  192. dialer := tsdial.NewDialer(netmon.NewStatic())
  193. if nettest.PreferMemNetwork() {
  194. dialer.SetSystemDialerForTest(nw.Dial)
  195. }
  196. nc, err := NewClient(ClientOpts{
  197. PrivKey: clientPrivate,
  198. ServerPubKey: serverPrivate.Public(),
  199. ServerURL: hs.URL,
  200. Dialer: dialer,
  201. Logf: t.Logf,
  202. })
  203. if err != nil {
  204. t.Fatal(err)
  205. }
  206. var sawConn atomic.Bool
  207. trace := httptrace.WithClientTrace(t.Context(), &httptrace.ClientTrace{
  208. GotConn: func(ci httptrace.GotConnInfo) {
  209. ncc, ok := ci.Conn.(*Conn)
  210. if !ok {
  211. // This trace hook sees two dials: the lower-level controlhttp upgrade's
  212. // dial (a tsdial.sysConn), and then the *ts2021.Conn we want.
  213. // Ignore the first one.
  214. return
  215. }
  216. sawConn.Store(true)
  217. ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
  218. defer cancel()
  219. payload, err := ncc.GetEarlyPayload(ctx)
  220. if err != nil {
  221. t.Errorf("GetEarlyPayload: %v", err)
  222. return
  223. }
  224. gotNonNil := payload != nil
  225. if gotNonNil != tt.sendEarlyPayload {
  226. t.Errorf("sendEarlyPayload = %v but got earlyPayload = %T", tt.sendEarlyPayload, payload)
  227. }
  228. if payload != nil {
  229. if payload.NodeKeyChallenge != chalPrivate.Public() {
  230. t.Errorf("earlyPayload.NodeKeyChallenge = %v; want %v", payload.NodeKeyChallenge, chalPrivate.Public())
  231. }
  232. }
  233. },
  234. })
  235. req := must.Get(http.NewRequestWithContext(trace, "GET", "https://unused.example/", nil))
  236. checkRes := func(t *testing.T, res *http.Response) {
  237. t.Helper()
  238. defer res.Body.Close()
  239. all, err := io.ReadAll(res.Body)
  240. if err != nil {
  241. t.Fatal(err)
  242. }
  243. if string(all) != msg {
  244. t.Errorf("got response %q; want %q", all, msg)
  245. }
  246. }
  247. // Verify we can do HTTP/2 against that conn.
  248. res, err := nc.Do(req)
  249. if err != nil {
  250. t.Fatal(err)
  251. }
  252. checkRes(t, res)
  253. if !sawConn.Load() {
  254. t.Error("ClientTrace.GotConn never saw the *ts2021.Conn")
  255. }
  256. // And try using the high-level nc.post API as well.
  257. res, err = nc.Post(context.Background(), "/", key.NodePublic{}, nil)
  258. if err != nil {
  259. t.Fatal(err)
  260. }
  261. checkRes(t, res)
  262. }
  263. // Upgrader is an http.Handler that hijacks and upgrades POST-with-Upgrade
  264. // request to a Tailscale 2021 connection, then hands the resulting
  265. // controlbase.Conn off to h2srv.
  266. type Upgrader struct {
  267. // h2srv is that will handle requests after the
  268. // connection has been upgraded to HTTP/2-over-noise.
  269. h2srv *http2.Server
  270. // httpBaseConfig is the http1 server config that h2srv is
  271. // associated with.
  272. httpBaseConfig *http.Server
  273. logf logger.Logf
  274. noiseKeyPriv key.MachinePrivate
  275. challenge key.ChallengePrivate
  276. sendEarlyPayload bool
  277. }
  278. func (up *Upgrader) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  279. if up == nil || up.h2srv == nil {
  280. http.Error(w, "invalid server config", http.StatusServiceUnavailable)
  281. return
  282. }
  283. if r.URL.Path != "/ts2021" {
  284. http.Error(w, "ts2021 upgrader installed at wrong path", http.StatusBadGateway)
  285. return
  286. }
  287. if up.noiseKeyPriv.IsZero() {
  288. http.Error(w, "keys not available", http.StatusServiceUnavailable)
  289. return
  290. }
  291. earlyWriteFn := func(protocolVersion int, w io.Writer) error {
  292. if !up.sendEarlyPayload {
  293. return nil
  294. }
  295. earlyJSON, err := json.Marshal(&tailcfg.EarlyNoise{
  296. NodeKeyChallenge: up.challenge.Public(),
  297. })
  298. if err != nil {
  299. return err
  300. }
  301. // 5 bytes that won't be mistaken for an HTTP/2 frame:
  302. // https://httpwg.org/specs/rfc7540.html#rfc.section.4.1 (Especially not
  303. // an HTTP/2 settings frame, which isn't of type 'T')
  304. var notH2Frame [5]byte
  305. copy(notH2Frame[:], EarlyPayloadMagic)
  306. var lenBuf [4]byte
  307. binary.BigEndian.PutUint32(lenBuf[:], uint32(len(earlyJSON)))
  308. // These writes are all buffered by caller, so fine to do them
  309. // separately:
  310. if _, err := w.Write(notH2Frame[:]); err != nil {
  311. return err
  312. }
  313. if _, err := w.Write(lenBuf[:]); err != nil {
  314. return err
  315. }
  316. if _, err := w.Write(earlyJSON[:]); err != nil {
  317. return err
  318. }
  319. return nil
  320. }
  321. cbConn, err := controlhttpserver.AcceptHTTP(r.Context(), w, r, up.noiseKeyPriv, earlyWriteFn)
  322. if err != nil {
  323. up.logf("controlhttp: Accept: %v", err)
  324. return
  325. }
  326. defer cbConn.Close()
  327. up.h2srv.ServeConn(cbConn, &http2.ServeConnOpts{
  328. BaseConfig: up.httpBaseConfig,
  329. })
  330. }