Skip to content

Commit

Permalink
WIP work towards networking support
Browse files Browse the repository at this point in the history
This supports networking restrictions
and is compatible with patch 11.
  • Loading branch information
gnoack committed Jan 6, 2024
1 parent 43ca26d commit 028b835
Show file tree
Hide file tree
Showing 13 changed files with 362 additions and 34 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 (V2 + 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
52 changes: 52 additions & 0 deletions landlock/net_opt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package landlock

import (
"fmt"

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

type NetOpt struct {
access AccessNetSet
port uint16
}

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

// TODO: Should it be called ListenTCP? (In Go, the notions of
// "binding" and "listening" are often conflated.)
func BindTCP(port uint16) NetOpt {
return NetOpt{
access: ll.AccessNetBindTCP,
port: port,
}
}

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

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

func (n NetOpt) 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 NetOpt) downgrade(c Config) (out Rule, ok bool) {
return NetOpt{
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 PathOpts
// - rule.accessNet ⊆ handledAccessNet for NetOpts
//
// 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 028b835

Please sign in to comment.