Add ACL test for limiting a single port. (#1258)

This commit is contained in:
Kristoffer Dalby 2023-03-20 08:52:52 +01:00 committed by GitHub
parent d12f247490
commit e38efd3cfa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 280 additions and 5 deletions

View file

@ -0,0 +1,57 @@
# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go
# To regenerate, run "go generate" in cmd/gh-action-integration-generator/
name: Integration Test v2 - TestACLAllowUser80Dst
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v18
if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
nix develop --command -- docker run \
--tty --rm \
--volume ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
--volume $PWD:$PWD -w $PWD/integration \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume $PWD/control_logs:/tmp/control \
golang:1 \
go test ./... \
-tags ts2019 \
-failfast \
-timeout 120m \
-parallel 1 \
-run "^TestACLAllowUser80Dst$"
- uses: actions/upload-artifact@v3
if: always() && steps.changed-files.outputs.any_changed == 'true'
with:
name: logs
path: "control_logs/*.log"

View file

@ -1,6 +1,7 @@
package integration package integration
import ( import (
"fmt"
"testing" "testing"
"github.com/juanfont/headscale" "github.com/juanfont/headscale"
@ -9,6 +10,44 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
const numberOfTestClients = 2
func aclScenario(t *testing.T, policy headscale.ACLPolicy) *Scenario {
t.Helper()
scenario, err := NewScenario()
assert.NoError(t, err)
spec := map[string]int{
"user1": numberOfTestClients,
"user2": numberOfTestClients,
}
err = scenario.CreateHeadscaleEnv(spec,
[]tsic.Option{
tsic.WithDockerEntrypoint([]string{
"/bin/bash",
"-c",
"/bin/sleep 3 ; update-ca-certificates ; python3 -m http.server 80 & tailscaled --tun=tsdev",
}),
tsic.WithDockerWorkdir("/"),
},
hsic.WithACLPolicy(&policy),
hsic.WithTestName("acldenyallping"),
)
assert.NoError(t, err)
// allClients, err := scenario.ListTailscaleClients()
// assert.NoError(t, err)
err = scenario.WaitForTailscaleSync()
assert.NoError(t, err)
_, err = scenario.ListTailscaleClientsFQDNs()
assert.NoError(t, err)
return scenario
}
// This tests a different ACL mechanism, if a host _cannot_ connect // This tests a different ACL mechanism, if a host _cannot_ connect
// to another node at all based on ACL, it should just not be part // to another node at all based on ACL, it should just not be part
// of the NetMap sent to the host. This is slightly different than // of the NetMap sent to the host. This is slightly different than
@ -179,3 +218,63 @@ func TestACLHostsInNetMapTable(t *testing.T) {
}) })
} }
} }
// Test to confirm that we can use user:80 from one user
// This should make the node appear in the peer list, but
// disallow ping.
// This ACL will not allow user1 access its own machines.
// Reported: https://github.com/juanfont/headscale/issues/699
func TestACLAllowUser80Dst(t *testing.T) {
IntegrationSkip(t)
scenario := aclScenario(t,
headscale.ACLPolicy{
ACLs: []headscale.ACL{
{
Action: "accept",
Sources: []string{"user1"},
Destinations: []string{"user2:80"},
},
},
},
)
user1Clients, err := scenario.ListTailscaleClients("user1")
assert.NoError(t, err)
user2Clients, err := scenario.ListTailscaleClients("user2")
assert.NoError(t, err)
// Test that user1 can visit all user2
for _, client := range user1Clients {
for _, peer := range user2Clients {
fqdn, err := peer.FQDN()
assert.NoError(t, err)
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
t.Logf("url from %s to %s", client.Hostname(), url)
result, err := client.Curl(url)
assert.Len(t, result, 13)
assert.NoError(t, err)
}
}
// Test that user2 _cannot_ visit user1
for _, client := range user2Clients {
for _, peer := range user1Clients {
fqdn, err := peer.FQDN()
assert.NoError(t, err)
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
t.Logf("url from %s to %s", client.Hostname(), url)
result, err := client.Curl(url)
assert.Empty(t, result)
assert.Error(t, err)
}
}
err = scenario.Shutdown()
assert.NoError(t, err)
}

View file

@ -24,5 +24,6 @@ type TailscaleClient interface {
WaitForLogout() error WaitForLogout() error
WaitForPeers(expected int) error WaitForPeers(expected int) error
Ping(hostnameOrIP string, opts ...tsic.PingOption) error Ping(hostnameOrIP string, opts ...tsic.PingOption) error
Curl(url string, opts ...tsic.CurlOption) (string, error)
ID() string ID() string
} }

View file

