mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
tsnet: generalize loopback listener to include SOCKS5
Some languages do not give you any useful access to the sockets underlying their networking packages. E.g. java.net.http.HttpClient provides no official access to its dialing logic. ...but everyone supports proxies. So add a SOCKS5 proxy on the listener we are already running. (The function being revamped is very new, I only added it in the last week and it wasn't part of any release, so I believe it is fine to redo its function signature.) Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
This commit is contained in:
parent
df2561f6a2
commit
387b68fe11
@ -38,6 +38,8 @@
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/logtail/filch"
|
||||
"tailscale.com/net/memnet"
|
||||
"tailscale.com/net/proxymux"
|
||||
"tailscale.com/net/socks5"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/types/logger"
|
||||
@ -91,22 +93,23 @@ type Server struct {
|
||||
// If empty, the Tailscale default is used.
|
||||
ControlURL string
|
||||
|
||||
initOnce sync.Once
|
||||
initErr error
|
||||
lb *ipnlocal.LocalBackend
|
||||
netstack *netstack.Impl
|
||||
linkMon *monitor.Mon
|
||||
rootPath string // the state directory
|
||||
hostname string
|
||||
shutdownCtx context.Context
|
||||
shutdownCancel context.CancelFunc
|
||||
localAPICred string // basic auth password for localAPITCPListener
|
||||
localAPITCPListener net.Listener // optional loopback, restricted to PID
|
||||
localAPIListener net.Listener // in-memory, used by localClient
|
||||
localClient *tailscale.LocalClient // in-memory
|
||||
logbuffer *filch.Filch
|
||||
logtail *logtail.Logger
|
||||
logid string
|
||||
initOnce sync.Once
|
||||
initErr error
|
||||
lb *ipnlocal.LocalBackend
|
||||
netstack *netstack.Impl
|
||||
linkMon *monitor.Mon
|
||||
rootPath string // the state directory
|
||||
hostname string
|
||||
shutdownCtx context.Context
|
||||
shutdownCancel context.CancelFunc
|
||||
proxyCred string // SOCKS5 proxy auth for loopbackListener
|
||||
localAPICred string // basic auth password for loopbackListener
|
||||
loopbackListener net.Listener // optional loopback for localapi and proxies
|
||||
localAPIListener net.Listener // in-memory, used by localClient
|
||||
localClient *tailscale.LocalClient // in-memory
|
||||
logbuffer *filch.Filch
|
||||
logtail *logtail.Logger
|
||||
logid string
|
||||
|
||||
mu sync.Mutex
|
||||
listeners map[listenKey]*listener
|
||||
@ -145,34 +148,49 @@ func (s *Server) LocalClient() (*tailscale.LocalClient, error) {
|
||||
return s.localClient, nil
|
||||
}
|
||||
|
||||
// LoopbackLocalAPI returns a loopback ip:port listening for the "LocalAPI".
|
||||
// Loopback starts a routing server on a loopback address.
|
||||
//
|
||||
// The server has multiple functions.
|
||||
//
|
||||
// It can be used as a SOCKS5 proxy onto the tailnet.
|
||||
// Authentication is required with the username "tsnet" and
|
||||
// the value of proxyCred used as the password.
|
||||
//
|
||||
// The HTTP server also serves out the "LocalAPI" on /localapi.
|
||||
// As the LocalAPI is powerful, access to endpoints requires BOTH passing a
|
||||
// "Sec-Tailscale: localapi" HTTP header and passing cred as a basic auth.
|
||||
//
|
||||
// It will start the server and the local client listener if they have not
|
||||
// been started yet.
|
||||
// "Sec-Tailscale: localapi" HTTP header and passing localAPICred as basic auth.
|
||||
//
|
||||
// If you only need to use the LocalAPI from Go, then prefer LocalClient
|
||||
// as it does not require communication via TCP.
|
||||
func (s *Server) LoopbackLocalAPI() (addr string, cred string, err error) {
|
||||
func (s *Server) Loopback() (addr string, proxyCred, localAPICred string, err error) {
|
||||
if err := s.Start(); err != nil {
|
||||
return "", "", err
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
if s.localAPITCPListener == nil {
|
||||
if s.loopbackListener == nil {
|
||||
var proxyCred [16]byte
|
||||
if _, err := crand.Read(proxyCred[:]); err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
s.proxyCred = hex.EncodeToString(proxyCred[:])
|
||||
|
||||
var cred [16]byte
|
||||
if _, err := crand.Read(cred[:]); err != nil {
|
||||
return "", "", err
|
||||
return "", "", "", err
|
||||
}
|
||||
s.localAPICred = hex.EncodeToString(cred[:])
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return "", "", "", err
|
||||
}
|
||||
s.localAPITCPListener = ln
|
||||
s.loopbackListener = ln
|
||||
|
||||
socksLn, httpLn := proxymux.SplitSOCKSAndHTTP(ln)
|
||||
|
||||
// TODO: add HTTP proxy support. Probably requires factoring
|
||||
// out the CONNECT code from tailscaled/proxy.go that uses
|
||||
// httputil.ReverseProxy and adding auth support.
|
||||
go func() {
|
||||
lah := localapi.NewHandler(s.lb, s.logf, s.logid)
|
||||
lah.PermitWrite = true
|
||||
@ -180,13 +198,23 @@ func (s *Server) LoopbackLocalAPI() (addr string, cred string, err error) {
|
||||
lah.RequiredPassword = s.localAPICred
|
||||
h := &localSecHandler{h: lah, cred: s.localAPICred}
|
||||
|
||||
if err := http.Serve(s.localAPITCPListener, h); err != nil {
|
||||
if err := http.Serve(httpLn, h); err != nil {
|
||||
s.logf("localapi tcp serve error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
s5s := &socks5.Server{
|
||||
Logf: logger.WithPrefix(s.logf, "socks5: "),
|
||||
Dialer: s.dialer.UserDial,
|
||||
Username: "tsnet",
|
||||
Password: s.proxyCred,
|
||||
}
|
||||
go func() {
|
||||
s.logf("SOCKS5 server exited: %v", s5s.Serve(socksLn))
|
||||
}()
|
||||
}
|
||||
|
||||
return s.localAPITCPListener.Addr().String(), s.localAPICred, nil
|
||||
return s.loopbackListener.Addr().String(), s.proxyCred, s.localAPICred, nil
|
||||
}
|
||||
|
||||
type localSecHandler struct {
|
||||
@ -301,8 +329,8 @@ func (s *Server) Close() error {
|
||||
if s.localAPIListener != nil {
|
||||
s.localAPIListener.Close()
|
||||
}
|
||||
if s.localAPITCPListener != nil {
|
||||
s.localAPITCPListener.Close()
|
||||
if s.loopbackListener != nil {
|
||||
s.loopbackListener.Close()
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
|
@ -11,11 +11,13 @@
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/proxy"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/tailcfg"
|
||||
@ -99,48 +101,37 @@ func startControl(t *testing.T) (controlURL string) {
|
||||
return controlURL
|
||||
}
|
||||
|
||||
func TestConn(t *testing.T) {
|
||||
controlURL := startControl(t)
|
||||
func startServer(t *testing.T, ctx context.Context, controlURL, hostname string) (*Server, netip.Addr) {
|
||||
t.Helper()
|
||||
|
||||
tmp := t.TempDir()
|
||||
tmps1 := filepath.Join(tmp, "s1")
|
||||
os.MkdirAll(tmps1, 0755)
|
||||
s1 := &Server{
|
||||
Dir: tmps1,
|
||||
tmp := filepath.Join(t.TempDir(), hostname)
|
||||
os.MkdirAll(tmp, 0755)
|
||||
s := &Server{
|
||||
Dir: tmp,
|
||||
ControlURL: controlURL,
|
||||
Hostname: "s1",
|
||||
Hostname: hostname,
|
||||
Store: new(mem.Store),
|
||||
Ephemeral: true,
|
||||
}
|
||||
defer s1.Close()
|
||||
|
||||
tmps2 := filepath.Join(tmp, "s1")
|
||||
os.MkdirAll(tmps2, 0755)
|
||||
s2 := &Server{
|
||||
Dir: tmps2,
|
||||
ControlURL: controlURL,
|
||||
Hostname: "s2",
|
||||
Store: new(mem.Store),
|
||||
Ephemeral: true,
|
||||
}
|
||||
defer s2.Close()
|
||||
|
||||
if !*verboseNodes {
|
||||
s1.Logf = logger.Discard
|
||||
s2.Logf = logger.Discard
|
||||
s.Logf = logger.Discard
|
||||
}
|
||||
t.Cleanup(func() { s.Close() })
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
s1status, err := s1.Up(ctx)
|
||||
status, err := s.Up(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s1ip := s1status.TailscaleIPs[0]
|
||||
if _, err := s2.Up(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return s, status.TailscaleIPs[0]
|
||||
}
|
||||
|
||||
func TestConn(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
controlURL := startControl(t)
|
||||
s1, s1ip := startServer(t, ctx, controlURL, "s1")
|
||||
s2, _ := startServer(t, ctx, controlURL, "s2")
|
||||
|
||||
lc2, err := s2.LocalClient()
|
||||
if err != nil {
|
||||
@ -187,31 +178,19 @@ func TestConn(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoopbackLocalAPI(t *testing.T) {
|
||||
controlURL := startControl(t)
|
||||
|
||||
tmp := t.TempDir()
|
||||
tmps1 := filepath.Join(tmp, "s1")
|
||||
os.MkdirAll(tmps1, 0755)
|
||||
s1 := &Server{
|
||||
Dir: tmps1,
|
||||
ControlURL: controlURL,
|
||||
Hostname: "s1",
|
||||
Store: new(mem.Store),
|
||||
Ephemeral: true,
|
||||
}
|
||||
defer s1.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if _, err := s1.Up(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
controlURL := startControl(t)
|
||||
s1, _ := startServer(t, ctx, controlURL, "s1")
|
||||
|
||||
addr, cred, err := s1.LoopbackLocalAPI()
|
||||
addr, proxyCred, localAPICred, err := s1.Loopback()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if proxyCred == localAPICred {
|
||||
t.Fatal("proxy password matches local API password, they should be different")
|
||||
}
|
||||
|
||||
url := "http://" + addr + "/localapi/v0/status"
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
@ -245,7 +224,7 @@ func TestLoopbackLocalAPI(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.SetBasicAuth("", cred)
|
||||
req.SetBasicAuth("", localAPICred)
|
||||
res, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@ -260,7 +239,7 @@ func TestLoopbackLocalAPI(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Sec-Tailscale", "localapi")
|
||||
req.SetBasicAuth("", cred)
|
||||
req.SetBasicAuth("", localAPICred)
|
||||
res, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@ -270,3 +249,53 @@ func TestLoopbackLocalAPI(t *testing.T) {
|
||||
t.Errorf("GET /status returned %d, want 200", res.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoopbackSOCKS5(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
controlURL := startControl(t)
|
||||
s1, s1ip := startServer(t, ctx, controlURL, "s1")
|
||||
s2, _ := startServer(t, ctx, controlURL, "s2")
|
||||
|
||||
addr, proxyCred, _, err := s2.Loopback()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ln, err := s1.Listen("tcp", ":8081")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
auth := &proxy.Auth{User: "tsnet", Password: proxyCred}
|
||||
socksDialer, err := proxy.SOCKS5("tcp", addr, auth, proxy.Direct)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
w, err := socksDialer.Dial("tcp", fmt.Sprintf("%s:8081", s1ip))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := "hello"
|
||||
if _, err := io.WriteString(w, want); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got := make([]byte, len(want))
|
||||
if _, err := io.ReadAtLeast(r, got, len(got)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("got: %q", got)
|
||||
if string(got) != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user