mirror of
https://github.com/tailscale/tailscale.git
synced 2025-01-07 08:07:42 +00:00
client/tailscale, tsnet, ipn/ipnlocal: prove nodekey ownership over noise
Fixes #5972 Change-Id: Ic33a93d3613ac5dbf172d6a8a459ca06a7f9e547 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
8c790207a0
commit
910db02652
@ -18,9 +18,6 @@
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"tailscale.com/types/key"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// I_Acknowledge_This_API_Is_Unstable must be set true to use this package
|
// I_Acknowledge_This_API_Is_Unstable must be set true to use this package
|
||||||
@ -93,29 +90,6 @@ func (c *Client) setAuth(r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// nodeKeyAuth is an AuthMethod for NewClient that authenticates requests
|
|
||||||
// using a node key over the Noise protocol.
|
|
||||||
type nodeKeyAuth key.NodePublic
|
|
||||||
|
|
||||||
func (k nodeKeyAuth) modifyRequest(req *http.Request) {
|
|
||||||
// QueryEscape the node key since it has a colon in it.
|
|
||||||
nk := url.QueryEscape(key.NodePublic(k).String())
|
|
||||||
req.SetBasicAuth(nk, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewNoiseClient is a convenience method for instantiating a new Client
|
|
||||||
// that uses the Noise protocol for authentication.
|
|
||||||
//
|
|
||||||
// tailnet is the globally unique identifier for a Tailscale network, such
|
|
||||||
// as "example.com" or "user@gmail.com".
|
|
||||||
func NewNoiseClient(tailnet string, noiseRoundTripper http.RoundTripper, nk key.NodePublic) *Client {
|
|
||||||
return &Client{
|
|
||||||
tailnet: tailnet,
|
|
||||||
auth: nodeKeyAuth(nk),
|
|
||||||
HTTPClient: &http.Client{Transport: noiseRoundTripper},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClient is a convenience method for instantiating a new Client.
|
// NewClient is a convenience method for instantiating a new Client.
|
||||||
//
|
//
|
||||||
// tailnet is the globally unique identifier for a Tailscale network, such
|
// tailnet is the globally unique identifier for a Tailscale network, such
|
||||||
|
@ -730,3 +730,13 @@ func (c *Auto) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) error {
|
|||||||
func (c *Auto) DoNoiseRequest(req *http.Request) (*http.Response, error) {
|
func (c *Auto) DoNoiseRequest(req *http.Request) (*http.Response, error) {
|
||||||
return c.direct.DoNoiseRequest(req)
|
return c.direct.DoNoiseRequest(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSingleUseNoiseRoundTripper returns a RoundTripper that can be only be used
|
||||||
|
// once (and must be used once) to make a single HTTP request over the noise
|
||||||
|
// channel to the coordination server.
|
||||||
|
//
|
||||||
|
// In addition to the RoundTripper, it returns the HTTP/2 channel's early noise
|
||||||
|
// payload, if any.
|
||||||
|
func (c *Auto) GetSingleUseNoiseRoundTripper(ctx context.Context) (http.RoundTripper, *tailcfg.EarlyNoise, error) {
|
||||||
|
return c.direct.GetSingleUseNoiseRoundTripper(ctx)
|
||||||
|
}
|
||||||
|
@ -1607,6 +1607,33 @@ func (c *Direct) DoNoiseRequest(req *http.Request) (*http.Response, error) {
|
|||||||
return nc.Do(req)
|
return nc.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSingleUseNoiseRoundTripper returns a RoundTripper that can be only be used
|
||||||
|
// once (and must be used once) to make a single HTTP request over the noise
|
||||||
|
// channel to the coordination server.
|
||||||
|
//
|
||||||
|
// In addition to the RoundTripper, it returns the HTTP/2 channel's early noise
|
||||||
|
// payload, if any.
|
||||||
|
func (c *Direct) GetSingleUseNoiseRoundTripper(ctx context.Context) (http.RoundTripper, *tailcfg.EarlyNoise, error) {
|
||||||
|
nc, err := c.getNoiseClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
for tries := 0; tries < 3; tries++ {
|
||||||
|
conn, err := nc.getConn(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
earlyPayloadMaybeNil, err := conn.getEarlyPayload(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if conn.h2cc.ReserveNewRequest() {
|
||||||
|
return conn, earlyPayloadMaybeNil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil, errors.New("[unexpected] failed to reserve a request on a connection")
|
||||||
|
}
|
||||||
|
|
||||||
// doPingerPing sends a Ping to pr.IP using pinger, and sends an http request back to
|
// doPingerPing sends a Ping to pr.IP using pinger, and sends an http request back to
|
||||||
// pr.URL with ping response data.
|
// pr.URL with ping response data.
|
||||||
func doPingerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinger Pinger, pingType tailcfg.PingType) {
|
func doPingerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinger Pinger, pingType tailcfg.PingType) {
|
||||||
|
@ -42,12 +42,24 @@ type noiseConn struct {
|
|||||||
reader io.Reader // (effectively Conn.Reader after header)
|
reader io.Reader // (effectively Conn.Reader after header)
|
||||||
earlyPayloadReady chan struct{} // closed after earlyPayload is set (including set to nil)
|
earlyPayloadReady chan struct{} // closed after earlyPayload is set (including set to nil)
|
||||||
earlyPayload *tailcfg.EarlyNoise
|
earlyPayload *tailcfg.EarlyNoise
|
||||||
|
earlyPayloadErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *noiseConn) RoundTrip(r *http.Request) (*http.Response, error) {
|
func (c *noiseConn) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||||
return c.h2cc.RoundTrip(r)
|
return c.h2cc.RoundTrip(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getEarlyPayload waits for the early noise payload to arrive.
|
||||||
|
// It may return (nil, nil) if the server begins HTTP/2 without one.
|
||||||
|
func (c *noiseConn) getEarlyPayload(ctx context.Context) (*tailcfg.EarlyNoise, error) {
|
||||||
|
select {
|
||||||
|
case <-c.earlyPayloadReady:
|
||||||
|
return c.earlyPayload, c.earlyPayloadErr
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The first 9 bytes from the server to client over Noise are either an HTTP/2
|
// The first 9 bytes from the server to client over Noise are either an HTTP/2
|
||||||
// settings frame (a normal HTTP/2 setup) or, as we added later, an "early payload"
|
// settings frame (a normal HTTP/2 setup) or, as we added later, an "early payload"
|
||||||
// header that's also 9 bytes long: 5 bytes (earlyPayloadMagic) followed by 4 bytes
|
// header that's also 9 bytes long: 5 bytes (earlyPayloadMagic) followed by 4 bytes
|
||||||
@ -80,33 +92,38 @@ func (c *noiseConn) Read(p []byte) (n int, err error) {
|
|||||||
// c.earlyPayload, closing c.earlyPayloadReady, and initializing c.reader for
|
// c.earlyPayload, closing c.earlyPayloadReady, and initializing c.reader for
|
||||||
// future reads.
|
// future reads.
|
||||||
func (c *noiseConn) readHeader() {
|
func (c *noiseConn) readHeader() {
|
||||||
|
defer close(c.earlyPayloadReady)
|
||||||
|
|
||||||
|
setErr := func(err error) {
|
||||||
|
c.reader = returnErrReader{err}
|
||||||
|
c.earlyPayloadErr = err
|
||||||
|
}
|
||||||
|
|
||||||
var hdr [hdrLen]byte
|
var hdr [hdrLen]byte
|
||||||
if _, err := io.ReadFull(c.Conn, hdr[:]); err != nil {
|
if _, err := io.ReadFull(c.Conn, hdr[:]); err != nil {
|
||||||
c.reader = returnErrReader{err}
|
setErr(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if string(hdr[:len(earlyPayloadMagic)]) != earlyPayloadMagic {
|
if string(hdr[:len(earlyPayloadMagic)]) != earlyPayloadMagic {
|
||||||
// No early payload. We have to return the 9 bytes read we already
|
// No early payload. We have to return the 9 bytes read we already
|
||||||
// consumed.
|
// consumed.
|
||||||
close(c.earlyPayloadReady)
|
|
||||||
c.reader = io.MultiReader(bytes.NewReader(hdr[:]), c.Conn)
|
c.reader = io.MultiReader(bytes.NewReader(hdr[:]), c.Conn)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
epLen := binary.BigEndian.Uint32(hdr[len(earlyPayloadMagic):])
|
epLen := binary.BigEndian.Uint32(hdr[len(earlyPayloadMagic):])
|
||||||
if epLen > 10<<20 {
|
if epLen > 10<<20 {
|
||||||
c.reader = returnErrReader{errors.New("invalid early payload length")}
|
setErr(errors.New("invalid early payload length"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
payBuf := make([]byte, epLen)
|
payBuf := make([]byte, epLen)
|
||||||
if _, err := io.ReadFull(c.Conn, payBuf); err != nil {
|
if _, err := io.ReadFull(c.Conn, payBuf); err != nil {
|
||||||
c.reader = returnErrReader{err}
|
setErr(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(payBuf, &c.earlyPayload); err != nil {
|
if err := json.Unmarshal(payBuf, &c.earlyPayload); err != nil {
|
||||||
c.reader = returnErrReader{err}
|
setErr(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
close(c.earlyPayloadReady)
|
|
||||||
c.reader = c.Conn
|
c.reader = c.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,12 +6,14 @@
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -3765,18 +3767,60 @@ func (b *LocalBackend) magicConn() (*magicsock.Conn, error) {
|
|||||||
return mc, nil
|
return mc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type noiseRoundTripper struct {
|
type keyProvingNoiseRoundTripper struct {
|
||||||
*LocalBackend
|
b *LocalBackend
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n noiseRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
func (n keyProvingNoiseRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
return n.LocalBackend.DoNoiseRequest(req)
|
b := n.b
|
||||||
|
|
||||||
|
var priv key.NodePrivate
|
||||||
|
|
||||||
|
b.mu.Lock()
|
||||||
|
cc := b.ccAuto
|
||||||
|
if nm := b.netMap; nm != nil {
|
||||||
|
priv = nm.PrivateKey
|
||||||
|
}
|
||||||
|
b.mu.Unlock()
|
||||||
|
if cc == nil {
|
||||||
|
return nil, errors.New("no client")
|
||||||
|
}
|
||||||
|
if priv.IsZero() {
|
||||||
|
return nil, errors.New("no netmap or private key")
|
||||||
|
}
|
||||||
|
rt, ep, err := cc.GetSingleUseNoiseRoundTripper(req.Context())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ep == nil || ep.NodeKeyChallenge.IsZero() {
|
||||||
|
go rt.RoundTrip(new(http.Request)) // return our reservation with a bogus request
|
||||||
|
return nil, errors.New("this coordination server does not support API calls over the Noise channel")
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryEscape the node key since it has a colon in it.
|
||||||
|
nk := url.QueryEscape(priv.Public().String())
|
||||||
|
req.SetBasicAuth(nk, "")
|
||||||
|
|
||||||
|
// genNodeProofHeaderValue returns the Tailscale-Node-Proof header's value to prove
|
||||||
|
// to chalPub that we control claimedPrivate.
|
||||||
|
genNodeProofHeaderValue := func(claimedPrivate key.NodePrivate, chalPub key.ChallengePublic) string {
|
||||||
|
// TODO(bradfitz): cache this somewhere?
|
||||||
|
box := claimedPrivate.SealToChallenge(chalPub, []byte(chalPub.String()))
|
||||||
|
return claimedPrivate.Public().String() + " " + base64.StdEncoding.EncodeToString(box)
|
||||||
|
}
|
||||||
|
|
||||||
|
// And prove we have the private key corresponding to the public key sent
|
||||||
|
// tin the basic auth username.
|
||||||
|
req.Header.Set("Tailscale-Node-Proof", genNodeProofHeaderValue(priv, ep.NodeKeyChallenge))
|
||||||
|
|
||||||
|
return rt.RoundTrip(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NoiseRoundTripper returns an http.RoundTripper that uses the LocalBackend's
|
// KeyProvingNoiseRoundTripper returns an http.RoundTripper that uses the LocalBackend's
|
||||||
// DoNoiseRequest method.
|
// DoNoiseRequest method and mutates the request to add an authorization header
|
||||||
func (b *LocalBackend) NoiseRoundTripper() http.RoundTripper {
|
// to prove the client's nodekey.
|
||||||
return noiseRoundTripper{b}
|
func (b *LocalBackend) KeyProvingNoiseRoundTripper() http.RoundTripper {
|
||||||
|
return keyProvingNoiseRoundTripper{b}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DoNoiseRequest sends a request to URL over the control plane
|
// DoNoiseRequest sends a request to URL over the control plane
|
||||||
|
@ -493,11 +493,8 @@ func (s *Server) APIClient() (*tailscale.Client, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
nm := s.lb.NetMap()
|
c := tailscale.NewClient("-", nil)
|
||||||
if nm == nil {
|
c.HTTPClient = &http.Client{Transport: s.lb.KeyProvingNoiseRoundTripper()}
|
||||||
return nil, errors.New("no netmap, not logged in?")
|
|
||||||
}
|
|
||||||
c := tailscale.NewNoiseClient(nm.Domain, s.lb.NoiseRoundTripper(), nm.NodeKey)
|
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,3 +82,6 @@ func (k ChallengePublic) MarshalText() ([]byte, error) {
|
|||||||
func (k *ChallengePublic) UnmarshalText(b []byte) error {
|
func (k *ChallengePublic) UnmarshalText(b []byte) error {
|
||||||
return parseHex(k.k[:], mem.B(b), mem.S(chalPublicHexPrefix))
|
return parseHex(k.k[:], mem.B(b), mem.S(chalPublicHexPrefix))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsZero reports whether k is the zero value.
|
||||||
|
func (k ChallengePublic) IsZero() bool { return k == ChallengePublic{} }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user