@ -55,6 +55,8 @@ type TailscaleInContainer struct {
headscaleHostname string headscaleHostname string
withSSH bool withSSH bool
withTags []string withTags []string
withEntrypoint []string
workdir string
} }
// Option represent optional settings that can be given to a // Option represent optional settings that can be given to a
@ -115,6 +117,24 @@ func WithSSH() Option {
} }
} }
// WithDockerWorkdir allows the docker working directory to be set.
func WithDockerWorkdir(dir string) Option {
return func(tsic *TailscaleInContainer) {
tsic.workdir = dir
}
}
// WithDockerEntrypoint allows the docker entrypoint of the container
// to be overridden. This is a dangerous option which can make
// the container not work as intended as a typo might prevent
// tailscaled and other processes from starting.
// Use with caution.
func WithDockerEntrypoint(args []string) Option {
return func(tsic *TailscaleInContainer) {
tsic.withEntrypoint = args
}
}
// New returns a new TailscaleInContainer instance. // New returns a new TailscaleInContainer instance.
func New( func New(
pool *dockertest.Pool, pool *dockertest.Pool,
@ -135,6 +155,12 @@ func New(
pool: pool, pool: pool,
network: network, network: network,
withEntrypoint: []string{
"/bin/bash",
"-c",
"/bin/sleep 3 ; update-ca-certificates ; tailscaled --tun=tsdev",
},
} }
for _, opt := range opts { for _, opt := range opts {
@ -147,11 +173,7 @@ func New(
// Cmd: []string{ // Cmd: []string{
// "tailscaled", "--tun=tsdev", // "tailscaled", "--tun=tsdev",
// }, // },
Entrypoint: []string{ Entrypoint: tsic.withEntrypoint,
"/bin/bash",
"-c",
"/bin/sleep 3 ; update-ca-certificates ; tailscaled --tun=tsdev",
},
} }
if tsic.headscaleHostname != "" { if tsic.headscaleHostname != "" {
@ -161,6 +183,10 @@ func New(
} }
} }
if tsic.workdir != "" {
tailscaleOptions.WorkingDir = tsic.workdir
}
// dockertest isnt very good at handling containers that has already // dockertest isnt very good at handling containers that has already
// been created, this is an attempt to make sure this container isnt // been created, this is an attempt to make sure this container isnt
// present. // present.
@ -520,6 +546,98 @@ func (t *TailscaleInContainer) Ping(hostnameOrIP string, opts ...PingOption) err
}) })
} }
type (
// CurlOption repreent optional settings that can be given
// to curl another host.
CurlOption = func(args *curlArgs)
curlArgs struct {
connectionTimeout time.Duration
maxTime time.Duration
retry int
retryDelay time.Duration
retryMaxTime time.Duration
}
)
// WithCurlConnectionTimeout sets the timeout for each connection started
// by curl.
func WithCurlConnectionTimeout(timeout time.Duration) CurlOption {
return func(args *curlArgs) {
args.connectionTimeout = timeout
}
}
// WithCurlMaxTime sets the max time for a transfer for each connection started
// by curl.
func WithCurlMaxTime(t time.Duration) CurlOption {
return func(args *curlArgs) {
args.maxTime = t
}
}
// WithCurlRetry sets the number of retries a connection is attempted by curl.
func WithCurlRetry(ret int) CurlOption {
return func(args *curlArgs) {
args.retry = ret
}
}
const (
defaultConnectionTimeout = 3 * time.Second
defaultMaxTime = 10 * time.Second
defaultRetry = 5
defaultRetryDelay = 0 * time.Second
defaultRetryMaxTime = 50 * time.Second
)
// Curl executes the Tailscale curl command and curls a hostname
// or IP. It accepts a series of CurlOption.
func (t *TailscaleInContainer) Curl(url string, opts ...CurlOption) (string, error) {
args := curlArgs{
connectionTimeout: defaultConnectionTimeout,
maxTime: defaultMaxTime,
retry: defaultRetry,
retryDelay: defaultRetryDelay,
retryMaxTime: defaultRetryMaxTime,
}
for _, opt := range opts {
opt(&args)
}
command := []string{
"curl",
"--silent",
"--connect-timeout", fmt.Sprintf("%d", int(args.connectionTimeout.Seconds())),
"--max-time", fmt.Sprintf("%d", int(args.maxTime.Seconds())),
"--retry", fmt.Sprintf("%d", args.retry),
"--retry-delay", fmt.Sprintf("%d", int(args.retryDelay.Seconds())),
"--retry-max-time", fmt.Sprintf("%d", int(args.retryMaxTime.Seconds())),
url,
}
var result string
err := t.pool.Retry(func() error {
var err error
result, _, err = t.Execute(command)
if err != nil {
log.Printf(
"failed to run curl command from %s to %s, err: %s",
t.Hostname(),
url,
err,
)
return err
}
return nil
})
return result, err
}
// WriteFile save file inside the Tailscale container. // WriteFile save file inside the Tailscale container.
func (t *TailscaleInContainer) WriteFile(path string, data []byte) error { func (t *TailscaleInContainer) WriteFile(path string, data []byte) error {
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data) return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)