-
Notifications
You must be signed in to change notification settings - Fork 576
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
audio-playback: Add support for snapped pipewire
A snapped pipewire requires being able to create the pulse folder inside the global XDG_RUNTIME_DIR folder (not the snap own folder). Also, it requires to be able to create both the pipewire-[0-9]-manager and its lock file. This patch adds that capability to the audio-playback slot; thus, a snap offering that slot will be able to create the corresponding global sockets.
- Loading branch information
1 parent
0dfd94b
commit a23df8e
Showing
4 changed files
with
353 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
// -*- Mode: Go; indent-tabs-mode: t -*- | ||
|
||
/* | ||
* Copyright (C) 2018 Canonical Ltd | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU General Public License version 3 as | ||
* published by the Free Software Foundation. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU General Public License | ||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
* | ||
*/ | ||
|
||
package builtin | ||
|
||
import ( | ||
"strings" | ||
|
||
"github.com/snapcore/snapd/interfaces" | ||
"github.com/snapcore/snapd/interfaces/apparmor" | ||
"github.com/snapcore/snapd/interfaces/seccomp" | ||
"github.com/snapcore/snapd/snap" | ||
) | ||
|
||
// The audio-playback interface is the companion interface to the audio-record | ||
// interface. The design of this interface is based on the idea that the slot | ||
// implementation (eg pulseaudio) is expected to query snapd on if the | ||
// audio-record slot is connected or not and the audio service will mediate | ||
// recording (ie, the rules below allow connecting to the audio service, but do | ||
// not implement enforcement rules; it is up to the audio service to provide | ||
// enforcement). If other audio recording servers require different security | ||
// policy for record and playback (eg, a different socket path), then those | ||
// accesses will be added to this interface. | ||
|
||
const pipewireSummary = `allows access to the pipewire sockets, and offer them` | ||
|
||
const pipewireBaseDeclarationSlots = ` | ||
pipewire: | ||
deny-auto-connection: true | ||
` | ||
|
||
const pipewireConnectedPlugAppArmor = ` | ||
# Allow communicating with pipewire service | ||
/{run,dev}/shm/pulse-shm-* mrwk, | ||
owner /run/user/[0-9]*/ r, | ||
owner /run/user/[0-9]*/pipewire-[0-9] rw, | ||
owner /run/user/[0-9]*/pipewire-[0-9]-manager rw, | ||
` | ||
|
||
const pipewireConnectedPlugAppArmorCore = ` | ||
owner /run/user/[0-9]*/###SLOT_SECURITY_TAGS###/pipewire-[0-9] rw, | ||
owner /run/user/[0-9]*/###SLOT_SECURITY_TAGS###/pipewire-[0-9]-manager rw, | ||
` | ||
|
||
const pipewireConnectedPlugSecComp = ` | ||
shmctl | ||
` | ||
|
||
const pipewirePermanentSlotAppArmor = ` | ||
capability sys_nice, | ||
capability sys_resource, | ||
owner @{PROC}/@{pid}/exe r, | ||
/etc/machine-id r, | ||
# For udev | ||
network netlink raw, | ||
/sys/devices/virtual/dmi/id/sys_vendor r, | ||
/sys/devices/virtual/dmi/id/bios_vendor r, | ||
/sys/**/sound/** r, | ||
# Shared memory based communication with clients | ||
/{run,dev}/shm/pulse-shm-* mrwk, | ||
owner /run/user/[0-9]*/pipewire-[0-9] rwk, | ||
owner /run/user/[0-9]*/pipewire-[0-9]-manager rwk, | ||
# This allows wireplumber to read the pulseaudio | ||
# configuration if pipewire runs inside a container | ||
/etc/pulse/ r, | ||
/etc/pulse/** r, | ||
` | ||
|
||
const pipewirePermanentSlotSecComp = ` | ||
# The following are needed for UNIX sockets | ||
personality | ||
setpriority | ||
bind | ||
listen | ||
accept | ||
accept4 | ||
shmctl | ||
# libudev | ||
socket AF_NETLINK - NETLINK_KOBJECT_UEVENT | ||
` | ||
|
||
type pipewireInterface struct{} | ||
|
||
func (iface *pipewireInterface) Name() string { | ||
return "pipewire" | ||
} | ||
|
||
func (iface *pipewireInterface) StaticInfo() interfaces.StaticInfo { | ||
return interfaces.StaticInfo{ | ||
Summary: pipewireSummary, | ||
ImplicitOnClassic: false, | ||
ImplicitOnCore: false, | ||
BaseDeclarationSlots: pipewireBaseDeclarationSlots, | ||
} | ||
} | ||
|
||
func (iface *pipewireInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { | ||
spec.AddSnippet(pipewireConnectedPlugAppArmor) | ||
if !implicitSystemConnectedSlot(slot) { | ||
old := "###SLOT_SECURITY_TAGS###" | ||
new := "snap." + slot.Snap().InstanceName() // forms the snap-instance-specific subdirectory name of /run/user/*/ used for XDG_RUNTIME_DIR | ||
snippet := strings.Replace(pipewireConnectedPlugAppArmorCore, old, new, -1) | ||
spec.AddSnippet(snippet) | ||
} | ||
return nil | ||
} | ||
|
||
func (iface *pipewireInterface) AppArmorPermanentSlot(spec *apparmor.Specification, slot *snap.SlotInfo) error { | ||
spec.AddSnippet(pipewirePermanentSlotAppArmor) | ||
return nil | ||
} | ||
|
||
func (iface *pipewireInterface) SecCompConnectedPlug(spec *seccomp.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { | ||
spec.AddSnippet(pipewireConnectedPlugSecComp) | ||
return nil | ||
} | ||
|
||
func (iface *pipewireInterface) SecCompPermanentSlot(spec *seccomp.Specification, slot *snap.SlotInfo) error { | ||
spec.AddSnippet(pipewirePermanentSlotSecComp) | ||
return nil | ||
} | ||
|
||
func (iface *pipewireInterface) AutoConnect(*snap.PlugInfo, *snap.SlotInfo) bool { | ||
return true | ||
} | ||
|
||
func init() { | ||
registerIface(&pipewireInterface{}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
// -*- Mode: Go; indent-tabs-mode: t -*- | ||
|
||
/* | ||
* Copyright (C) 2018 Canonical Ltd | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU General Public License version 3 as | ||
* published by the Free Software Foundation. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU General Public License | ||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
* | ||
*/ | ||
|
||
package builtin_test | ||
|
||
import ( | ||
. "gopkg.in/check.v1" | ||
|
||
"github.com/snapcore/snapd/interfaces" | ||
"github.com/snapcore/snapd/interfaces/apparmor" | ||
"github.com/snapcore/snapd/interfaces/builtin" | ||
"github.com/snapcore/snapd/interfaces/seccomp" | ||
"github.com/snapcore/snapd/interfaces/udev" | ||
"github.com/snapcore/snapd/release" | ||
"github.com/snapcore/snapd/snap" | ||
"github.com/snapcore/snapd/testutil" | ||
) | ||
|
||
type pipewireInterfaceSuite struct { | ||
iface interfaces.Interface | ||
coreSlotInfo *snap.SlotInfo | ||
coreSlot *interfaces.ConnectedSlot | ||
classicSlotInfo *snap.SlotInfo | ||
classicSlot *interfaces.ConnectedSlot | ||
plugInfo *snap.PlugInfo | ||
plug *interfaces.ConnectedPlug | ||
} | ||
|
||
var _ = Suite(&pipewireInterfaceSuite{ | ||
iface: builtin.MustInterface("pipewire"), | ||
}) | ||
|
||
const pipewireMockPlugSnapInfoYaml = `name: consumer | ||
version: 1.0 | ||
apps: | ||
app: | ||
command: foo | ||
plugs: [pipewire] | ||
` | ||
|
||
// a pipewire slot on a pipewire snap (as installed on a core/all-snap system) | ||
const pipewireMockCoreSlotSnapInfoYaml = `name: pipewire | ||
version: 1.0 | ||
apps: | ||
app1: | ||
command: foo | ||
slots: [pipewire] | ||
` | ||
|
||
// a pipewire slot on the core snap (as automatically added on classic) | ||
const pipewireMockClassicSlotSnapInfoYaml = `name: core | ||
version: 0 | ||
type: os | ||
slots: | ||
pipewire: | ||
interface: pipewire | ||
` | ||
|
||
func (s *pipewireInterfaceSuite) SetUpTest(c *C) { | ||
s.coreSlot, s.coreSlotInfo = MockConnectedSlot(c, pipewireMockCoreSlotSnapInfoYaml, nil, "pipewire") | ||
s.classicSlot, s.classicSlotInfo = MockConnectedSlot(c, pipewireMockClassicSlotSnapInfoYaml, nil, "pipewire") | ||
s.plug, s.plugInfo = MockConnectedPlug(c, pipewireMockPlugSnapInfoYaml, nil, "pipewire") | ||
} | ||
|
||
func (s *pipewireInterfaceSuite) TestName(c *C) { | ||
c.Assert(s.iface.Name(), Equals, "pipewire") | ||
} | ||
|
||
func (s *pipewireInterfaceSuite) TestSanitizeSlot(c *C) { | ||
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.coreSlotInfo), IsNil) | ||
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.classicSlotInfo), IsNil) | ||
} | ||
|
||
func (s *pipewireInterfaceSuite) TestSanitizePlug(c *C) { | ||
c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil) | ||
} | ||
|
||
func (s *pipewireInterfaceSuite) TestSecComp(c *C) { | ||
restore := release.MockOnClassic(false) | ||
defer restore() | ||
|
||
// connected plug to core slot | ||
spec := seccomp.NewSpecification(s.plug.AppSet()) | ||
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.coreSlot), IsNil) | ||
c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) | ||
c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "shmctl\n") | ||
|
||
// connected core slot to plug | ||
spec = seccomp.NewSpecification(s.coreSlot.AppSet()) | ||
c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.coreSlot), IsNil) | ||
c.Assert(spec.SecurityTags(), HasLen, 0) | ||
|
||
// permanent core slot | ||
spec = seccomp.NewSpecification(s.coreSlot.AppSet()) | ||
c.Assert(spec.AddPermanentSlot(s.iface, s.coreSlotInfo), IsNil) | ||
c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.pipewire.app1"}) | ||
c.Assert(spec.SnippetForTag("snap.pipewire.app1"), testutil.Contains, "listen\n") | ||
} | ||
|
||
func (s *pipewireInterfaceSuite) TestSecCompOnClassic(c *C) { | ||
restore := release.MockOnClassic(true) | ||
defer restore() | ||
|
||
// connected plug to classic slot | ||
spec := seccomp.NewSpecification(s.plug.AppSet()) | ||
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.classicSlot), IsNil) | ||
c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) | ||
c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "shmctl\n") | ||
|
||
c.Assert(spec.SnippetForTag("snap.consumer.app"), Not(testutil.Contains), "owner /run/user/[0-9]*/snap.pipewire/pulse/ r,\n") | ||
c.Assert(spec.SnippetForTag("snap.consumer.app"), Not(testutil.Contains), "owner /run/user/[0-9]*/snap.pipewire/pulse/native rwk,\n") | ||
c.Assert(spec.SnippetForTag("snap.consumer.app"), Not(testutil.Contains), "owner /run/user/[0-9]*/snap.pipewire/pulse/pid r,\n") | ||
|
||
// connected classic slot to plug | ||
spec = seccomp.NewSpecification(s.classicSlot.AppSet()) | ||
c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.classicSlot), IsNil) | ||
c.Assert(spec.SecurityTags(), HasLen, 0) | ||
|
||
// permanent classic slot | ||
spec = seccomp.NewSpecification(s.classicSlot.AppSet()) | ||
c.Assert(spec.AddPermanentSlot(s.iface, s.classicSlotInfo), IsNil) | ||
c.Assert(spec.SecurityTags(), HasLen, 0) | ||
|
||
c.Assert(spec.SnippetForTag("snap.pipewire.app1"), Not(testutil.Contains), "owner /run/user/[0-9]*/pipewire-[0-9] rwk,\n") | ||
c.Check(spec.SnippetForTag("snap.pipewire.app1"), Not(testutil.Contains), "/etc/pulse/ r,\n") | ||
c.Check(spec.SnippetForTag("snap.pipewire.app1"), Not(testutil.Contains), "/etc/pulse/** r,\n") | ||
} | ||
|
||
func (s *pipewireInterfaceSuite) TestAppArmor(c *C) { | ||
restore := release.MockOnClassic(false) | ||
defer restore() | ||
|
||
// connected plug to core slot | ||
spec := apparmor.NewSpecification(s.plug.AppSet()) | ||
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.coreSlot), IsNil) | ||
c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) | ||
c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/{run,dev}/shm/pulse-shm-* mrwk,\n") | ||
c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "owner /run/user/[0-9]*/snap.pipewire/pipewire-[0-9] rw,\n") | ||
c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "owner /run/user/[0-9]*/snap.pipewire/pipewire-[0-9]-manager rw,\n") | ||
|
||
// connected core slot to plug | ||
spec = apparmor.NewSpecification(s.coreSlot.AppSet()) | ||
c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.coreSlot), IsNil) | ||
c.Assert(spec.SecurityTags(), HasLen, 0) | ||
|
||
// permanent core slot | ||
spec = apparmor.NewSpecification(s.coreSlot.AppSet()) | ||
c.Assert(spec.AddPermanentSlot(s.iface, s.coreSlotInfo), IsNil) | ||
c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.pipewire.app1"}) | ||
c.Assert(spec.SnippetForTag("snap.pipewire.app1"), testutil.Contains, "owner /run/user/[0-9]*/pipewire-[0-9] rwk,\n") | ||
c.Assert(spec.SnippetForTag("snap.pipewire.app1"), testutil.Contains, "owner /run/user/[0-9]*/pipewire-[0-9]-manager rwk,\n") | ||
} | ||
|
||
func (s *pipewireInterfaceSuite) TestAppArmorOnClassic(c *C) { | ||
restore := release.MockOnClassic(true) | ||
defer restore() | ||
|
||
// connected plug to classic slot | ||
spec := apparmor.NewSpecification(s.plug.AppSet()) | ||
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.classicSlot), IsNil) | ||
c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) | ||
c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/{run,dev}/shm/pulse-shm-* mrwk,\n") | ||
c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "owner /run/user/[0-9]*/pipewire-[0-9] rw,\n") | ||
c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "owner /run/user/[0-9]*/pipewire-[0-9]-manager rw,\n") | ||
|
||
// connected classic slot to plug | ||
spec = apparmor.NewSpecification(s.classicSlot.AppSet()) | ||
c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.classicSlot), IsNil) | ||
c.Assert(spec.SecurityTags(), HasLen, 0) | ||
|
||
// permanent classic slot | ||
spec = apparmor.NewSpecification(s.classicSlot.AppSet()) | ||
c.Assert(spec.AddPermanentSlot(s.iface, s.classicSlotInfo), IsNil) | ||
c.Assert(spec.SecurityTags(), HasLen, 0) | ||
} | ||
|
||
func (s *pipewireInterfaceSuite) TestUDev(c *C) { | ||
spec := udev.NewSpecification(s.coreSlot.AppSet()) | ||
c.Assert(spec.AddPermanentSlot(s.iface, s.coreSlotInfo), IsNil) | ||
c.Assert(spec.Snippets(), HasLen, 0) | ||
} | ||
|
||
func (s *pipewireInterfaceSuite) TestInterfaces(c *C) { | ||
c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface) | ||
} |