|
|
|
|
@ -4,27 +4,40 @@ |
|
|
|
|
package tsnet |
|
|
|
|
|
|
|
|
|
import ( |
|
|
|
|
"bufio" |
|
|
|
|
"context" |
|
|
|
|
"crypto/ecdsa" |
|
|
|
|
"crypto/elliptic" |
|
|
|
|
"crypto/rand" |
|
|
|
|
"crypto/tls" |
|
|
|
|
"crypto/x509" |
|
|
|
|
"crypto/x509/pkix" |
|
|
|
|
"errors" |
|
|
|
|
"flag" |
|
|
|
|
"fmt" |
|
|
|
|
"io" |
|
|
|
|
"io/ioutil" |
|
|
|
|
"math/big" |
|
|
|
|
"net" |
|
|
|
|
"net/http" |
|
|
|
|
"net/http/httptest" |
|
|
|
|
"net/netip" |
|
|
|
|
"os" |
|
|
|
|
"path/filepath" |
|
|
|
|
"strings" |
|
|
|
|
"sync" |
|
|
|
|
"testing" |
|
|
|
|
"time" |
|
|
|
|
|
|
|
|
|
"golang.org/x/net/proxy" |
|
|
|
|
"tailscale.com/ipn" |
|
|
|
|
"tailscale.com/ipn/store/mem" |
|
|
|
|
"tailscale.com/net/netns" |
|
|
|
|
"tailscale.com/tailcfg" |
|
|
|
|
"tailscale.com/tstest/integration" |
|
|
|
|
"tailscale.com/tstest/integration/testcontrol" |
|
|
|
|
"tailscale.com/types/logger" |
|
|
|
|
"tailscale.com/util/must" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
// TestListener_Server ensures that the listener type always keeps the Server
|
|
|
|
|
@ -93,6 +106,10 @@ func startControl(t *testing.T) (controlURL string) { |
|
|
|
|
derpMap := integration.RunDERPAndSTUN(t, derpLogf, "127.0.0.1") |
|
|
|
|
control := &testcontrol.Server{ |
|
|
|
|
DERPMap: derpMap, |
|
|
|
|
DNSConfig: &tailcfg.DNSConfig{ |
|
|
|
|
Proxied: true, |
|
|
|
|
}, |
|
|
|
|
MagicDNSDomain: "tail-scale.ts.net", |
|
|
|
|
} |
|
|
|
|
control.HTTPTestServer = httptest.NewUnstartedServer(control) |
|
|
|
|
control.HTTPTestServer.Start() |
|
|
|
|
@ -102,17 +119,96 @@ func startControl(t *testing.T) (controlURL string) { |
|
|
|
|
return controlURL |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
type testCertIssuer struct { |
|
|
|
|
mu sync.Mutex |
|
|
|
|
certs map[string]*tls.Certificate |
|
|
|
|
|
|
|
|
|
root *x509.Certificate |
|
|
|
|
rootKey *ecdsa.PrivateKey |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func newCertIssuer() *testCertIssuer { |
|
|
|
|
rootKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) |
|
|
|
|
if err != nil { |
|
|
|
|
panic(err) |
|
|
|
|
} |
|
|
|
|
t := &x509.Certificate{ |
|
|
|
|
SerialNumber: big.NewInt(1), |
|
|
|
|
Subject: pkix.Name{ |
|
|
|
|
CommonName: "root", |
|
|
|
|
}, |
|
|
|
|
NotBefore: time.Now(), |
|
|
|
|
NotAfter: time.Now().Add(time.Hour), |
|
|
|
|
IsCA: true, |
|
|
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, |
|
|
|
|
KeyUsage: x509.KeyUsageCertSign, |
|
|
|
|
BasicConstraintsValid: true, |
|
|
|
|
} |
|
|
|
|
rootDER, err := x509.CreateCertificate(rand.Reader, t, t, &rootKey.PublicKey, rootKey) |
|
|
|
|
if err != nil { |
|
|
|
|
panic(err) |
|
|
|
|
} |
|
|
|
|
rootCA, err := x509.ParseCertificate(rootDER) |
|
|
|
|
if err != nil { |
|
|
|
|
panic(err) |
|
|
|
|
} |
|
|
|
|
return &testCertIssuer{ |
|
|
|
|
certs: make(map[string]*tls.Certificate), |
|
|
|
|
root: rootCA, |
|
|
|
|
rootKey: rootKey, |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (tci *testCertIssuer) getCert(chi *tls.ClientHelloInfo) (*tls.Certificate, error) { |
|
|
|
|
tci.mu.Lock() |
|
|
|
|
defer tci.mu.Unlock() |
|
|
|
|
cert, ok := tci.certs[chi.ServerName] |
|
|
|
|
if ok { |
|
|
|
|
return cert, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
certPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
certTmpl := &x509.Certificate{ |
|
|
|
|
SerialNumber: big.NewInt(1), |
|
|
|
|
DNSNames: []string{chi.ServerName}, |
|
|
|
|
NotBefore: time.Now(), |
|
|
|
|
NotAfter: time.Now().Add(time.Hour), |
|
|
|
|
} |
|
|
|
|
certDER, err := x509.CreateCertificate(rand.Reader, certTmpl, tci.root, &certPrivKey.PublicKey, tci.rootKey) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
cert = &tls.Certificate{ |
|
|
|
|
Certificate: [][]byte{certDER, tci.root.Raw}, |
|
|
|
|
PrivateKey: certPrivKey, |
|
|
|
|
} |
|
|
|
|
tci.certs[chi.ServerName] = cert |
|
|
|
|
return cert, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (tci *testCertIssuer) Pool() *x509.CertPool { |
|
|
|
|
p := x509.NewCertPool() |
|
|
|
|
p.AddCert(tci.root) |
|
|
|
|
return p |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
var testCertRoot = newCertIssuer() |
|
|
|
|
|
|
|
|
|
func startServer(t *testing.T, ctx context.Context, controlURL, hostname string) (*Server, netip.Addr) { |
|
|
|
|
t.Helper() |
|
|
|
|
|
|
|
|
|
tmp := filepath.Join(t.TempDir(), hostname) |
|
|
|
|
os.MkdirAll(tmp, 0755) |
|
|
|
|
s := &Server{ |
|
|
|
|
Dir: tmp, |
|
|
|
|
ControlURL: controlURL, |
|
|
|
|
Hostname: hostname, |
|
|
|
|
Store: new(mem.Store), |
|
|
|
|
Ephemeral: true, |
|
|
|
|
Dir: tmp, |
|
|
|
|
ControlURL: controlURL, |
|
|
|
|
Hostname: hostname, |
|
|
|
|
Store: new(mem.Store), |
|
|
|
|
Ephemeral: true, |
|
|
|
|
getCertForTesting: testCertRoot.getCert, |
|
|
|
|
} |
|
|
|
|
if !*verboseNodes { |
|
|
|
|
s.Logf = logger.Discard |
|
|
|
|
@ -368,3 +464,112 @@ func TestListenerCleanup(t *testing.T) { |
|
|
|
|
t.Fatalf("second ln.Close error: %v, want net.ErrClosed", err) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func TestFunnel(t *testing.T) { |
|
|
|
|
ctx, dialCancel := context.WithTimeout(context.Background(), 30*time.Second) |
|
|
|
|
defer dialCancel() |
|
|
|
|
|
|
|
|
|
controlURL := startControl(t) |
|
|
|
|
s1, _ := startServer(t, ctx, controlURL, "s1") |
|
|
|
|
s2, _ := startServer(t, ctx, controlURL, "s2") |
|
|
|
|
|
|
|
|
|
ln := must.Get(s1.ListenFunnel("tcp", ":443")) |
|
|
|
|
defer ln.Close() |
|
|
|
|
wantSrcAddrPort := netip.MustParseAddrPort("127.0.0.1:1234") |
|
|
|
|
wantTarget := ipn.HostPort("s1.tail-scale.ts.net:443") |
|
|
|
|
srv := &http.Server{ |
|
|
|
|
ConnContext: func(ctx context.Context, c net.Conn) context.Context { |
|
|
|
|
tc, ok := c.(*tls.Conn) |
|
|
|
|
if !ok { |
|
|
|
|
t.Errorf("ConnContext called with non-TLS conn: %T", c) |
|
|
|
|
} |
|
|
|
|
if fc, ok := tc.NetConn().(*ipn.FunnelConn); !ok { |
|
|
|
|
t.Errorf("ConnContext called with non-FunnelConn: %T", c) |
|
|
|
|
} else if fc.Src != wantSrcAddrPort { |
|
|
|
|
t.Errorf("ConnContext called with wrong SrcAddrPort; got %v, want %v", fc.Src, wantSrcAddrPort) |
|
|
|
|
} else if fc.Target != wantTarget { |
|
|
|
|
t.Errorf("ConnContext called with wrong Target; got %q, want %q", fc.Target, wantTarget) |
|
|
|
|
} |
|
|
|
|
return ctx |
|
|
|
|
}, |
|
|
|
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
fmt.Fprintf(w, "hello") |
|
|
|
|
}), |
|
|
|
|
} |
|
|
|
|
go srv.Serve(ln) |
|
|
|
|
|
|
|
|
|
c := &http.Client{ |
|
|
|
|
Transport: &http.Transport{ |
|
|
|
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { |
|
|
|
|
return dialIngressConn(s2, s1, addr) |
|
|
|
|
}, |
|
|
|
|
TLSClientConfig: &tls.Config{ |
|
|
|
|
RootCAs: testCertRoot.Pool(), |
|
|
|
|
}, |
|
|
|
|
}, |
|
|
|
|
} |
|
|
|
|
resp, err := c.Get("https://s1.tail-scale.ts.net:443") |
|
|
|
|
if err != nil { |
|
|
|
|
t.Fatal(err) |
|
|
|
|
} |
|
|
|
|
defer resp.Body.Close() |
|
|
|
|
if resp.StatusCode != 200 { |
|
|
|
|
t.Errorf("unexpected status code: %v", resp.StatusCode) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
body, err := ioutil.ReadAll(resp.Body) |
|
|
|
|
if err != nil { |
|
|
|
|
t.Fatal(err) |
|
|
|
|
} |
|
|
|
|
if string(body) != "hello" { |
|
|
|
|
t.Errorf("unexpected body: %q", body) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func dialIngressConn(from, to *Server, target string) (net.Conn, error) { |
|
|
|
|
toLC := must.Get(to.LocalClient()) |
|
|
|
|
toStatus := must.Get(toLC.StatusWithoutPeers(context.Background())) |
|
|
|
|
peer6 := toStatus.Self.PeerAPIURL[1] // IPv6
|
|
|
|
|
toPeerAPI, ok := strings.CutPrefix(peer6, "http://") |
|
|
|
|
if !ok { |
|
|
|
|
return nil, fmt.Errorf("unexpected PeerAPIURL %q", peer6) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
dialCtx, dialCancel := context.WithTimeout(context.Background(), 30*time.Second) |
|
|
|
|
outConn, err := from.Dial(dialCtx, "tcp", toPeerAPI) |
|
|
|
|
dialCancel() |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
req, err := http.NewRequest("POST", "/v0/ingress", nil) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
req.Host = toPeerAPI |
|
|
|
|
req.Header.Set("Tailscale-Ingress-Src", "127.0.0.1:1234") |
|
|
|
|
req.Header.Set("Tailscale-Ingress-Target", target) |
|
|
|
|
if err := req.Write(outConn); err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
br := bufio.NewReader(outConn) |
|
|
|
|
res, err := http.ReadResponse(br, req) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
defer res.Body.Close() // just to appease vet
|
|
|
|
|
if res.StatusCode != 101 { |
|
|
|
|
return nil, fmt.Errorf("unexpected status code: %v", res.StatusCode) |
|
|
|
|
} |
|
|
|
|
return &bufferedConn{outConn, br}, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
type bufferedConn struct { |
|
|
|
|
net.Conn |
|
|
|
|
reader *bufio.Reader |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (c *bufferedConn) Read(b []byte) (int, error) { |
|
|
|
|
return c.reader.Read(b) |
|
|
|
|
} |
|
|
|
|
|