В этом материале будет рассмотрено, как запускать приложение .NET Core / .NET 5 в качестве сервиса под Linux. Мы воспользуемся Systemd, чтобы интегрировать наше приложение с операционной системой, научимся запускать и останавливать наш сервис, а также получать от него логи.
Чтобы организовать атаку на цепочку поставок при помощи .NET, мне потребовалось настроить DNS-сервер, который перехватывал бы те хост-имена, которые ко мне направляются. Давайте возьмём этот кейс для примера.
❯ Создание .NET-приложения, которое будет работать как сервис
.NET-сервису потребуется использовать модель хостинга
Microsoft.Extensions.Hosting
, применяемую в Microsoft. Так мы обеспечим работоспособность любого приложения ASP.NET Core, а также проектов, выполняемых с применением шаблона Worker Service (dotnet new worker
). Здесь будем работать с Rider.Далее потребуется установить пакет NuGet:
Microsoft.Extensions.Hosting.Systemd
. Этот пакет предоставляет хостинговую инфраструктуру .NET для сервисов Systemd. Иными словами, в нём содержится инфраструктура, необходимая для работы с демоном Systemd под Linux.Осталось сделать ещё одну вещь: зарегистрировать в нашем приложении хостинговые расширения Systemd. В
Program.cs
понадобится добавить .UseSystemd()
к сборщику хоста:public class Program
{
// ...
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSystemd() // add this
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker>();
});
}
Когда всё это настроено, наше приложение может работать как сервис Systemd под Linux! Ну, почти… Осталось сделать ещё две вещи:
- Написать приложение
- Зарегистрировать его для работы с Systemd
Начнём с первого пункта и реализуем DNS-сервер, который будем использовать в качестве сервиса.
❯ Реализуем сервер .NET DNS в качестве сервиса
DNS-сервер, рассмотренный здесь, пишется на .NET с применением отличного DNS-пакета от Мирзы Капетановича, а далее при помощи Systemd размещается на Linux как сервис.
Давайте соберём простой DNS-сервис, возвращающий актуальное время как запись в формате
TXT
.Примечание: можете пропустить этот раздел, если просто хотите изучить, как регистрировать приложения .NET в качестве сервисов Systemd.
Сперва установим пакет
DNS
. В нём содержится почти готовый DNS-сервер, так что нам понадобится всего лишь записать его код и реализовать серверную логику.Класс
Worker
, созданный на основе шаблона worker service (сервис-исполнитель) – хорошая отправная точка. В нём можно применить инъекцию конструктора, чтобы получить доступ ко всем сервисам, имеющимся в вашем приложении. Мы воспользуемся интерфейсом ILogger (уже имеющимся в шаблоне) и IConfiguration
, чтобы получить доступ к appsettings.json
/переменным окружения/аргументам командной строки. Здесь мы наследуем BackgroundService
, в котором есть метод ExecuteAsync
, именно этот метод и отвечает за выполнение нашего сервиса. Вот так выглядит костяк Worker
:public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly IConfiguration _configuration;
public Worker(ILogger<Worker> logger, IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// ... здесь идёт вся логика ...
}
}
Теперь давайте напишем сам DNS-сервер. В методе ExecuteAsync добавим следующий код:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var server = new DnsServer(new SampleRequestResolver());
server.Listening += (_, _) => _logger.LogInformation("DNS server is listening...");
server.Requested += (_, e) => _logger.LogInformation("Request: {Domain}", e.Request.Questions.First().Name);
server.Errored += (_, e) =>
{
_logger.LogError(e.Exception, "An error occurred");
if (e.Exception is ResponseException responseError)
{
_logger.LogError("Response: {Response}", responseError.Response);
}
};
await Task.WhenAny(new[]
{
server.Listen(
port: int.TryParse(_configuration["Port"], out var port) ? port : 53531,
ip: IPAddress.Any),
Task.Delay(-1, stoppingToken)
});
}
А теперь объясню развёрнуто:
- Мы настраиваем
DnsServer
, который будет разрешать записи при помощиSampleRequestResolver
(его нам ещё потребуется собрать). - Мы подписываемся на некоторые события, предоставляемые
DnsServer
, так что сможем просматривать логи входящих запросов и сразу замечать, не пошло ли что-нибудь не так. - Наконец, начинаем слушать входящие запросы.
Для
ExecuteAsync
мы передаём CancellationToken
, который задействуется, когда Systemd прикажет нашему сервису завершиться. Поскольку сама библиотека DNS не поддерживает CancellationToken
, я воспользуюсь здесь Task.WhenAny
, чтобы запустить DNS-сервер. Так мы сможем завершить наш сервис, как только будет затребован его сброс.Сам сервер будет слушать порт, указанный в конфигурации. Когда этот порт прямо не указан, по умолчанию задаётся
53531
.И ещё: тот самый класс
SampleRequestResolver
. Он должен реализовывать интерфейс IRequestResolver
и собирать ответы на входящие DNS-запросы. Поскольку эта тема выходит за рамки данного поста, просто покажу быструю реализацию, которая возвращает текущие значения даты и времени для любой запрошенной TXT-записи. Для всех прочих запросов возвращается ошибка:public class SampleRequestResolver : IRequestResolver
{
public Task<IResponse> Resolve(IRequest request, CancellationToken cancellationToken = new CancellationToken())
{
IResponse response = Response.FromRequest(request);
foreach (var question in response.Questions)
{
if (question.Type == RecordType.TXT)
{
response.AnswerRecords.Add(new TextResourceRecord(
question.Name, CharacterString.FromString(DateTime.UtcNow.ToString("O"))));
}
else
{
response.ResponseCode = ResponseCode.Refused;
}
}
return Task.FromResult(response);
}
}
Попробуйте выполнить сервис на локальной машине и убедитесь – он работает! Его удобно протестировать при помощи таких инструментов как
nslookup
и dig
.Посмотрите README.md проекта DNS – там есть и другие примеры. Будьте осторожны, чтобы случайно не собрать открытый DNS-преобразователь, если исследуете, как обрабатывать вашу собственную DNS.
❯ Пишем конфигурацию сервисного блока
Давайте развернём наш сервис на Linux! Systemd использует файлы конфигурации сервисного блока, в которых определяется, что делает сервис, должен ли он перезапускаться, и всё подобное.
Вам потребуется создать файл
.service
на той машине с Linux, на которой вы собираетесь зарегистрировать и запускать сервис. В простейшей форме данный сервисный файл выглядит так:[Unit]
Description=DNS Server
[Service]
Type=notify
ExecStart=/usr/sbin/DnsServer --port=53
[Install]
WantedBy=multi-user.target
В разделе
[Unit]
содержится самая общая информация о нашем приложении. Systemd может выполнять не только сервисы, и [Unit]
– это общий раздел для всех типов приложений, которые можно так запускать. Я добавил только описание (Description
), но здесь доступны и многие другие опции.В разделе
[Service]
определяются детали, касающиеся нашего приложения. Для приложений .NET тип Type
будет notify
, так, что мы сможем уведомлять Systemd, когда хост начинает или завершает работу. За всё это отвечает пакет Microsoft.Extensions.Hosting.Systemd.
В ExecStart
определяется путь к двоичному файлу, используемому для запуска нашего сервиса. В показанном далее примере я воспользуюсь самодостаточным .NET-приложением и через аргумент командной строки сообщу ему, какой порт слушать.Наконец, в разделе
[Install]
определяется, в каких операционных системах можно будет запускать наш сервис. В данном случае выставлено значение multi-user.target
, при котором мы можем запускать сервис всякий раз, когда находимся в многопользовательской среде (то есть, почти всегда). Здесь также можно было бы задать graphical.target, и тогда для запуска этого сервиса у вас должна быть загружена графическая среда.❯ Создаём самодостаточное .NET-приложение
Сам наш сервис должен быть доступен на машине с Linux, выбранной в качестве платформы, и на ней мы определим, где Systemd может найти его (это делается при помощи
ExecStart
): /usr/sbin/DnsServer
.Последнее, что нам остаётся сделать – развернуть .NET на целевой машине. Поэтому я решил собрать самодостаточное приложение, представляющее собой единый исполняемый файл – в нём содержится и среда выполнения .NET, и все необходимые зависимости. Для сборки приложения воспользуюсь следующей командой:
dotnet publish -c Release -r linux-x64 --self-contained=true -p:PublishSingleFile=true -p:GenerateRuntimeConfigurationFiles=true -o artifacts
Так создаётся исполняемый файл DnsServer размером примерно 62 МБ (внём содержится всё, что нам нужно от среды выполнения .NET). Скопируйте его в
/usr/sbin/DnsServer
на машине с Linux и убедитесь, что он поддаётся выполнению (sudo chmod 0755 /usr/sbin/DnsServer
).❯ Установка и запуск сервиса под Linux
Созданный нами файл
.service
(я назвал его dnsserver.service
) должен находиться в каталоге /etc/systemd/system/
на той машине Linux, на которой мы собираемся разворачивать сервис.Далее выполните следующую команду, чтобы Systemd загрузил этот новый конфигурационный файл:
sudo systemctl daemon-reload
На данном этапе мы уже должны быть в состоянии посмотреть статус нашего DNS-сервера, работающего как сервис:
sudo systemctl status dnsserver.service
Упомяну и несколько других команд, которые могут вам пригодиться:
- Для запуска и остановки сервиса:
sudo systemctl start dnsserver.service
sudo systemctl stop dnsserver.service
sudo systemctl restart dnsserver.service
- Чтобы убедиться, что сервис запускается одновременно с той машиной, на которой стоит Linux:
sudo systemctl enable dnsserver.service
Поздравляю, теперь у нас есть приложение .NET, работающее как сервис на Linux!
❯ Что происходит в моём сервисе?
Итак, сервис работает, но ведь ещё и интересно узнать, что он делает. Можно просмотреть самые свежие записи логов, которые выдаёт наше приложение. Для этого воспользуемся следующей командой:
sudo systemctl status dnsserver.service
Итак, мы видим, что сервис работает, можем узнать детали о процессе с заданным id, а также изучить последние записи логов.
Если логи вас очень интересуют, то при помощи journalctl можно вывести все логи для блока (-u) – это и есть наш DNS-сервер:
sudo journalctl -u dnsserver.service
В данном случае круто, что благодаря пакету Microsoft.Extensions.Hosting.Systemd мы также получаем здесь цветовые коды, а также данные об уровне серьёзности по каждой записи в логах:
❯ Заключение
В этом посте было рассмотрено, как использовать приложение .NET в качестве сервиса Systemd под Linux. Для этого мы воспользовались пакетом
Microsoft.Extensions.Hosting.Systemd
. Правда, мы только минимально познакомились с темой. Есть ещё множество опций, которые можно сообщить Systemd, ещё можно фильтровать уровни логов при помощи journalctl, и делать многое другое. Рекомендую посмотреть статью Нильса Свимберге, если хотите подробнее изучить эту тему. Комментарии (13)
13werwolf13
10.09.2023 08:31скиньте эту статью кто нибудь разработчикам terraria, а то их dedicated server до сих пор не вкурсе что его надо запускать в виде демона в НЕинтерактивном режиме..
mayorovp
10.09.2023 08:31И у майнкрафта та же проблема.
Я как-то делал костыль на Rust, чтобы запускать такие программы неинтерактивно, перенаправляя поток ввода в unix сокет.
13werwolf13
10.09.2023 08:31+1сервер майнкрафта хотя бы работает в виде systemd службы без костылей, хотя и теряется возможность посылать ему команды. сервер террарии же просто валится, костыль для запуска его ввиде службы являет собой запуск внутри screen.
P.S.: за ваш костыль спасибо, посмотрю, хотя бы из чистого интерес
ArtLkv
10.09.2023 08:31А можно ли на .NET и C# сделать службу не для systemd, а для OpenRC или runit? Или Microsoft поддерживает решение лишь для systemd? Просто из интереса.
mayorovp
10.09.2023 08:31А что мешает? Вся поддержка systemd сводится к работе с механизмами сокетов (LISTEN_FDS) и sd_notify (NOTIFY_SOCKET).
У OpenRC и runit разве есть подобные особые механизмы? Насколько я знаю, их нет. Следовательно, для них и особой поддержки никакой не требуется.
mayorovp
Небольшое уточнение: не надо использовать PublishSingleFile, если только вам не требуется один файл любой ценой. Потому что в этом режиме приложение при запуске распаковывает необходимые нативные библиотеки во временную папку. Что сказывается, к примеру, на времени запуска.
s207883
Не приведет ли это к необходимости установки .net в систему?
stantum
Смотря как сбилдить
mayorovp
Нет, режим
--self-contained
означает распространение рантайма вместе с приложениемs207883
Благодарю
alan008
Они обещали, что в .net 7 или 8 этот режим будет работать получше, как минимум отсекать неиспользуемую часть рантайма.
mayorovp
Это же независимая фича, её можно будет включить (или выключить) отдельно. Надеюсь.
alan008
Да, отдельная.
<PublishTrimmed>true</PublishTrimmed>
Про само self-contained:
https://learn.microsoft.com/en-us/dotnet/core/deploying/#publish-self-contained
Про trim self-contained:
https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trim-self-contained