package container

import (
	"context"
	_ "embed"
	"slices"
	"strings"
	"testing"

	"github.com/moby/moby/client"
	"github.com/moby/moby/v2/integration/internal/container"
	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"
	"gotest.tools/v3/skip"
)

var (
	//go:embed testdata/af_alg.c
	afALGSource string

	//go:embed testdata/af_vsock.c
	afVSOCKSource string

	//go:embed testdata/socketcall.c
	socketcallSource string
)

// compileAndExecSocketDenied writes a C source file into the container,
// compiles it with the given compiler command, runs the binary as uid 1000,
// and asserts that socket creation fails.
func compileAndExecSocketDenied(ctx context.Context, t *testing.T, apiClient client.APIClient, cID string, name string, src string, cc []string) {
	t.Helper()

	binPath := "/tmp/" + name
	srcPath := binPath + ".c"

	res := container.ExecT(ctx, t, apiClient, cID, []string{
		"sh", "-c", "cat > " + srcPath + " << 'CEOF'\n" + src + "\nCEOF",
	})
	res.AssertSuccess(t)

	compileCmd := append(cc, srcPath, "-o", binPath)
	res = container.ExecT(ctx, t, apiClient, cID, compileCmd)
	res.AssertSuccess(t)

	res, err := container.Exec(ctx, apiClient, cID, []string{binPath},
		func(ec *client.ExecCreateOptions) {
			ec.User = "1000"
		},
	)
	assert.NilError(t, err)
	assert.Check(t, is.Equal(res.ExitCode, 1), "expected %s socket program to fail", name)

	out := strings.ToLower(res.Combined())
	assert.Check(t, is.Contains(out, "socket"), "expected socket-related error message")
	// Seccomp returns EPERM ("not permitted"), AppArmor returns EACCES
	// ("permission denied"). Accept either.
	denied := strings.Contains(out, "not permitted") || strings.Contains(out, "permission denied")
	assert.Check(t, denied, "expected EPERM or EACCES, got: %s", res.Combined())
}

// TestExecSocketDenied verifies that AF_ALG and AF_VSOCK sockets cannot be
// created inside a container. AF_ALG is blocked by the default seccomp profile
// (via socket arg filtering) and by the default AppArmor profile (via
// "deny network alg"). AF_VSOCK is blocked by seccomp only.
func TestExecSocketDenied(t *testing.T) {
	skip.If(t, testEnv.DaemonInfo.OSType != "linux")

	ctx := setupTest(t)
	apiClient := testEnv.APIClient()

	cID := container.Run(ctx, t, apiClient, container.WithImage("debian:trixie-slim"), container.WithCmd("sleep", "infinity"))

	// Install build dependencies as root.
	res := container.ExecT(ctx, t, apiClient, cID, []string{
		"sh", "-c", "apt-get update && apt-get install -y --no-install-recommends gcc libc-dev linux-libc-dev",
	})
	res.AssertSuccess(t)

	gcc := []string{"gcc"}

	arch := testEnv.DaemonInfo.Architecture
	isAmd64 := arch == "amd64" || arch == "x86_64"

	t.Run("AF_ALG", func(t *testing.T) {
		compileAndExecSocketDenied(ctx, t, apiClient, cID, "AF_ALG", afALGSource, gcc)
	})
	t.Run("AF_VSOCK", func(t *testing.T) {
		compileAndExecSocketDenied(ctx, t, apiClient, cID, "AF_VSOCK", afVSOCKSource, gcc)
	})

	// Test socketcall(2) via int $0x80 to invoke the ia32 compat syscall
	// path from a native 64-bit binary. MAP_32BIT is used to place the
	// args array below 4 GB since the ia32 compat path truncates all
	// registers to 32 bits.
	//
	// The socketcall binary is compiled with -DSOCK_FAMILY and -DSOCK_TYPE
	// to set the address family and socket type at compile time.
	t.Run("socketcall_int80", func(t *testing.T) {
		skip.If(t, !isAmd64, "int $0x80 ia32 compat only available on amd64")
		// Seccomp cannot filter socketcall arguments (the address family
		// is behind a userspace pointer). Only an LSM (AppArmor or
		// SELinux) can deny AF_ALG via the security_socket_create hook.
		hasLSM := slices.Contains(testEnv.DaemonInfo.SecurityOptions, "name=apparmor") ||
			slices.Contains(testEnv.DaemonInfo.SecurityOptions, "name=selinux")
		skip.If(t, !hasLSM, "socketcall filtering requires AppArmor or SELinux")

		srcPath := "/tmp/socketcall.c"
		res := container.ExecT(ctx, t, apiClient, cID, []string{
			"sh", "-c", "cat > " + srcPath + " << 'CEOF'\n" + socketcallSource + "\nCEOF",
		})
		res.AssertSuccess(t)

		// AF_ALG (38) via socketcall must be denied by the LSM
		// (AppArmor's "deny network alg" or SELinux's alg_socket deny),
		// which catches it at the security_socket_create hook even
		// though seccomp cannot filter socketcall args.
		t.Run("AF_ALG", func(t *testing.T) {
			binPath := "/tmp/socketcall_af_alg"
			res := container.ExecT(ctx, t, apiClient, cID, append(gcc,
				"-DSOCK_FAMILY=AF_ALG", "-DSOCK_TYPE=SOCK_SEQPACKET",
				"-include", "linux/if_alg.h",
				srcPath, "-o", binPath,
			))
			res.AssertSuccess(t)

			res, err := container.Exec(ctx, apiClient, cID, []string{binPath},
				func(ec *client.ExecCreateOptions) {
					ec.User = "1000"
				},
			)
			assert.NilError(t, err)
			assert.Check(t, is.Equal(res.ExitCode, 1), "expected AF_ALG socketcall to fail, got: %s", res.Combined())
			assert.Check(t, is.Contains(strings.ToLower(res.Combined()), "permission denied"))
		})

		// AF_INET via socketcall must still work to ensure the deny
		// rule is targeted and does not break legitimate usage.
		t.Run("AF_INET", func(t *testing.T) {
			binPath := "/tmp/socketcall_af_inet"
			res := container.ExecT(ctx, t, apiClient, cID, append(gcc,
				"-DSOCK_FAMILY=AF_INET", "-DSOCK_TYPE=SOCK_STREAM",
				srcPath, "-o", binPath,
			))
			res.AssertSuccess(t)

			res, err := container.Exec(ctx, apiClient, cID, []string{binPath},
				func(ec *client.ExecCreateOptions) {
					ec.User = "1000"
				},
			)
			assert.NilError(t, err)
			assert.Check(t, is.Equal(res.ExitCode, 0), "expected AF_INET socketcall to succeed, got: %s", res.Combined())
		})
	})
}