Skip to content

Commit

Permalink
Networking support
Browse files Browse the repository at this point in the history
This supports networking restrictions
and is compatible with Linux 6.7.
  • Loading branch information
gnoack committed Jan 9, 2024
1 parent 43ca26d commit 72ee8c1
Show file tree
Hide file tree
Showing 13 changed files with 368 additions and 36 deletions.
15 changes: 12 additions & 3 deletions landlock/abi_versions.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package landlock
import ll "github.com/landlock-lsm/go-landlock/landlock/syscall"

type abiInfo struct {
version int
supportedAccessFS AccessFSSet
version int
supportedAccessFS AccessFSSet
supportedAccessNet AccessNetSet
}

var abiInfos = []abiInfo{
Expand All @@ -24,10 +25,18 @@ var abiInfos = []abiInfo{
version: 3,
supportedAccessFS: (1 << 15) - 1,
},
{
version: 4,
supportedAccessFS: (1 << 15) - 1,
supportedAccessNet: (1 << 2) - 1,
},
}

func (a abiInfo) asConfig() Config {
return Config{handledAccessFS: a.supportedAccessFS}
return Config{
handledAccessFS: a.supportedAccessFS,
handledAccessNet: a.supportedAccessNet,
}
}

// getSupportedABIVersion returns the kernel-supported ABI version.
Expand Down
2 changes: 1 addition & 1 deletion landlock/abi_versions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func TestAbiVersionsIncrementing(t *testing.T) {
}

