Многое было не понятно из первых четырёх статей, так как была одна теория. Теперь на практике мы создадим наш первый, простой интерфейс и пройдём весь путь от форка snapd под наши нужды до запуска программы, которая будет использовать новый интерфейс в форке.
Давайте оглядимся. Каждый раз, когда добавляется новый интерфейс, следующие файлы претерпевают изменения:
Кроме того, если слот интерфейса должен явно отражаться в core snap, то нужны изменения в snap/implicit{,_test}.go, но к этому вернёмся позже. Итак, давайте создадим наш новый интерфейс. Как было сказано в теории Анатомия интерфейса snappy нам нужно создать новый тип с несколькими методами. Берите любимый редактор и создавайте файл interfaces/builtin/hello.go с кодом
// -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2016 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 www.gnu.org/licenses/>. * */ package builtin import ( "fmt" "github.com/snapcore/snapd/interfaces" ) // HelloInterface is the hello interface for a tutorial. type HelloInterface struct{} // String returns the same value as Name(). func (iface *HelloInterface) Name() string { return "hello" } // SanitizeSlot checks and possibly modifies a slot. func (iface *HelloInterface) SanitizeSlot(slot *interfaces.Slot) error { if iface.Name() != slot.Interface { panic(fmt.Sprintf("slot is not of interface %q", iface)) } // NOTE: currently we don't check anything on the slot side. return nil } // SanitizePlug checks and possibly modifies a plug. func (iface *HelloInterface) SanitizePlug(plug *interfaces.Plug) error { if iface.Name() != plug.Interface { panic(fmt.Sprintf("plug is not of interface %q", iface)) } // NOTE: currently we don't check anything on the plug side. return nil } // ConnectedSlotSnippet returns security snippet specific to a given connection between the hello slot and some plug. func (iface *HelloInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { switch securitySystem { case interfaces.SecurityAppArmor: return nil, nil case interfaces.SecuritySecComp: return nil, nil case interfaces.SecurityDBus: return nil, nil case interfaces.SecurityUDev: return nil, nil case interfaces.SecurityMount: return nil, nil default: return nil, interfaces.ErrUnknownSecurity } } // PermanentSlotSnippet returns security snippet permanently granted to hello slots. func (iface *HelloInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { switch securitySystem { case interfaces.SecurityAppArmor: return nil, nil case interfaces.SecuritySecComp: return nil, nil case interfaces.SecurityDBus: return nil, nil case interfaces.SecurityUDev: return nil, nil case interfaces.SecurityMount: return nil, nil default: return nil, interfaces.ErrUnknownSecurity } } // ConnectedPlugSnippet returns security snippet specific to a given connection between the hello plug and some slot. func (iface *HelloInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { switch securitySystem { case interfaces.SecurityAppArmor: return nil, nil case interfaces.SecuritySecComp: return nil, nil case interfaces.SecurityDBus: return nil, nil case interfaces.SecurityUDev: return nil, nil case interfaces.SecurityMount: return nil, nil default: return nil, interfaces.ErrUnknownSecurity } } // PermanentPlugSnippet returns the configuration snippet required to use a hello interface. func (iface *HelloInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) { switch securitySystem { case interfaces.SecurityAppArmor: return nil, nil case interfaces.SecuritySecComp: return nil, nil case interfaces.SecurityDBus: return nil, nil case interfaces.SecurityUDev: return nil, nil case interfaces.SecurityMount: return nil, nil default: return nil, interfaces.ErrUnknownSecurity } } // AutoConnect returns true if plugs and slots should be implicitly // auto-connected when an unambiguous connection candidate is available. // // This interface does not auto-connect. func (iface *HelloInterface) AutoConnect() bool { return false }
Теперь нужно сделать ещё одно изменение, чтобы о новом интерфейсе узнал snapd. Нужно добавить наше детище в список allInterfaces. Заметьте что используется функция init() для этого и все изменения затрагивают один файл, что меньше вызывает конфликтов для других разработчиков, создающих свои интерфейсы. Если хотите можете использовать готовый патч.
diff --git a/interfaces/builtin/all_test.go b/interfaces/builtin/all_test.go
index 46ca587..86c8fad 100644 --- a/interfaces/builtin/all_test.go +++ b/interfaces/builtin/all_test.go @@ -62,4 +62,5 @@ func (s *AllSuite) TestInterfaces(c *C) { c.Check(all, DeepContains, builtin.NewCupsControlInterface()) c.Check(all, DeepContains, builtin.NewOpticalDriveInterface()) c.Check(all, DeepContains, builtin.NewCameraInterface()) + c.Check(all, Contains, &builtin.HelloInterface{}) }
diff --git a/interfaces/builtin/hello.go b/interfaces/builtin/hello.go
index d791fc5..616985e 100644 --- a/interfaces/builtin/hello.go +++ b/interfaces/builtin/hello.go @@ -130,3 +130,7 @@ func (iface *HelloInterface) PermanentPlugSnippet(plug *interfaces.Plug, securit func (iface *HelloInterface) AutoConnect() bool { return false } + +func init() { + allInterfaces = append(allInterfaces, &HelloInterface{}) +}
Теперь нужно перейти в snap/ каталог и отредактировать файл implicit.go, добавив hello в implicitClassicSlots. Как видно по патчу ниже, обновлен также файл с тестами, который проверяет количество добавленных слотов, заданных неявно. Это изменение позволит snapd автоматически создавать слот hello для core snap. Если глянуть данный файл, то можно заметить что множество интерфейсов используют этот трюк по добавлению слотов к core snap.
diff --git a/snap/implicit.go b/snap/implicit.go
index 3df6810..098b312 100644 --- a/snap/implicit.go +++ b/snap/implicit.go @@ -60,6 +60,7 @@ var implicitClassicSlots = []string{ "pulseaudio", "unity7", "x11", + "hello", } // AddImplicitSlots adds implicitly defined slots to a given snap.
diff --git a/snap/implicit_test.go b/snap/implicit_test.go
index e9c4b07..364a6ef 100644 --- a/snap/implicit_test.go +++ b/snap/implicit_test.go @@ -56,7 +56,7 @@ func (s *InfoSnapYamlTestSuite) TestAddImplicitSlotsOnClassic(c *C) { c.Assert(info.Slots["unity7"].Interface, Equals, "unity7") c.Assert(info.Slots["unity7"].Name, Equals, "unity7") c.Assert(info.Slots["unity7"].Snap, Equals, info) - c.Assert(info.Slots, HasLen, 29) + c.Assert(info.Slots, HasLen, 30) } func (s *InfoSnapYamlTestSuite) TestImplicitSlotsAreRealInterfaces(c *C) {
Давайте глянем, что у нас есть:
Теперь посмотрите как всё выглядело у меня. Можете заметить специфичный стиль сообщения в коммите. Все коммиты предворяются именем каталога где происходили изменения и через двоеточие суть изменения. Хотелось добавить развёрнутое описание, но изменения тривиальны.
Нужно запустить наш изменённый код через devtools. В данном каталоге вызываем - ./refresh-bits snapd setup run-snapd restore
Произойдёт:
Скрипт запросит у вас пароль, так как будут произведены операции, затрагивающие всю систему. Консоль будет заблокирована и ожидает нажатия Ctrl + C для прерывания работы. Не нажимайте прерывание пока не убедитесь, что ваши изменения работают.
Чтобы легко проверить наши изменения - sudo snap interfaces | grep hello
Почему sudo? Это из-за особенности работы refresh-bits. В обычной жизни sudo не нужно. Работает?
Давайте потытожим что сделано:
Теперь давайте добавим функционал нашему интерфейсу, чтобы предоставить дополнительные разрешения через него. Напоминаю из Анатомия интерфейса snappy, что вы можете свободно связывать дополнительные сниппеты безопасности, которые инкапсулируют разрешения сниппетов (permanent или connection-based) у slots и plugs.
Интерфейсы позволяют передать дополнительную информацию конкретной части системы безопасности, чтобы конкретное приложение могло делать больше дозволенного. Эта дополнительная информация обменивается в виде сниппетов. Сниппеты - это биты нетипизированных данных, блобы, последовательность байтов.
Сниппеты permanent существуют всегда пока существует slots или plugs. Как правило, такое используется обычно в snap (но не в core snap), которые предоставляют сервис. Примерами являются: network-manager, modem-manager, docker, lxd, x11. Предоставленные разрешения позволят сервису работать, создав канал коммуникации (с сокетом, именем на шине DBus), чтобы клиенты смогли подсоединится.
Сниппеты connection-based применяются только в момент соединения между slots и plugs (interface connection). Эти разрешения выдаются клиенту и, как правило, включают в себя вызов IPC и разрешение на открытие сокета (типа X11 сокета) или на использование объекта/метода DBus.
Особый класс соединений сниппетов существует для вещей, которые никак не связаны "клиент разговаривает с сервером", а скорее связан "программа использует службы ядра". Ярким примером является network interface, который даёт доступ к системным вызовам, касающихся сети. В данный момент мы рассмотрим этот последний случай и через интерфейс hello дадим доступ к системному вызову, который обычно запрещён, - перезагрузка системы.
Нам нужна программа, которую упакуем в snap, и она вызовет перезагрузку системы. Вся программа на языке C
graceful-reboot.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/reboot.h> #include <linux reboot.h> #include <errno.h> int main() { sync(); if (reboot(LINUX_REBOOT_CMD_RESTART) != 0) { switch (errno) { case EPERM: printf("Insufficient permissions to reboot the system\n"); break; default: perror("reboot()"); break; } return EXIT_FAILURE; } printf("Reboot requested\n"); return EXIT_SUCCESS; }
Makefile
CFLAGS += -Wall .PHONY: all all: graceful-reboot .PHONY: clean clean: rm -f graceful-reboot graceful-reboot: graceful-reboot.c .PHONY: install install: graceful-reboot install -d $(DESTDIR)/usr/bin install -m 0755 graceful-reboot $(DESTDIR)/usr/bin/graceful-reboot
snapcraft.yaml
name: graceful-reboot version: 1 summary: Reboots the system gracefully description: | This snap contains a graceful-reboot application that requests the system to reboot by talking to the init daemon. The application uses a custom "hello" interface that is developed as a part of a tutorial. confinement: strict apps: graceful-reboot: command: graceful-reboot plugs: [hello] parts: main: plugin: make source: .
Теперь у нас есть всё что нужно. Можно использовать snapcraft и, скомпилировав программу, упаковать её в пакет снап. Отметьте, что режим ограничения указан строгим - confinement: strict. Это говорит snapcraft'у и snapd'у что для программы не отключать песочницу через режим devmode. Цель как раз в этом и состоит, что не давать программе поблажек и она сможет сделать перезагрузку хостовой системы, благодаря нашему интерфейсу.
snapcraft; sudo snap install ./graceful-reboot_1_amd64.snap
Готовы? Не бойтесь, система не будет перезагружена .
graceful-reboot
Bad system call
Ага! Нужно кое-что подправить. Как было описано в Среда выполнения snap, существует такая часть песочницы как Seccomp, которая блокирует системные вызовы, которые явно не разрешены. Можно глянуть системный журнал и увидим сам факт блокировки.
В современных Ubuntu вызовите - journalctl -n 100
или по старинке - grep -F audit /var/log/syslog|tail
sie 10 09:47:02 x200t audit[13864]: SECCOMP auid=1000 uid=1000 gid=1000 ses=2 pid=13864 comm="graceful-reboot" exe="/snap/graceful-reboot/x1/usr/bin/graceful-reboot" sig=31 arch=c000003e syscall=169 compat=0 ip=0x7f7ef30dcfd6 code=0x0
Как расшифровать этот код? Ключевая информация здесь номер системного вызова. В данном случае - 169.
scmp_sys_resolver 169
reboot
Бинго! Наш случай прост и очевиден, хотя часто имена функций в библиотеках не всегда так явно связаны с именем системного вызова.
Давайте изменим наш интерфейс и сделаем его более функциональным. Нужно переименовать его из hello в reboot. Это повлияет на имя файла, имя типа (type) и строку, возвращающейся из Name(). Мы дадим разрешения для plug при факте его соединения через бэкенд seccomp и позволим использовать системный вызов reboot. Для закрепления материала лучше самому попробовать внести изменения или воспользоваться патчем.
Самое главное что метод ConnectedPlugSnippet, возвращает, когда его спрашивают про SecuritySecComp, что имя системного вызова reboot разрешено.
diff --git a/interfaces/builtin/reboot.go b/interfaces/builtin/reboot.go
index 91962e1..ba7a9e3 100644 --- a/interfaces/builtin/reboot.go +++ b/interfaces/builtin/reboot.go @@ -91,7 +91,7 @@ func (iface *RebootInterface) PermanentSlotSnippet(slot *interfaces.Slot, securi func (iface *RebootInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { switch securitySystem { case interfaces.SecurityAppArmor: return nil, nil case interfaces.SecuritySecComp: - return nil, nil + return []byte(`reboot`), nil case interfaces.SecurityDBus:
Теперь нужно вернуться к Терминалу и, прервав refresh-bits, заново его стартануть, чтобы собрать новую локальную версию snapd. Если теперь спросить об известных интерфейсах, то можно увидеть наш обновлённый интерфейс reboot.
Теперь нужно обновить graceful-reboot и его snapcraft.yaml
apps: graceful-reboot: command: graceful-reboot plugs: [reboot]
Собираем программу в обновлённый snap пакет и переустанавливаем его. Если теперь спросить snapd про интерфейсы, то вы увидите, что core snap предоставляет слот reboot и пакет graceful-reboot имеет plug reboot, но они не соединены.
Давайте соединим - sudo snap connect graceful-reboot:reboot ubuntu-core:reboot
И проверим факт соединения - sudo snap interfaces | grep reboot
:reboot graceful-reboot
Успешно! Теперь прежде чем начнём пробовать в работе наш интерфейс, глянем на созданный для нас профиль seccomp. Профиль является производным от базового шаблона seccomp и набором от plugs, slots и соединений данного snap пакета. Каждое приложение в snap идёт со своим профилем, который можно глянуть для нашего примера по адресу /var/lib/snapd/seccomp/profiles/snap.graceful-reboot.graceful-reboot
grep reboot /var/lib/snapd/seccomp/profiles/snap.graceful-reboot.graceful-reboot
reboot
Есть несколько моментов, которые стоит отметить:
apparmor_parser -r /path/to/the/profile
Ну и как заработало? Пробуем снова
graceful-reboot
Insufficient permissions to reboot the system
Процесс не убивают, но перезагрузку не дают выполнить. Если глянуть в системный журнал, то там будет подсказка
sie 10 10:25:55 x200t kernel: audit: type=1400 audit(1470817555.936:47): apparmor="DENIED" operation="capable" profile="snap.graceful-reboot.graceful-reboot" pid=14867 comm="graceful-reboot" capability=22 capname="sys_boot"
Вмешался AppArmor и сказал, что нам нужна возможность sys_boot. Придётся изменить код интерфейса для реализации требуемого и указать в нужном синтаксисе - "capability sys_boot," . Обратите внимание на запятую, которую нужно указывать. Давайте патчить наш интерфейс и перезапускать refresh-bits.
diff --git a/interfaces/builtin/reboot.go b/interfaces/builtin/reboot.go
index 91962e1..ba7a9e3 100644 --- a/interfaces/builtin/reboot.go +++ b/interfaces/builtin/reboot.go @@ -91,7 +91,7 @@ func (iface *RebootInterface) PermanentSlotSnippet(slot *interfaces.Slot, securi func (iface *RebootInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { switch securitySystem { case interfaces.SecurityAppArmor: - return nil, nil + return []byte(`capability sys_boot,`), nil case interfaces.SecuritySecComp: return []byte(`reboot`), nil case interfaces.SecurityDBus:
Внимание! Следующая команда перезагрузит вашу систему. sudo /snap/bin/graceful-reboot
Так как такие интерфейсы, как наш reboot, встречаются часто, то для них в snapd можно указать требуемое в более краткой форме. Абсолютно идетичный интерфейс можно было указать в коротеньком сниппете.
// NewRebootInterface returns a new "reboot" interface. func NewRebootInterface() interfaces.Interface { return &commonInterface{ name: "reboot", connectedPlugAppArmor: `capability sys_boot,`, connectedPlugSecComp: `reboot`, reservedForOS: true, } }
Теперь везде где было &RebootInterface{} можно указать NewRebootInterface(). Сокращение позволяет легче читать код и передать смысл и цель интерфейса.
Оглавление цикла статей про Snappy.
Предыдущая статья Среда выполнения snap.