types: add option to disable taildrop, improve tests (#2955)

This commit is contained in:
Kristoffer Dalby
2025-12-12 11:35:16 +01:00
committed by GitHub
parent 87bd67318b
commit 642073f4b8
6 changed files with 365 additions and 94 deletions

View File

@@ -67,6 +67,7 @@ release.
PeerChangedPatch responses instead of full map updates, reducing bandwidth PeerChangedPatch responses instead of full map updates, reducing bandwidth
and improving performance and improving performance
- Tags can now be tagOwner of other tags [#2930](https://github.com/juanfont/headscale/pull/2930) - Tags can now be tagOwner of other tags [#2930](https://github.com/juanfont/headscale/pull/2930)
- Add `taildrop.enabled` configuration option to enable/disable Taildrop file sharing [#2955](https://github.com/juanfont/headscale/pull/2955)
## 0.27.2 (2025-xx-xx) ## 0.27.2 (2025-xx-xx)

View File

@@ -295,8 +295,7 @@ dns:
# Split DNS (see https://tailscale.com/kb/1054/dns/), # Split DNS (see https://tailscale.com/kb/1054/dns/),
# a map of domains and which DNS server to use for each. # a map of domains and which DNS server to use for each.
split: split: {}
{}
# foo.bar.com: # foo.bar.com:
# - 1.1.1.1 # - 1.1.1.1
# darp.headscale.net: # darp.headscale.net:
@@ -408,6 +407,15 @@ logtail:
# default static port 41641. This option is intended as a workaround for some buggy # default static port 41641. This option is intended as a workaround for some buggy
# firewall devices. See https://tailscale.com/kb/1181/firewalls/ for more information. # firewall devices. See https://tailscale.com/kb/1181/firewalls/ for more information.
randomize_client_port: false randomize_client_port: false
# Taildrop configuration
# Taildrop is the file sharing feature of Tailscale, allowing nodes to send files to each other.
# https://tailscale.com/kb/1106/taildrop/
taildrop:
# Enable or disable Taildrop for all nodes.
# When enabled, nodes can send files to other nodes owned by the same user.
# Tagged devices and cross-user transfers are not permitted by Tailscale clients.
enabled: true
# Advanced performance tuning parameters. # Advanced performance tuning parameters.
# The defaults are carefully chosen and should rarely need adjustment. # The defaults are carefully chosen and should rarely need adjustment.
# Only modify these if you have identified a specific performance issue. # Only modify these if you have identified a specific performance issue.

View File

@@ -205,6 +205,7 @@ func TestTailNode(t *testing.T) {
BaseDomain: tt.baseDomain, BaseDomain: tt.baseDomain,
TailcfgDNSConfig: tt.dnsConfig, TailcfgDNSConfig: tt.dnsConfig,
RandomizeClientPort: false, RandomizeClientPort: false,
Taildrop: types.TaildropConfig{Enabled: true},
} }
_ = primary.SetRoutes(tt.node.ID, tt.node.SubnetRoutes()...) _ = primary.SetRoutes(tt.node.ID, tt.node.SubnetRoutes()...)
@@ -272,7 +273,7 @@ func TestNodeExpiry(t *testing.T) {
func(id types.NodeID) []netip.Prefix { func(id types.NodeID) []netip.Prefix {
return []netip.Prefix{} return []netip.Prefix{}
}, },
&types.Config{}, &types.Config{Taildrop: types.TaildropConfig{Enabled: true}},
) )
if err != nil { if err != nil {
t.Fatalf("nodeExpiry() error = %v", err) t.Fatalf("nodeExpiry() error = %v", err)

View File

@@ -94,6 +94,7 @@ type Config struct {
LogTail LogTailConfig LogTail LogTailConfig
RandomizeClientPort bool RandomizeClientPort bool
Taildrop TaildropConfig
CLI CLIConfig CLI CLIConfig
@@ -211,6 +212,10 @@ type LogTailConfig struct {
Enabled bool Enabled bool
} }
type TaildropConfig struct {
Enabled bool
}
type CLIConfig struct { type CLIConfig struct {
Address string Address string
APIKey string APIKey string
@@ -382,6 +387,7 @@ func LoadConfig(path string, isFile bool) error {
viper.SetDefault("logtail.enabled", false) viper.SetDefault("logtail.enabled", false)
viper.SetDefault("randomize_client_port", false) viper.SetDefault("randomize_client_port", false)
viper.SetDefault("taildrop.enabled", true)
viper.SetDefault("ephemeral_node_inactivity_timeout", "120s") viper.SetDefault("ephemeral_node_inactivity_timeout", "120s")
@@ -1048,6 +1054,9 @@ func LoadServerConfig() (*Config, error) {
LogTail: logTailConfig, LogTail: logTailConfig,
RandomizeClientPort: randomizeClientPort, RandomizeClientPort: randomizeClientPort,
Taildrop: TaildropConfig{
Enabled: viper.GetBool("taildrop.enabled"),
},
Policy: policyConfig(), Policy: policyConfig(),

View File

@@ -1028,7 +1028,6 @@ func (nv NodeView) TailNode(
tsaddr.SortPrefixes(allowedIPs) tsaddr.SortPrefixes(allowedIPs)
capMap := tailcfg.NodeCapMap{ capMap := tailcfg.NodeCapMap{
tailcfg.CapabilityFileSharing: []tailcfg.RawMessage{},
tailcfg.CapabilityAdmin: []tailcfg.RawMessage{}, tailcfg.CapabilityAdmin: []tailcfg.RawMessage{},
tailcfg.CapabilitySSH: []tailcfg.RawMessage{}, tailcfg.CapabilitySSH: []tailcfg.RawMessage{},
} }
@@ -1036,6 +1035,10 @@ func (nv NodeView) TailNode(
capMap[tailcfg.NodeAttrRandomizeClientPort] = []tailcfg.RawMessage{} capMap[tailcfg.NodeAttrRandomizeClientPort] = []tailcfg.RawMessage{}
} }
if cfg.Taildrop.Enabled {
capMap[tailcfg.CapabilityFileSharing] = []tailcfg.RawMessage{}
}
tNode := tailcfg.Node{ tNode := tailcfg.Node{
//nolint:gosec // G115: NodeID values are within int64 range //nolint:gosec // G115: NodeID values are within int64 range
ID: tailcfg.NodeID(nv.ID()), ID: tailcfg.NodeID(nv.ID()),

View File

@@ -14,6 +14,7 @@ import (
"github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/hscontrol/util"
"github.com/juanfont/headscale/integration/hsic" "github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/integrationutil"
"github.com/juanfont/headscale/integration/tsic" "github.com/juanfont/headscale/integration/tsic"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/samber/lo" "github.com/samber/lo"
@@ -366,12 +367,18 @@ func TestPingAllByHostname(t *testing.T) {
// This might mean we approach setup slightly wrong, but for now, ignore // This might mean we approach setup slightly wrong, but for now, ignore
// the linter // the linter
// nolint:tparallel // nolint:tparallel
// TestTaildrop tests the Taildrop file sharing functionality across multiple scenarios:
// 1. Same-user transfers: Nodes owned by the same user can send files to each other
// 2. Cross-user transfers: Nodes owned by different users cannot send files to each other
// 3. Tagged device transfers: Tagged devices cannot send nor receive files
//
// Each user gets len(MustTestVersions) nodes to ensure compatibility across all supported versions.
func TestTaildrop(t *testing.T) { func TestTaildrop(t *testing.T) {
IntegrationSkip(t) IntegrationSkip(t)
spec := ScenarioSpec{ spec := ScenarioSpec{
NodesPerUser: len(MustTestVersions), NodesPerUser: 0, // We'll create nodes manually to control tags
Users: []string{"user1"}, Users: []string{"user1", "user2"},
} }
scenario, err := NewScenario(spec) scenario, err := NewScenario(spec)
@@ -385,16 +392,99 @@ func TestTaildrop(t *testing.T) {
) )
requireNoErrHeadscaleEnv(t, err) requireNoErrHeadscaleEnv(t, err)
headscale, err := scenario.Headscale()
requireNoErrGetHeadscale(t, err)
userMap, err := headscale.MapUsers()
require.NoError(t, err)
networks := scenario.Networks()
require.NotEmpty(t, networks, "scenario should have at least one network")
network := networks[0]
// Create untagged nodes for user1 using all test versions
user1Key, err := scenario.CreatePreAuthKey(userMap["user1"].GetId(), true, false)
require.NoError(t, err)
var user1Clients []TailscaleClient
for i, version := range MustTestVersions {
t.Logf("Creating user1 client %d with version %s", i, version)
client, err := scenario.CreateTailscaleNode(
version,
tsic.WithNetwork(network),
)
require.NoError(t, err)
err = client.Login(headscale.GetEndpoint(), user1Key.GetKey())
require.NoError(t, err)
err = client.WaitForRunning(integrationutil.PeerSyncTimeout())
require.NoError(t, err)
user1Clients = append(user1Clients, client)
scenario.GetOrCreateUser("user1").Clients[client.Hostname()] = client
}
// Create untagged nodes for user2 using all test versions
user2Key, err := scenario.CreatePreAuthKey(userMap["user2"].GetId(), true, false)
require.NoError(t, err)
var user2Clients []TailscaleClient
for i, version := range MustTestVersions {
t.Logf("Creating user2 client %d with version %s", i, version)
client, err := scenario.CreateTailscaleNode(
version,
tsic.WithNetwork(network),
)
require.NoError(t, err)
err = client.Login(headscale.GetEndpoint(), user2Key.GetKey())
require.NoError(t, err)
err = client.WaitForRunning(integrationutil.PeerSyncTimeout())
require.NoError(t, err)
user2Clients = append(user2Clients, client)
scenario.GetOrCreateUser("user2").Clients[client.Hostname()] = client
}
// Create a tagged device (tags-as-identity: tags come from PreAuthKey)
// Use "head" version to test latest behavior
taggedKey, err := scenario.CreatePreAuthKeyWithTags(userMap["user1"].GetId(), true, false, []string{"tag:server"})
require.NoError(t, err)
taggedClient, err := scenario.CreateTailscaleNode(
"head",
tsic.WithNetwork(network),
)
require.NoError(t, err)
err = taggedClient.Login(headscale.GetEndpoint(), taggedKey.GetKey())
require.NoError(t, err)
err = taggedClient.WaitForRunning(integrationutil.PeerSyncTimeout())
require.NoError(t, err)
// Add tagged client to user1 for tracking (though it's tagged, not user-owned)
scenario.GetOrCreateUser("user1").Clients[taggedClient.Hostname()] = taggedClient
allClients, err := scenario.ListTailscaleClients() allClients, err := scenario.ListTailscaleClients()
requireNoErrListClients(t, err) requireNoErrListClients(t, err)
// Expected: len(MustTestVersions) for user1 + len(MustTestVersions) for user2 + 1 tagged
expectedClientCount := len(MustTestVersions)*2 + 1
require.Len(t, allClients, expectedClientCount,
"should have %d clients: %d user1 + %d user2 + 1 tagged",
expectedClientCount, len(MustTestVersions), len(MustTestVersions))
err = scenario.WaitForTailscaleSync() err = scenario.WaitForTailscaleSync()
requireNoErrSync(t, err) requireNoErrSync(t, err)
// This will essentially fetch and cache all the FQDNs // Cache FQDNs
_, err = scenario.ListTailscaleClientsFQDNs() _, err = scenario.ListTailscaleClientsFQDNs()
requireNoErrListFQDN(t, err) requireNoErrListFQDN(t, err)
// Install curl on all clients
for _, client := range allClients { for _, client := range allClients {
if !strings.Contains(client.Hostname(), "head") { if !strings.Contains(client.Hostname(), "head") {
command := []string{"apk", "add", "curl"} command := []string{"apk", "add", "curl"}
@@ -403,110 +493,269 @@ func TestTaildrop(t *testing.T) {
t.Fatalf("failed to install curl on %s, err: %s", client.Hostname(), err) t.Fatalf("failed to install curl on %s, err: %s", client.Hostname(), err)
} }
} }
}
// Helper to get FileTargets for a client.
getFileTargets := func(client TailscaleClient) ([]apitype.FileTarget, error) {
curlCommand := []string{ curlCommand := []string{
"curl", "curl",
"--unix-socket", "--unix-socket",
"/var/run/tailscale/tailscaled.sock", "/var/run/tailscale/tailscaled.sock",
"http://local-tailscaled.sock/localapi/v0/file-targets", "http://local-tailscaled.sock/localapi/v0/file-targets",
} }
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
result, _, err := client.Execute(curlCommand) result, _, err := client.Execute(curlCommand)
assert.NoError(ct, err) if err != nil {
return nil, err
}
var fts []apitype.FileTarget var fts []apitype.FileTarget
err = json.Unmarshal([]byte(result), &fts) if err := json.Unmarshal([]byte(result), &fts); err != nil {
return nil, fmt.Errorf("failed to parse file-targets response: %w (response: %s)", err, result)
}
return fts, nil
}
// Helper to check if a client is in the FileTargets list
isInFileTargets := func(fts []apitype.FileTarget, targetHostname string) bool {
for _, ft := range fts {
if strings.Contains(ft.Node.Name, targetHostname) {
return true
}
}
return false
}
// Test 1: Verify user1 nodes can see each other in FileTargets but not user2 nodes or tagged node
t.Run("FileTargets-user1", func(t *testing.T) {
for _, client := range user1Clients {
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
fts, err := getFileTargets(client)
assert.NoError(ct, err) assert.NoError(ct, err)
if len(fts) != len(allClients)-1 { // Should see the other user1 clients
ftStr := fmt.Sprintf("FileTargets for %s:\n", client.Hostname()) for _, peer := range user1Clients {
for _, ft := range fts { if peer.Hostname() == client.Hostname() {
ftStr += fmt.Sprintf("\t%s\n", ft.Node.Name) continue
} }
assert.Failf(ct, "client %s does not have all its peers as FileTargets", assert.True(ct, isInFileTargets(fts, peer.Hostname()),
"got %d, want: %d\n%s", "user1 client %s should see user1 peer %s in FileTargets", client.Hostname(), peer.Hostname())
len(fts),
len(allClients)-1,
ftStr,
)
} }
// Should NOT see user2 clients
for _, peer := range user2Clients {
assert.False(ct, isInFileTargets(fts, peer.Hostname()),
"user1 client %s should NOT see user2 peer %s in FileTargets", client.Hostname(), peer.Hostname())
}
// Should NOT see tagged client
assert.False(ct, isInFileTargets(fts, taggedClient.Hostname()),
"user1 client %s should NOT see tagged client %s in FileTargets", client.Hostname(), taggedClient.Hostname())
}, 10*time.Second, 1*time.Second) }, 10*time.Second, 1*time.Second)
} }
})
for _, client := range allClients { // Test 2: Verify user2 nodes can see each other in FileTargets but not user1 nodes or tagged node
command := []string{"touch", fmt.Sprintf("/tmp/file_from_%s", client.Hostname())} t.Run("FileTargets-user2", func(t *testing.T) {
for _, client := range user2Clients {
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
fts, err := getFileTargets(client)
assert.NoError(ct, err)
if _, _, err := client.Execute(command); err != nil { // Should see the other user2 clients
t.Fatalf("failed to create taildrop file on %s, err: %s", client.Hostname(), err) for _, peer := range user2Clients {
if peer.Hostname() == client.Hostname() {
continue
}
assert.True(ct, isInFileTargets(fts, peer.Hostname()),
"user2 client %s should see user2 peer %s in FileTargets", client.Hostname(), peer.Hostname())
} }
for _, peer := range allClients { // Should NOT see user1 clients
if client.Hostname() == peer.Hostname() { for _, peer := range user1Clients {
assert.False(ct, isInFileTargets(fts, peer.Hostname()),
"user2 client %s should NOT see user1 peer %s in FileTargets", client.Hostname(), peer.Hostname())
}
// Should NOT see tagged client
assert.False(ct, isInFileTargets(fts, taggedClient.Hostname()),
"user2 client %s should NOT see tagged client %s in FileTargets", client.Hostname(), taggedClient.Hostname())
}, 10*time.Second, 1*time.Second)
}
})
// Test 3: Verify tagged device has no FileTargets (empty list)
t.Run("FileTargets-tagged", func(t *testing.T) {
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
fts, err := getFileTargets(taggedClient)
assert.NoError(ct, err)
assert.Empty(ct, fts, "tagged client %s should have no FileTargets", taggedClient.Hostname())
}, 10*time.Second, 1*time.Second)
})
// Test 4: Same-user file transfer works (user1 -> user1) for all version combinations
t.Run("SameUserTransfer", func(t *testing.T) {
for _, sender := range user1Clients {
// Create file on sender
filename := fmt.Sprintf("file_from_%s", sender.Hostname())
command := []string{"touch", fmt.Sprintf("/tmp/%s", filename)}
_, _, err := sender.Execute(command)
require.NoError(t, err, "failed to create taildrop file on %s", sender.Hostname())
for _, receiver := range user1Clients {
if sender.Hostname() == receiver.Hostname() {
continue continue
} }
// It is safe to ignore this error as we handled it when caching it receiverFQDN, _ := receiver.FQDN()
peerFQDN, _ := peer.FQDN()
t.Run(fmt.Sprintf("%s-%s", client.Hostname(), peer.Hostname()), func(t *testing.T) { t.Run(fmt.Sprintf("%s->%s", sender.Hostname(), receiver.Hostname()), func(t *testing.T) {
command := []string{ sendCommand := []string{
"tailscale", "file", "cp", "tailscale", "file", "cp",
fmt.Sprintf("/tmp/file_from_%s", client.Hostname()), fmt.Sprintf("/tmp/%s", filename),
fmt.Sprintf("%s:", peerFQDN), fmt.Sprintf("%s:", receiverFQDN),
} }
assert.EventuallyWithT(t, func(ct *assert.CollectT) { assert.EventuallyWithT(t, func(ct *assert.CollectT) {
t.Logf( t.Logf("Sending file from %s to %s", sender.Hostname(), receiver.Hostname())
"Sending file from %s to %s\n", _, _, err := sender.Execute(sendCommand)
client.Hostname(),
peer.Hostname(),
)
_, _, err := client.Execute(command)
assert.NoError(ct, err) assert.NoError(ct, err)
}, 10*time.Second, 1*time.Second) }, 10*time.Second, 1*time.Second)
}) })
} }
} }
for _, client := range allClients { // Receive files on all user1 clients
command := []string{ for _, client := range user1Clients {
"tailscale", "file", getCommand := []string{"tailscale", "file", "get", "/tmp/"}
"get", _, _, err := client.Execute(getCommand)
"/tmp/", require.NoError(t, err, "failed to get taildrop file on %s", client.Hostname())
}
if _, _, err := client.Execute(command); err != nil {
t.Fatalf("failed to get taildrop file on %s, err: %s", client.Hostname(), err)
}
for _, peer := range allClients { // Verify files from all other user1 clients exist
for _, peer := range user1Clients {
if client.Hostname() == peer.Hostname() { if client.Hostname() == peer.Hostname() {
continue continue
} }
t.Run(fmt.Sprintf("%s-%s", client.Hostname(), peer.Hostname()), func(t *testing.T) { t.Run(fmt.Sprintf("verify-%s-received-from-%s", client.Hostname(), peer.Hostname()), func(t *testing.T) {
command := []string{ lsCommand := []string{"ls", fmt.Sprintf("/tmp/file_from_%s", peer.Hostname())}
"ls", result, _, err := client.Execute(lsCommand)
fmt.Sprintf("/tmp/file_from_%s", peer.Hostname()), require.NoErrorf(t, err, "failed to ls taildrop file from %s", peer.Hostname())
} assert.Equal(t, fmt.Sprintf("/tmp/file_from_%s\n", peer.Hostname()), result)
log.Printf(
"Checking file in %s from %s\n",
client.Hostname(),
peer.Hostname(),
)
result, _, err := client.Execute(command)
require.NoErrorf(t, err, "failed to execute command to ls taildrop")
log.Printf("Result for %s: %s\n", peer.Hostname(), result)
if fmt.Sprintf("/tmp/file_from_%s\n", peer.Hostname()) != result {
t.Fatalf(
"taildrop result is not correct %s, wanted %s",
result,
fmt.Sprintf("/tmp/file_from_%s\n", peer.Hostname()),
)
}
}) })
} }
} }
})
// Test 5: Cross-user file transfer fails (user1 -> user2)
t.Run("CrossUserTransferBlocked", func(t *testing.T) {
sender := user1Clients[0]
receiver := user2Clients[0]
// Create file on sender
filename := fmt.Sprintf("cross_user_file_from_%s", sender.Hostname())
command := []string{"touch", fmt.Sprintf("/tmp/%s", filename)}
_, _, err := sender.Execute(command)
require.NoError(t, err, "failed to create taildrop file on %s", sender.Hostname())
// Attempt to send file - this should fail
receiverFQDN, _ := receiver.FQDN()
sendCommand := []string{
"tailscale", "file", "cp",
fmt.Sprintf("/tmp/%s", filename),
fmt.Sprintf("%s:", receiverFQDN),
}
t.Logf("Attempting cross-user file send from %s to %s (should fail)", sender.Hostname(), receiver.Hostname())
_, stderr, err := sender.Execute(sendCommand)
// The file transfer should fail because user2 is not in user1's FileTargets
// Either the command errors, or it silently fails (check stderr for error message)
if err != nil {
t.Logf("Cross-user transfer correctly failed with error: %v", err)
} else if strings.Contains(stderr, "not a valid peer") || strings.Contains(stderr, "unknown target") {
t.Logf("Cross-user transfer correctly rejected: %s", stderr)
} else {
// Even if command succeeded, verify the file was NOT received
getCommand := []string{"tailscale", "file", "get", "/tmp/"}
receiver.Execute(getCommand)
lsCommand := []string{"ls", fmt.Sprintf("/tmp/%s", filename)}
_, _, lsErr := receiver.Execute(lsCommand)
assert.Error(t, lsErr, "Cross-user file should NOT have been received")
}
})
// Test 6: Tagged device cannot send files
t.Run("TaggedCannotSend", func(t *testing.T) {
// Create file on tagged client
filename := fmt.Sprintf("file_from_tagged_%s", taggedClient.Hostname())
command := []string{"touch", fmt.Sprintf("/tmp/%s", filename)}
_, _, err := taggedClient.Execute(command)
require.NoError(t, err, "failed to create taildrop file on tagged client")
// Attempt to send to user1 client - should fail because tagged client has no FileTargets
receiver := user1Clients[0]
receiverFQDN, _ := receiver.FQDN()
sendCommand := []string{
"tailscale", "file", "cp",
fmt.Sprintf("/tmp/%s", filename),
fmt.Sprintf("%s:", receiverFQDN),
}
t.Logf("Attempting tagged->user file send from %s to %s (should fail)", taggedClient.Hostname(), receiver.Hostname())
_, stderr, err := taggedClient.Execute(sendCommand)
if err != nil {
t.Logf("Tagged client send correctly failed with error: %v", err)
} else if strings.Contains(stderr, "not a valid peer") || strings.Contains(stderr, "unknown target") || strings.Contains(stderr, "no matches for") {
t.Logf("Tagged client send correctly rejected: %s", stderr)
} else {
// Verify file was NOT received
getCommand := []string{"tailscale", "file", "get", "/tmp/"}
receiver.Execute(getCommand)
lsCommand := []string{"ls", fmt.Sprintf("/tmp/%s", filename)}
_, _, lsErr := receiver.Execute(lsCommand)
assert.Error(t, lsErr, "Tagged client's file should NOT have been received")
}
})
// Test 7: Tagged device cannot receive files (user1 tries to send to tagged)
t.Run("TaggedCannotReceive", func(t *testing.T) {
sender := user1Clients[0]
// Create file on sender
filename := fmt.Sprintf("file_to_tagged_from_%s", sender.Hostname())
command := []string{"touch", fmt.Sprintf("/tmp/%s", filename)}
_, _, err := sender.Execute(command)
require.NoError(t, err, "failed to create taildrop file on %s", sender.Hostname())
// Attempt to send to tagged client - should fail because tagged is not in user1's FileTargets
taggedFQDN, _ := taggedClient.FQDN()
sendCommand := []string{
"tailscale", "file", "cp",
fmt.Sprintf("/tmp/%s", filename),
fmt.Sprintf("%s:", taggedFQDN),
}
t.Logf("Attempting user->tagged file send from %s to %s (should fail)", sender.Hostname(), taggedClient.Hostname())
_, stderr, err := sender.Execute(sendCommand)
if err != nil {
t.Logf("Send to tagged client correctly failed with error: %v", err)
} else if strings.Contains(stderr, "not a valid peer") || strings.Contains(stderr, "unknown target") || strings.Contains(stderr, "no matches for") {
t.Logf("Send to tagged client correctly rejected: %s", stderr)
} else {
// Verify file was NOT received by tagged client
getCommand := []string{"tailscale", "file", "get", "/tmp/"}
taggedClient.Execute(getCommand)
lsCommand := []string{"ls", fmt.Sprintf("/tmp/%s", filename)}
_, _, lsErr := taggedClient.Execute(lsCommand)
assert.Error(t, lsErr, "File to tagged client should NOT have been received")
}
})
} }
func TestUpdateHostnameFromClient(t *testing.T) { func TestUpdateHostnameFromClient(t *testing.T) {