func TestSupportedAccessFS(t *testing.T) {
got := abiInfos[3].supportedAccessFS
got := abiInfos[4].supportedAccessFS
want := supportedAccessFS

if got != want {
Expand Down
35 changes: 35 additions & 0 deletions landlock/accessnet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package landlock

// AccessNetSet is a set of Landlockable network access rights.
type AccessNetSet uint64

var accessNetNames = []string{
"bind_tcp",
"connect_tcp",
}

var supportedAccessNet = AccessNetSet((1 << len(accessNetNames)) - 1)

func (a AccessNetSet) String() string {
return accessSetString(uint64(a), accessNetNames)
}

func (a AccessNetSet) isSubset(b AccessNetSet) bool {
return a&b == a
}

func (a AccessNetSet) intersect(b AccessNetSet) AccessNetSet {
return a & b
}

func (a AccessNetSet) union(b AccessNetSet) AccessNetSet {
return a | b
}

func (a AccessNetSet) isEmpty() bool {
return a == 0
}

func (a AccessNetSet) valid() bool {
return a.isSubset(supportedAccessNet)
}
40 changes: 32 additions & 8 deletions landlock/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ var (
V2 = abiInfos[2].asConfig()
// Landlock V3 support (V2 + file truncation)
V3 = abiInfos[3].asConfig()
// Landlock V4 support (V3 + networking)
V4 = abiInfos[4].asConfig()
)

// v0 denotes "no Landlock support". Only used internally.
Expand All @@ -44,8 +46,9 @@ var v0 = Config{}
// landlockable operations to be restricted and the constraints on it
// (e.g. best effort mode).
type Config struct {
handledAccessFS AccessFSSet
bestEffort bool
handledAccessFS AccessFSSet
handledAccessNet AccessNetSet
bestEffort bool
}

// NewConfig creates a new Landlock configuration with the given parameters.
Expand All @@ -64,15 +67,24 @@ func NewConfig(args ...interface{}) (*Config, error) {
// invalid Config values.
var c Config
for _, arg := range args {
if afs, ok := arg.(AccessFSSet); ok {
switch arg := arg.(type) {
case AccessFSSet:
if !c.handledAccessFS.isEmpty() {
return nil, errors.New("only one AccessFSSet may be provided")
}
if !afs.valid() {
if !arg.valid() {
return nil, errors.New("unsupported AccessFSSet value; upgrade go-landlock?")
}
c.handledAccessFS = afs
} else {
c.handledAccessFS = arg
case AccessNetSet:
if !c.handledAccessNet.isEmpty() {
return nil, errors.New("only one AccessNetSet may be provided")
}
if !arg.valid() {
return nil, errors.New("unsupported AccessNetSet value; upgrade go-landlock?")
}
c.handledAccessNet = arg
default:
return nil, fmt.Errorf("unknown argument %v; only AccessFSSet-type argument is supported", arg)
}
}
Expand Down Expand Up @@ -103,6 +115,11 @@ func (c Config) String() string {
fsDesc = "all"
}

var netDesc = c.handledAccessNet.String()
if abi.supportedAccessNet == c.handledAccessNet && c.handledAccessNet != 0 {
fsDesc = "all"
}

var bestEffort = ""
if c.bestEffort {
bestEffort = " (best effort)"
Expand All @@ -115,7 +132,7 @@ func (c Config) String() string {
version = fmt.Sprintf("V%v", abi.version)
}

return fmt.Sprintf("{Landlock %v; FS: %v%v}", version, fsDesc, bestEffort)
return fmt.Sprintf("{Landlock %v; FS: %v; Net: %v%v}", version, fsDesc, netDesc, bestEffort)
}

// BestEffort returns a config that will opportunistically enforce
Expand Down Expand Up @@ -222,6 +239,12 @@ func (c Config) BestEffort() Config {
//
// [Kernel Documentation about Access Rights]: https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#access-rights
func (c Config) RestrictPaths(rules ...Rule) error {
c.handledAccessNet = 0 // clear out everything but file system access
return restrict(c, rules...)
}

func (c Config) RestrictNet(rules ...Rule) error {
c.handledAccessFS = 0 // clear out everything but network access
return restrict(c, rules...)
}

Expand All @@ -231,7 +254,8 @@ type PathOpt = Rule

// compatibleWith is true if c is compatible to work at the given Landlock ABI level.
func (c Config) compatibleWithABI(abi abiInfo) bool {
return c.handledAccessFS.isSubset(abi.supportedAccessFS)
return (c.handledAccessFS.isSubset(abi.supportedAccessFS) &&
c.handledAccessNet.isSubset(abi.supportedAccessNet))
}

// restrictTo returns a config that is a subset of c and which is compatible with the given ABI.
Expand Down
17 changes: 10 additions & 7 deletions landlock/config_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package landlock

import (
"fmt"
"testing"

ll "github.com/landlock-lsm/go-landlock/landlock/syscall"
Expand All @@ -13,24 +12,28 @@ func TestConfigString(t *testing.T) {
want string
}{
{
cfg: Config{handledAccessFS: 0},
want: fmt.Sprintf("{Landlock V0; FS: %v}", AccessFSSet(0)),
cfg: Config{handledAccessFS: 0, handledAccessNet: 0},
want: "{Landlock V0; FS: ∅; Net: ∅}",
},
{
cfg: Config{handledAccessFS: ll.AccessFSWriteFile},
want: "{Landlock V1; FS: {write_file}}",
want: "{Landlock V1; FS: {write_file}; Net: ∅}",
},
{
cfg: Config{handledAccessNet: ll.AccessNetBindTCP},
want: "{Landlock V4; FS: ∅; Net: {bind_tcp}}",
},
{
cfg: V1,
want: "{Landlock V1; FS: all}",
want: "{Landlock V1; FS: all; Net: ∅}",
},
{
cfg: V1.BestEffort(),
want: "{Landlock V1; FS: all (best effort)}",
want: "{Landlock V1; FS: all; Net: ∅ (best effort)}",
},
{
cfg: Config{handledAccessFS: 1 << 63},
want: "{Landlock V???; FS: {1<<63}}",
want: "{Landlock V???; FS: {1<<63}; Net: ∅}",
},
} {
got := tc.cfg.String()
Expand Down
55 changes: 55 additions & 0 deletions landlock/net_opt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package landlock

import (
"fmt"

ll "github.com/landlock-lsm/go-landlock/landlock/syscall"
)

type NetRule struct {
access AccessNetSet
port uint16
}

func DialTCP(port uint16) NetRule {
return NetRule{
access: ll.AccessNetConnectTCP,
port: port,
}
}

// BindTCP is a [Rule] which grants the right to bind a socket to a
// given TCP port.
//
// In Go, the bind(2) operation is usually run as part of
// net.Listen().
func BindTCP(port uint16) NetRule {
return NetRule{
access: ll.AccessNetBindTCP,
port: port,
}
}

func (n NetRule) String() string {
return fmt.Sprintf("ALLOW %v on TCP port %v", n.access, n.port)
}

func (n NetRule) compatibleWithConfig(c Config) bool {
return n.access.isSubset(c.handledAccessNet)
}

func (n NetRule) addToRuleset(rulesetFD int, c Config) error {
flags := 0
attr := &ll.NetServiceAttr{
AllowedAccess: uint64(n.access),
Port: n.port,
}
return ll.LandlockAddNetServiceRule(rulesetFD, attr, flags)
}

func (n NetRule) downgrade(c Config) (out Rule, ok bool) {
return NetRule{
access: n.access.intersect(c.handledAccessNet),
port: n.port,
}, true
}
3 changes: 2 additions & 1 deletion landlock/opt.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ type Rule interface {
//
// It establishes that:
//
// - rule.accessFS ⊆ handledAccessFS
// - rule.accessFS ⊆ handledAccessFS for PathRules
// - rule.accessNet ⊆ handledAccessNet for NetRules
//
// If the rule is unsupportable under the given Config at
// all, ok is false. This happens when c represents a Landlock
Expand Down
5 changes: 3 additions & 2 deletions landlock/restrict.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,13 @@ func restrict(c Config, rules ...Rule) error {
// always implicit, even in Landlock V1. So enabling Landlock
// on a Landlock V1 kernel without any handled access rights
// will still forbid linking files between directories.
if c.handledAccessFS.isEmpty() {
if c.handledAccessFS.isEmpty() && c.handledAccessNet.isEmpty() {
return nil // Success: Nothing to restrict.
}

rulesetAttr := ll.RulesetAttr{
HandledAccessFS: uint64(c.handledAccessFS),
HandledAccessFS: uint64(c.handledAccessFS),
HandledAccessNet: uint64(c.handledAccessNet),
}
fd, err := ll.LandlockCreateRuleset(&rulesetAttr, 0)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion landlock/restrict_downgrade_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
ll "github.com/landlock-lsm/go-landlock/landlock/syscall"
)

func TestDowngrade(t *testing.T) {
func TestDowngradeAccessFS(t *testing.T) {
for _, tc := range []struct {
Name string

Expand Down
Loading

0 comments on commit 72ee8c1

Please sign in to comment.