From b83894abd668f491fc6ce6e504f812a7d8a203b0 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Fri, 24 Sep 2021 09:49:29 +0200 Subject: [PATCH 01/10] Add support for taildrop (#118) --- machine.go | 1 + 1 file changed, 1 insertion(+) diff --git a/machine.go b/machine.go index 3e9786a2..1d4939c1 100644 --- a/machine.go +++ b/machine.go @@ -167,6 +167,7 @@ func (m Machine) toNode(includeRoutes bool) (*tailcfg.Node, error) { KeepAlive: true, MachineAuthorized: m.Registered, + Capabilities: []string{tailcfg.CapabilityFileSharing}, } return &n, nil } From cab5641d9598ec1ebf3217bbb0e6274ff6c1e74b Mon Sep 17 00:00:00 2001 From: Juan Font Date: Fri, 24 Sep 2021 09:50:01 +0200 Subject: [PATCH 02/10] Update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a57900a0..1047ff3a 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Headscale implements this coordination server. - [x] Node registration via pre-auth keys (including reusable keys, and ephemeral node support) - [X] JSON-formatted output - [X] ACLs +- [X] Taildrop (File Sharing) - [X] Support for alternative IP ranges in the tailnets (default Tailscale's 100.64.0.0/10) - [X] DNS (passing DNS servers to nodes) - [X] Share nodes between ~~users~~ namespaces From a36328dbfc3c36442837fe8e7e5ccb36702337ff Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sat, 25 Sep 2021 13:12:44 +0200 Subject: [PATCH 03/10] Added integration tests --- integration_test.go | 67 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/integration_test.go b/integration_test.go index 4b28b148..8e58e357 100644 --- a/integration_test.go +++ b/integration_test.go @@ -529,6 +529,73 @@ func (s *IntegrationTestSuite) TestSharedNodes() { // } } +func (s *IntegrationTestSuite) TestTailDrop() { + for _, scales := range s.namespaces { + ips, err := getIPs(scales.tailscales) + assert.Nil(s.T(), err) + + for hostname, tailscale := range scales.tailscales { + command := []string{"touch", fmt.Sprintf("/tmp/file_from_%s", hostname)} + _, err := executeCommand( + &tailscale, + command, + ) + assert.Nil(s.T(), err) + for peername, ip := range ips { + s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { + // We currently cant send files to so skip that + if peername != hostname { + command := []string{ + "tailscale", + "file", + "cp", + fmt.Sprintf("/tmp/file_from_%s", hostname), + fmt.Sprintf("%s:", ip), + } + fmt.Printf("Sending file from %s (%s) to %s (%s)\n", hostname, ips[hostname], peername, ip) + _, err := executeCommand( + &tailscale, + command, + ) + assert.Nil(t, err) + } + }) + } + } + + for hostname, tailscale := range scales.tailscales { + command := []string{ + "tailscale", "file", + "get", + ".", + } + _, err := executeCommand( + &tailscale, + command, + ) + assert.Nil(s.T(), err) + for peername, ip := range ips { + s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { + if peername != hostname { + command := []string{ + "ls", + fmt.Sprintf("/tmp/file_from_%s", peername), + } + fmt.Printf("Checking file in %s (%s) from %s (%s)\n", hostname, ips[hostname], peername, ip) + result, err := executeCommand( + &tailscale, + command, + ) + assert.Nil(t, err) + fmt.Printf("Result for %s: %s\n", peername, result) + assert.Equal(t, result, fmt.Sprintf("/tmp/file_from_%s\n", peername)) + } + }) + } + } + } +} + func getIPs(tailscales map[string]dockertest.Resource) (map[string]netaddr.IP, error) { ips := make(map[string]netaddr.IP) for hostname, tailscale := range tailscales { From 05a5f21c3d799f23c277b174a94f4e7ac7f197e8 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sun, 26 Sep 2021 12:22:59 +0200 Subject: [PATCH 04/10] Use curl to uploaded the file --- integration_test.go | 63 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/integration_test.go b/integration_test.go index 8e58e357..eb2ce209 100644 --- a/integration_test.go +++ b/integration_test.go @@ -533,6 +533,8 @@ func (s *IntegrationTestSuite) TestTailDrop() { for _, scales := range s.namespaces { ips, err := getIPs(scales.tailscales) assert.Nil(s.T(), err) + apiURLs, err := getAPIURLs(scales.tailscales) + assert.Nil(s.T(), err) for hostname, tailscale := range scales.tailscales { command := []string{"touch", fmt.Sprintf("/tmp/file_from_%s", hostname)} @@ -545,12 +547,19 @@ func (s *IntegrationTestSuite) TestTailDrop() { s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { // We currently cant send files to so skip that if peername != hostname { + + // Under normal circumstances, we should be able to send a file + // using `tailscale file cp` - but not in userspace networking mode + // So curl! + peerAPI, ok := apiURLs[ip] command := []string{ - "tailscale", - "file", - "cp", + "ALL_PROXY=socks5://localhost:1055/", + "curl", + "-X", + "PUT", + "--upload-file", fmt.Sprintf("/tmp/file_from_%s", hostname), - fmt.Sprintf("%s:", ip), + fmt.Sprintf("%s/v0/put/file_from_%s", peerAPI, hostname), } fmt.Printf("Sending file from %s (%s) to %s (%s)\n", hostname, ips[hostname], peername, ip) _, err := executeCommand( @@ -618,3 +627,49 @@ func getIPs(tailscales map[string]dockertest.Resource) (map[string]netaddr.IP, e } return ips, nil } + +func getAPIURLs(tailscales map[string]dockertest.Resource) (map[netaddr.IP]string, error) { + fts := make(map[netaddr.IP]string) + for hostname, tailscale := range tailscales { + command := []string{"tailscale", "ip"} + result, err := executeCommand( + &tailscale, + command, + ) + if err != nil { + return nil, err + } + ip, err := netaddr.ParseIP(strings.TrimSuffix(result, "\n")) + if err != nil { + return nil, err + } + + command := []string{ + "curl", + "--unix-socket", + "/run/tailscale/tailscaled.sock", + "http://localhost/localapi/v0/file-targets", + } + result, err := executeCommand( + &tailscale, + command, + ) + if err != nil { + return nil, err + } + var pft []apitype.FileTarget + if err := json.Unmarshal(body, &ft); err != nil { + return nil, fmt.Errorf("invalid JSON: %w", err) + } + for _, ft := range pft { + n := ft.Node + for _, a := range n.Addresses { + if a.IP() == ip { + fts[ip] = ft.PeerAPIURL + break + } + } + } + return fts, nil + } +} From 83ead36fce9fa11f5417daec7e6269bf7698076e Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 26 Sep 2021 14:22:11 +0200 Subject: [PATCH 05/10] Integration tests working for taildrop --- integration_test.go | 44 ++++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/integration_test.go b/integration_test.go index eb2ce209..472e4798 100644 --- a/integration_test.go +++ b/integration_test.go @@ -21,6 +21,7 @@ import ( "github.com/ory/dockertest/v3/docker" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + "tailscale.com/client/tailscale/apitype" "tailscale.com/ipn/ipnstate" "inet.af/netaddr" @@ -93,13 +94,14 @@ func TestIntegrationTestSuite(t *testing.T) { } } -func executeCommand(resource *dockertest.Resource, cmd []string) (string, error) { +func executeCommand(resource *dockertest.Resource, cmd []string, env []string) (string, error) { var stdout bytes.Buffer var stderr bytes.Buffer exitCode, err := resource.Exec( cmd, dockertest.ExecOptions{ + Env: env, StdOut: &stdout, StdErr: &stderr, }, @@ -277,6 +279,7 @@ func (s *IntegrationTestSuite) SetupSuite() { result, err := executeCommand( &headscale, []string{"headscale", "namespaces", "create", namespace}, + []string{}, ) assert.Nil(s.T(), err) fmt.Println("headscale create namespace result: ", result) @@ -285,6 +288,7 @@ func (s *IntegrationTestSuite) SetupSuite() { authKey, err := executeCommand( &headscale, []string{"headscale", "--namespace", namespace, "preauthkeys", "create", "--reusable", "--expiration", "24h"}, + []string{}, ) assert.Nil(s.T(), err) @@ -299,6 +303,7 @@ func (s *IntegrationTestSuite) SetupSuite() { result, err := executeCommand( &tailscale, command, + []string{}, ) fmt.Println("tailscale result: ", result) assert.Nil(s.T(), err) @@ -324,6 +329,7 @@ func (s *IntegrationTestSuite) TestListNodes() { result, err := executeCommand( &headscale, []string{"headscale", "--namespace", namespace, "nodes", "list"}, + []string{}, ) assert.Nil(s.T(), err) @@ -374,6 +380,7 @@ func (s *IntegrationTestSuite) TestStatus() { result, err := executeCommand( &tailscale, command, + []string{}, ) assert.Nil(t, err) @@ -435,6 +442,7 @@ func (s *IntegrationTestSuite) TestPingAllPeers() { result, err := executeCommand( &tailscale, command, + []string{}, ) assert.Nil(t, err) fmt.Printf("Result for %s: %s\n", hostname, result) @@ -453,6 +461,7 @@ func (s *IntegrationTestSuite) TestSharedNodes() { result, err := executeCommand( &headscale, []string{"headscale", "nodes", "list", "-o", "json", "--namespace", "shared"}, + []string{}, ) assert.Nil(s.T(), err) @@ -465,6 +474,7 @@ func (s *IntegrationTestSuite) TestSharedNodes() { result, err := executeCommand( &headscale, []string{"headscale", "nodes", "share", "--namespace", "shared", fmt.Sprint(machine.ID), "main"}, + []string{}, ) assert.Nil(s.T(), err) @@ -474,6 +484,7 @@ func (s *IntegrationTestSuite) TestSharedNodes() { result, err = executeCommand( &headscale, []string{"headscale", "nodes", "list", "--namespace", "main"}, + []string{}, ) assert.Nil(s.T(), err) fmt.Println("Nodelist after sharing", result) @@ -541,6 +552,7 @@ func (s *IntegrationTestSuite) TestTailDrop() { _, err := executeCommand( &tailscale, command, + []string{}, ) assert.Nil(s.T(), err) for peername, ip := range ips { @@ -552,8 +564,8 @@ func (s *IntegrationTestSuite) TestTailDrop() { // using `tailscale file cp` - but not in userspace networking mode // So curl! peerAPI, ok := apiURLs[ip] + assert.True(t, ok) command := []string{ - "ALL_PROXY=socks5://localhost:1055/", "curl", "-X", "PUT", @@ -565,6 +577,7 @@ func (s *IntegrationTestSuite) TestTailDrop() { _, err := executeCommand( &tailscale, command, + []string{"ALL_PROXY=socks5://localhost:1055/"}, ) assert.Nil(t, err) } @@ -576,11 +589,12 @@ func (s *IntegrationTestSuite) TestTailDrop() { command := []string{ "tailscale", "file", "get", - ".", + "/tmp/", } _, err := executeCommand( &tailscale, command, + []string{}, ) assert.Nil(s.T(), err) for peername, ip := range ips { @@ -594,6 +608,7 @@ func (s *IntegrationTestSuite) TestTailDrop() { result, err := executeCommand( &tailscale, command, + []string{}, ) assert.Nil(t, err) fmt.Printf("Result for %s: %s\n", peername, result) @@ -613,6 +628,7 @@ func getIPs(tailscales map[string]dockertest.Resource) (map[string]netaddr.IP, e result, err := executeCommand( &tailscale, command, + []string{}, ) if err != nil { return nil, err @@ -630,11 +646,12 @@ func getIPs(tailscales map[string]dockertest.Resource) (map[string]netaddr.IP, e func getAPIURLs(tailscales map[string]dockertest.Resource) (map[netaddr.IP]string, error) { fts := make(map[netaddr.IP]string) - for hostname, tailscale := range tailscales { + for _, tailscale := range tailscales { command := []string{"tailscale", "ip"} result, err := executeCommand( &tailscale, command, + []string{}, ) if err != nil { return nil, err @@ -644,32 +661,35 @@ func getAPIURLs(tailscales map[string]dockertest.Resource) (map[netaddr.IP]strin return nil, err } - command := []string{ + command = []string{ "curl", "--unix-socket", "/run/tailscale/tailscaled.sock", "http://localhost/localapi/v0/file-targets", } - result, err := executeCommand( + result, err = executeCommand( &tailscale, command, + []string{}, ) if err != nil { return nil, err } + fmt.Println(ip) + fmt.Println(result) var pft []apitype.FileTarget - if err := json.Unmarshal(body, &ft); err != nil { + if err := json.Unmarshal([]byte(result), &pft); err != nil { return nil, fmt.Errorf("invalid JSON: %w", err) } for _, ft := range pft { n := ft.Node - for _, a := range n.Addresses { - if a.IP() == ip { - fts[ip] = ft.PeerAPIURL - break + for _, a := range n.Addresses { // just add all the addresses + if _, ok := fts[a.IP()]; !ok { + fts[a.IP()] = ft.PeerAPIURL } } } - return fts, nil } + fmt.Printf("API URLs: %+v\n", fts) + return fts, nil } From ada40960bddebd021c1d8dbbe8973bf0a4861133 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 26 Sep 2021 14:33:01 +0200 Subject: [PATCH 06/10] Removed unnecesary prints --- integration_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/integration_test.go b/integration_test.go index 472e4798..71d0fc9d 100644 --- a/integration_test.go +++ b/integration_test.go @@ -675,8 +675,7 @@ func getAPIURLs(tailscales map[string]dockertest.Resource) (map[netaddr.IP]strin if err != nil { return nil, err } - fmt.Println(ip) - fmt.Println(result) + var pft []apitype.FileTarget if err := json.Unmarshal([]byte(result), &pft); err != nil { return nil, fmt.Errorf("invalid JSON: %w", err) @@ -690,6 +689,5 @@ func getAPIURLs(tailscales map[string]dockertest.Resource) (map[netaddr.IP]strin } } } - fmt.Printf("API URLs: %+v\n", fts) return fts, nil } From eb87fc92156c86f9d5c090784d7f2b0852fb0c28 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 26 Sep 2021 15:17:27 +0200 Subject: [PATCH 07/10] Fixed getAPIURLs method --- integration_test.go | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/integration_test.go b/integration_test.go index 71d0fc9d..1f4a9b33 100644 --- a/integration_test.go +++ b/integration_test.go @@ -647,27 +647,13 @@ func getIPs(tailscales map[string]dockertest.Resource) (map[string]netaddr.IP, e func getAPIURLs(tailscales map[string]dockertest.Resource) (map[netaddr.IP]string, error) { fts := make(map[netaddr.IP]string) for _, tailscale := range tailscales { - command := []string{"tailscale", "ip"} - result, err := executeCommand( - &tailscale, - command, - []string{}, - ) - if err != nil { - return nil, err - } - ip, err := netaddr.ParseIP(strings.TrimSuffix(result, "\n")) - if err != nil { - return nil, err - } - - command = []string{ + command := []string{ "curl", "--unix-socket", "/run/tailscale/tailscaled.sock", "http://localhost/localapi/v0/file-targets", } - result, err = executeCommand( + result, err := executeCommand( &tailscale, command, []string{}, From 7d0da8b578442b6cbd4c778131bded289aa2c289 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 26 Sep 2021 17:38:51 +0200 Subject: [PATCH 08/10] Added retries --- integration_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/integration_test.go b/integration_test.go index 1f4a9b33..9f50c7ed 100644 --- a/integration_test.go +++ b/integration_test.go @@ -567,6 +567,8 @@ func (s *IntegrationTestSuite) TestTailDrop() { assert.True(t, ok) command := []string{ "curl", + "--retry", + "10", "-X", "PUT", "--upload-file", From 5626f598ce6b540c903e89a422aabefa44b6581b Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 26 Sep 2021 18:59:23 +0200 Subject: [PATCH 09/10] Do several attempts to send the file --- integration_test.go | 52 ++++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/integration_test.go b/integration_test.go index 9f50c7ed..c4b8300c 100644 --- a/integration_test.go +++ b/integration_test.go @@ -565,22 +565,44 @@ func (s *IntegrationTestSuite) TestTailDrop() { // So curl! peerAPI, ok := apiURLs[ip] assert.True(t, ok) - command := []string{ - "curl", - "--retry", - "10", - "-X", - "PUT", - "--upload-file", - fmt.Sprintf("/tmp/file_from_%s", hostname), - fmt.Sprintf("%s/v0/put/file_from_%s", peerAPI, hostname), + + // We still have some issues with the test infrastructure, so + // lets run curl multiple times until it works. + + attempts := 0 + var err error + for { + command := []string{ + "curl", + "--retry-connrefused", + "--retry-delay", + "30", + "--retry", + "10", + "--connect-timeout", + "60", + "-X", + "PUT", + "--upload-file", + fmt.Sprintf("/tmp/file_from_%s", hostname), + fmt.Sprintf("%s/v0/put/file_from_%s", peerAPI, hostname), + } + fmt.Printf("Sending file from %s (%s) to %s (%s)\n", hostname, ips[hostname], peername, ip) + _, err = executeCommand( + &tailscale, + command, + []string{"ALL_PROXY=socks5://localhost:1055/"}, + ) + if err == nil { + break + } else { + time.Sleep(10 * time.Second) + attempts++ + if attempts > 10 { + break + } + } } - fmt.Printf("Sending file from %s (%s) to %s (%s)\n", hostname, ips[hostname], peername, ip) - _, err := executeCommand( - &tailscale, - command, - []string{"ALL_PROXY=socks5://localhost:1055/"}, - ) assert.Nil(t, err) } }) From c801a8c553af055ec61b4978ce8f6b8130fa8aff Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 26 Sep 2021 20:23:15 +0200 Subject: [PATCH 10/10] Improve comments on taildrop tests --- integration_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/integration_test.go b/integration_test.go index c4b8300c..f62ca1da 100644 --- a/integration_test.go +++ b/integration_test.go @@ -557,7 +557,6 @@ func (s *IntegrationTestSuite) TestTailDrop() { assert.Nil(s.T(), err) for peername, ip := range ips { s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { - // We currently cant send files to so skip that if peername != hostname { // Under normal circumstances, we should be able to send a file @@ -566,9 +565,8 @@ func (s *IntegrationTestSuite) TestTailDrop() { peerAPI, ok := apiURLs[ip] assert.True(t, ok) - // We still have some issues with the test infrastructure, so + // TODO(juanfont): We still have some issues with the test infrastructure, so // lets run curl multiple times until it works. - attempts := 0 var err error for {