Авторские статьи

Создание своего первого интерфейса.


Многое было не понятно из первых четырёх статей, так как была одна теория. Теперь на практике мы создадим наш первый, простой интерфейс и пройдём весь путь от форка snapd под наши нужды до запуска программы, которая будет использовать новый интерфейс в форке.

Добавление нового hello интерфейса

Давайте оглядимся. Каждый раз, когда добавляется новый интерфейс, следующие файлы претерпевают изменения:

  • Файл interfaces/builtin/foo{,_test}.go содержит сам код интерфейса
  • Файл interfaces/builtin/all{,_test}.go содержит небольшие изменения, используемые при регистрации нового интерфейса

Кроме того, если слот интерфейса должен явно отражаться в 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
}
Подсказка: всякий раз когда вы делаете изменения в коде используйте go fmt для форматирования кода по стандартам языка Go. Проверки кода snappy не допустят ваши изменения, пока код не будет отформатирован правильно.

Теперь нужно сделать ещё одно изменение, чтобы о новом интерфейсе узнал 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) {

Давайте глянем, что у нас есть:

  • Первый патч добавляет интерфейс hello.
  • Второй патч регистрирует его в allInterfaces.
  • Третий патч добавляет неявный слот к core snap.

Теперь посмотрите как всё выглядело у меня. Можете заметить специфичный стиль сообщения в коммите. Все коммиты предворяются именем каталога где происходили изменения и через двоеточие суть изменения. Хотелось добавить развёрнутое описание, но изменения тривиальны.

Смотрим наш интерфейс в первый раз

Нужно запустить наш изменённый код через devtools. В данном каталоге вызываем - ./refresh-bits snapd setup run-snapd restore
Произойдёт:

  • snapd будет построен из вашего локального источника, корректно основываясь на $GOPATH
  • подготовка к запуску локально созданного snapd
  • запуск локально созданного snapd
  • восстановится обычная версия snapd

Скрипт запросит у вас пароль, так как будут произведены операции, затрагивающие всю систему. Консоль будет заблокирована и ожидает нажатия Ctrl + C для прерывания работы. Не нажимайте прерывание пока не убедитесь, что ваши изменения работают.

Чтобы легко проверить наши изменения - sudo snap interfaces | grep hello Почему sudo? Это из-за особенности работы refresh-bits. В обычной жизни sudo не нужно. Работает?

Подсказка: Если у вас появились проблемы или не появился интерфейс hello, то скорее всего refresh-bits собирает ванильную версию snapd из апстрима. Перейдите в $GOPATH/src/github.com/snapcore/snapd и убедитесь что там ваш форк. Если это не так, то удалите каталог и поместите туда ваш форк и пробуйте снова.

Давайте потытожим что сделано:

  • Добавлен новый интерфейс в interfaces/builtin/hello.go
  • Зарегистрирован новый интерфейс в списке allInterfaces
  • Реализовано, что snapd добавляет неявный (описанный внутри) слот для core snap.
  • Использовали refresh-bits для запуска своей копии snapd.

Предоставление разрешений через интерфейсы

Теперь давайте добавим функционал нашему интерфейсу, чтобы предоставить дополнительные разрешения через него. Напоминаю из Анатомия интерфейса 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 дадим доступ к системному вызову, который обычно запрещён, - перезагрузка системы.

graceful-reboot snap

Нам нужна программа, которую упакуем в 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

Подсказка: для makefile важна разница между табуляцией и пробелами. При копипасте убедитесь, что табуляция сохранилась в разделе install
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^Hreboot interface

Давайте изменим наш интерфейс и сделаем его более функциональным. Нужно переименовать его из 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

Есть несколько моментов, которые стоит отметить:

  • Профиля безопасности являются производными от интерфейсов и изменяются только в процессе установления соединения, первоначальной установке пакета snap и его обновления.
  • На практике нужно было сделать или disconnect / connect изменённого интерфейса hello или переустанавливать пакет snap, что более предпочтительно.
  • Snapd запоминает соединения, которые были сделаны в явном виде, и восстанавливает их при обновлении snap пакета. Если была осуществлена переименовка интерфейса во время его работы, то он будет ругаться в системный журнал о том, что невозможно сделать переподсоединение, так как интерфейса hello уже не существует. Заставить забыть можно только удалением и установкой заново snap пакета.
  • Для экспериментов вы можете напрямую подправить профиль seccomp для данной программы. Для примера вы можете вписать дополнительные вызовы, которые будут разрешены. Если результат вас устроит, то потом это можно будет закодировать в исходниках snapd.
  • Для экспериментов можно так же напрямую подправить профиля AppArmor, но не забывайте перезагружать профиль, чтобы изменения стали известны ядру linux. 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 с полным путём к бинарнику, так как это баг sudo, который не принимает /snap/bin/

Внимание! Следующая команда перезагрузит вашу систему. 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.

Дата последней правки: 2023-12-27 14:28:42

RSS vasilisc.com   


Разделы

Главная
Новости
Ворох бумаг
Видео Linux
Игры в Linux
Безопасность
Статьи об Astra Linux
Статьи о FreeBSD
Статьи об Ubuntu
Статьи о Snappy
Статьи об Ubuntu Phone
Статьи о Kubuntu
Статьи о Xubuntu
Статьи о Lubuntu
Статьи об Open Source
Карта сайта