mirror of
https://github.com/systemd/systemd.git
synced 2026-11-06 05:17:24 +00:00
report: add simple geoip metric (#43804)
When collecting reports it's sometimes interesting to know where the node reporting is actually located. Add a simple report source that adds that, backed by a simple geoip HTTP request. This is opt-in, due to privacy and so on.
This commit is contained in:
19 files changed
+1458
-1
No files matched your search
@@ -928,3 +928,8 @@ Tools using the Varlink protocol (such as `varlinkctl`) or sd-bus (such as
|
||||
notification carries no list of updated resources. This is useful to
|
||||
unconditionally trigger follow-up work such as relinking a kernel or
|
||||
recomputing a TPM policy.
|
||||
|
||||
`systemd-report-geoip`:
|
||||
|
||||
* `$SYSTEMD_REPORT_GEOIP_ENDPOINT` – takes an HTTP endpoint
|
||||
URL. Overrides the GeoIP endpoint to use.
|
||||
@@ -0,0 +1,101 @@
|
||||
<?xml version='1.0'?>
|
||||
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
|
||||
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
|
||||
<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
|
||||
|
||||
<refentry id="report-geoip.conf" conditional="ENABLE_REPORT HAVE_LIBCURL"
|
||||
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||
<refentryinfo>
|
||||
<title>report-geoip.conf</title>
|
||||
<productname>systemd</productname>
|
||||
</refentryinfo>
|
||||
|
||||
<refmeta>
|
||||
<refentrytitle>report-geoip.conf</refentrytitle>
|
||||
<manvolnum>5</manvolnum>
|
||||
</refmeta>
|
||||
|
||||
<refnamediv>
|
||||
<refname>report-geoip.conf</refname>
|
||||
<refname>report-geoip.conf.d</refname>
|
||||
<refpurpose>GeoIP report metrics configuration file</refpurpose>
|
||||
</refnamediv>
|
||||
|
||||
<refsynopsisdiv>
|
||||
<para><simplelist>
|
||||
<member><filename>/etc/systemd/report-geoip.conf</filename></member>
|
||||
<member><filename>/run/systemd/report-geoip.conf</filename></member>
|
||||
<member><filename>/usr/local/lib/systemd/report-geoip.conf</filename></member>
|
||||
<member><filename>/usr/lib/systemd/report-geoip.conf</filename></member>
|
||||
<member><filename>/etc/systemd/report-geoip.conf.d/*.conf</filename></member>
|
||||
<member><filename>/run/systemd/report-geoip.conf.d/*.conf</filename></member>
|
||||
<member><filename>/usr/local/lib/systemd/report-geoip.conf.d/*.conf</filename></member>
|
||||
<member><filename>/usr/lib/systemd/report-geoip.conf.d/*.conf</filename></member>
|
||||
</simplelist></para>
|
||||
</refsynopsisdiv>
|
||||
|
||||
<refsect1>
|
||||
<title>Description</title>
|
||||
|
||||
<para>This file configures the behavior of
|
||||
<citerefentry><refentrytitle>systemd-report-geoip@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
|
||||
a metrics source for
|
||||
<citerefentry><refentrytitle>systemd-report</refentrytitle><manvolnum>1</manvolnum></citerefentry>
|
||||
reporting the approximate geographic location of the system.</para>
|
||||
</refsect1>
|
||||
|
||||
<xi:include href="standard-conf.xml" xpointer="main-conf" />
|
||||
|
||||
<refsect1>
|
||||
<title>Options</title>
|
||||
|
||||
<para>All options are configured in the [GeoIP] section:</para>
|
||||
|
||||
<variablelist class='config-directives'>
|
||||
|
||||
<varlistentry>
|
||||
<term><varname>Endpoint=</varname></term>
|
||||
|
||||
<listitem><para>The HTTP or HTTPS URL of the geolocation web service to query. The service must
|
||||
answer a GET request with a JSON object in the format of the
|
||||
<ulink url="https://reallyfreegeoip.org/">reallyfreegeoip.org</ulink> service, describing the
|
||||
location of the address the request originates from. The field names used by
|
||||
<ulink url="https://ipinfo.io/">ipinfo.io</ulink> (i.e. <literal>country</literal>,
|
||||
<literal>region</literal>, <literal>continent</literal> in place of <literal>country_name</literal>,
|
||||
<literal>region_name</literal>, <literal>continent_name</literal>) are understood too. Fields the
|
||||
service does not provide are not reported. If assigned the empty string, the default is
|
||||
restored. The default is chosen at compile time.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v263"/>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><varname>RefreshSec=</varname></term>
|
||||
|
||||
<listitem><para>Controls how often the geolocation data is refreshed from the endpoint, at most.
|
||||
Takes a time span, see
|
||||
<citerefentry><refentrytitle>systemd.time</refentrytitle><manvolnum>7</manvolnum></citerefentry> for
|
||||
details. The acquired data is cached locally, and the endpoint is only contacted again once the
|
||||
cached data is older than the specified time. If set to zero, the endpoint is contacted on every
|
||||
request. If set to <literal>infinity</literal>, the endpoint is contacted only once, and the cached
|
||||
data is used from then on. Defaults to <literal>30min</literal>.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v263"/>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
|
||||
<para>Use <programlisting>systemd-analyze cat-config systemd/report-geoip.conf</programlisting>
|
||||
to display the full config.</para>
|
||||
</refsect1>
|
||||
|
||||
<refsect1>
|
||||
<title>See Also</title>
|
||||
<para><simplelist type="inline">
|
||||
<member><citerefentry><refentrytitle>systemd-report-geoip@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
|
||||
<member><citerefentry><refentrytitle>systemd-report</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
|
||||
</simplelist></para>
|
||||
</refsect1>
|
||||
|
||||
</refentry>
|
||||
@@ -83,6 +83,10 @@ manpages = [
|
||||
['poweroff', '8', ['halt', 'reboot'], ''],
|
||||
['pstore.conf', '5', ['pstore.conf.d'], 'ENABLE_PSTORE'],
|
||||
['repart.d', '5', [], 'ENABLE_REPART'],
|
||||
['report-geoip.conf',
|
||||
'5',
|
||||
['report-geoip.conf.d'],
|
||||
'ENABLE_REPORT HAVE_LIBCURL'],
|
||||
['resolvectl', '1', ['resolvconf'], 'ENABLE_RESOLVE'],
|
||||
['resolved.conf', '5', ['resolved.conf.d'], 'ENABLE_RESOLVE'],
|
||||
['run0', '1', [], ''],
|
||||
@@ -1225,6 +1229,10 @@ manpages = [
|
||||
'8',
|
||||
['systemd-report-files', 'systemd-report-files.socket'],
|
||||
'ENABLE_REPORT'],
|
||||
['systemd-report-geoip@.service',
|
||||
'8',
|
||||
['systemd-report-geoip', 'systemd-report-geoip.socket'],
|
||||
'ENABLE_REPORT HAVE_LIBCURL'],
|
||||
['systemd-report-hwmon@.service',
|
||||
'8',
|
||||
['systemd-report-hwmon', 'systemd-report-hwmon.socket'],
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
<?xml version="1.0"?> <!--*-nxml-*-->
|
||||
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
|
||||
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
|
||||
<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
|
||||
|
||||
<refentry id="systemd-report-geoip_.service" conditional="ENABLE_REPORT HAVE_LIBCURL"
|
||||
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||
|
||||
<refentryinfo>
|
||||
<title>systemd-report-geoip@.service</title>
|
||||
<productname>systemd</productname>
|
||||
</refentryinfo>
|
||||
|
||||
<refmeta>
|
||||
<refentrytitle>systemd-report-geoip@.service</refentrytitle>
|
||||
<manvolnum>8</manvolnum>
|
||||
</refmeta>
|
||||
|
||||
<refnamediv>
|
||||
<refname>systemd-report-geoip@.service</refname>
|
||||
<refname>systemd-report-geoip.socket</refname>
|
||||
<refname>systemd-report-geoip</refname>
|
||||
<refpurpose>Report the approximate geographic location of the system as a metric</refpurpose>
|
||||
</refnamediv>
|
||||
|
||||
<refsynopsisdiv>
|
||||
<para><filename>systemd-report-geoip@.service</filename></para>
|
||||
<para><filename>systemd-report-geoip.socket</filename></para>
|
||||
<para><filename>/usr/lib/systemd/systemd-report-geoip</filename></para>
|
||||
</refsynopsisdiv>
|
||||
|
||||
<refsect1>
|
||||
<title>Description</title>
|
||||
|
||||
<para><command>systemd-report-geoip@.service</command> is a metrics source for
|
||||
<citerefentry><refentrytitle>systemd-report</refentrytitle><manvolnum>1</manvolnum></citerefentry>. It
|
||||
reports the approximate geographic location of the system, as determined by a public IP geolocation
|
||||
web service from the public IP address the system connects to it with. It implements the
|
||||
<constant>io.systemd.Metrics</constant> Varlink interface on a socket linked into the
|
||||
<filename>/run/systemd/report/</filename> directory.</para>
|
||||
|
||||
<para>The geolocation data is acquired via HTTP from the endpoint configured in
|
||||
<citerefentry><refentrytitle>report-geoip.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry>. The
|
||||
acquired data is cached, and only refreshed once the cache is older than the configured
|
||||
<varname>RefreshSec=</varname> interval. If the endpoint cannot be reached, the cached data is reported
|
||||
even if it is older than that. If no data is available at all, no metrics are reported.</para>
|
||||
</refsect1>
|
||||
|
||||
<refsect1>
|
||||
<title>Metrics</title>
|
||||
|
||||
<para>The following metrics are reported, all in the <literal>io.systemd.GeoIP.</literal> namespace. Fields
|
||||
the geolocation service does not provide are not reported.</para>
|
||||
|
||||
<variablelist>
|
||||
<varlistentry>
|
||||
<term><constant>io.systemd.GeoIP.PublicAddress</constant></term>
|
||||
<listitem><para>The public IP address of the system, as seen by the geolocation service. The address
|
||||
is validated and reported in normalized form.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v263"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><constant>io.systemd.GeoIP.ASN</constant></term>
|
||||
<term><constant>io.systemd.GeoIP.ASName</constant></term>
|
||||
<term><constant>io.systemd.GeoIP.ASDomain</constant></term>
|
||||
<listitem><para>The Autonomous System Number (e.g. <literal>AS3320</literal>) of the network the public
|
||||
IP address belongs to, and the name and domain of the organization operating it.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v263"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><constant>io.systemd.GeoIP.ContinentCode</constant></term>
|
||||
<term><constant>io.systemd.GeoIP.ContinentName</constant></term>
|
||||
<listitem><para>The code (e.g. <literal>EU</literal>) and the name of the continent the system is
|
||||
located in.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v263"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><constant>io.systemd.GeoIP.CountryCode</constant></term>
|
||||
<term><constant>io.systemd.GeoIP.CountryName</constant></term>
|
||||
<listitem><para>The ISO 3166-1 alpha-2 code and the name of the country the system is located
|
||||
in.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v263"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><constant>io.systemd.GeoIP.RegionCode</constant></term>
|
||||
<term><constant>io.systemd.GeoIP.RegionName</constant></term>
|
||||
<listitem><para>The code and name of the region (state, province, …) the system is located
|
||||
in.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v263"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><constant>io.systemd.GeoIP.City</constant></term>
|
||||
<term><constant>io.systemd.GeoIP.ZipCode</constant></term>
|
||||
<listitem><para>The city and postal code of the area the system is located in.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v263"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><constant>io.systemd.GeoIP.Timezone</constant></term>
|
||||
<listitem><para>The IANA time zone name of the area the system is located in.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v263"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><constant>io.systemd.GeoIP.Latitude</constant></term>
|
||||
<term><constant>io.systemd.GeoIP.Longitude</constant></term>
|
||||
<listitem><para>The geographic coordinates of the system's location, in decimal degrees.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v263"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><constant>io.systemd.GeoIP.Timestamp</constant></term>
|
||||
<listitem><para>The time at which the reported data was acquired from the geolocation service, in
|
||||
microseconds since the UNIX epoch (<constant>CLOCK_REALTIME</constant>). When the data is served from
|
||||
the cache this is the time the cache was last refreshed, not the time of the report.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v263"/></listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
</refsect1>
|
||||
|
||||
<refsect1>
|
||||
<title>Privacy</title>
|
||||
|
||||
<para>Note that refreshing the geolocation data means contacting a third party web service, which
|
||||
thereby learns the public IP address of the system. The reported metrics identify the system's public IP
|
||||
address and approximate location, and hence are sensitive. Only enable this service if this is acceptable
|
||||
for the systems the reports are collected from.</para>
|
||||
</refsect1>
|
||||
|
||||
<!-- Note: we do not document the command line switches here. The systemd-report-geoip binary itself should
|
||||
not be invoked by users, but only as a service. Use systemd-report(1) to acquire metrics. -->
|
||||
|
||||
<refsect1>
|
||||
<title>See Also</title>
|
||||
<para><simplelist type="inline">
|
||||
<member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
|
||||
<member><citerefentry><refentrytitle>systemd-report</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
|
||||
<member><citerefentry><refentrytitle>report-geoip.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry></member>
|
||||
</simplelist></para>
|
||||
</refsect1>
|
||||
|
||||
</refentry>
|
||||
@@ -981,6 +981,9 @@ conf.set_quoted('DNS_SERVERS', dns_servers)
|
||||
ntp_servers = get_option('ntp-servers')
|
||||
conf.set_quoted('NTP_SERVERS', ntp_servers)
|
||||
|
||||
geoip_endpoint = get_option('geoip-endpoint')
|
||||
conf.set_quoted('GEOIP_ENDPOINT', geoip_endpoint)
|
||||
|
||||
default_locale = get_option('default-locale')
|
||||
conf.set_quoted('SYSTEMD_DEFAULT_LOCALE', default_locale)
|
||||
|
||||
@@ -3149,6 +3152,7 @@ summary({
|
||||
'default LLMNR mode' : default_llmnr,
|
||||
'default DNS servers' : dns_servers.split(' '),
|
||||
'default NTP servers' : ntp_servers.split(' '),
|
||||
'default GeoIP endpoint' : geoip_endpoint,
|
||||
'default net.naming_scheme= value' : default_net_naming_scheme,
|
||||
'default KillUserProcesses= value' : kill_user_processes,
|
||||
'default locale' : default_locale,
|
||||
|
||||
@@ -602,3 +602,7 @@ option('oss-fuzz', type : 'boolean', value : false,
|
||||
description : 'build against oss-fuzz')
|
||||
option('llvm-fuzz', type : 'boolean', value : false,
|
||||
description : 'build against LLVM libFuzzer')
|
||||
|
||||
option('geoip-endpoint', type : 'string',
|
||||
description : 'default URL of the IP geolocation web service queried by systemd-report-geoip',
|
||||
value : 'https://reallyfreegeoip.org/json/')
|
||||
@@ -107,7 +107,7 @@ _systemd_analyze() {
|
||||
|
||||
local CONFIGS='locale.conf systemd/bootchart.conf systemd/coredump.conf systemd/journald.conf
|
||||
systemd/journal-remote.conf systemd/journal-upload.conf systemd/logind.conf
|
||||
systemd/resolved.conf systemd/networkd.conf systemd/pstore.conf systemd/resolved.conf
|
||||
systemd/resolved.conf systemd/networkd.conf systemd/pstore.conf systemd/report-geoip.conf systemd/resolved.conf
|
||||
systemd/sleep.conf systemd/system.conf systemd/timedated.conf
|
||||
systemd/timesyncd.conf systemd/user.conf udev/udev.conf'
|
||||
|
||||
|
||||
@@ -176,6 +176,30 @@ int state_directory_generic(RuntimeScope scope, const char *suffix, char **ret)
|
||||
}
|
||||
}
|
||||
|
||||
int state_directory(RuntimeScope scope, const char *fallback_suffix, char **ret) {
|
||||
int r;
|
||||
|
||||
assert(ret);
|
||||
|
||||
/* Accept $STATE_DIRECTORY as authoritative, i.e. only works for our service's own state dir, as
|
||||
* fallback_suffix is ignored if $STATE_DIRECTORY is set.
|
||||
*
|
||||
* If $STATE_DIRECTORY is missing, apply the fallback suffix to /var/lib/, or $XDG_STATE_HOME if we
|
||||
* are in a user runtime scope.
|
||||
*
|
||||
* Return value indicates whether the suffix was applied or not. */
|
||||
|
||||
const char *e = secure_getenv("STATE_DIRECTORY");
|
||||
if (e)
|
||||
return strdup_to(ret, e);
|
||||
|
||||
r = state_directory_generic(scope, fallback_suffix, ret);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static const char* const user_data_unit_paths[] = {
|
||||
"/usr/local/lib/systemd/user",
|
||||
"/usr/local/share/systemd/user",
|
||||
|
||||
@@ -63,6 +63,7 @@ int runtime_directory(RuntimeScope scope, const char *fallback_suffix, char **re
|
||||
int runtime_directory_resolve(RuntimeScope scope, const char *suffix, const char *identifier, char **ret);
|
||||
int runtime_directory_make(RuntimeScope scope, const char *suffix, const char *identifier, char **ret);
|
||||
int state_directory_generic(RuntimeScope scope, const char *suffix, char **ret);
|
||||
int state_directory(RuntimeScope scope, const char *fallback_suffix, char **ret);
|
||||
|
||||
/* We don't treat /etc/xdg/systemd/ in these functions as the xdg base dir spec suggests because we assume
|
||||
* that is a link to /etc/systemd/ anyway. */
|
||||
|
||||
@@ -58,6 +58,17 @@ executables += [
|
||||
'report-files-server.c',
|
||||
),
|
||||
},
|
||||
libexec_template + {
|
||||
'name' : 'systemd-report-geoip',
|
||||
'sources' : files(
|
||||
'report-geoip.c',
|
||||
'report-geoip-server.c',
|
||||
),
|
||||
'dependencies' : [
|
||||
libcurl_cflags,
|
||||
],
|
||||
'conditions' : ['HAVE_LIBCURL'],
|
||||
},
|
||||
libexec_template + {
|
||||
'name' : 'systemd-report-hwmon',
|
||||
'sources' : files(
|
||||
@@ -101,3 +112,10 @@ executables += [
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
custom_target(
|
||||
input : 'report-geoip.conf.in',
|
||||
output : 'report-geoip.conf',
|
||||
command : [jinja2_cmdline, '@INPUT@', '@OUTPUT@'],
|
||||
install : conf.get('HAVE_LIBCURL') == 1 and install_sysconfdir_samples,
|
||||
install_dir : pkgconfigfiledir)
|
||||
@@ -0,0 +1,175 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include "sd-json.h"
|
||||
#include "sd-varlink.h"
|
||||
|
||||
#include "alloc-util.h"
|
||||
#include "build.h"
|
||||
#include "conf-parser.h"
|
||||
#include "dlopen-note.h"
|
||||
#include "log.h"
|
||||
#include "main-func.h"
|
||||
#include "report-geoip.h"
|
||||
#include "string-util.h"
|
||||
#include "time-util.h"
|
||||
#include "varlink-io.systemd.Metrics.h"
|
||||
#include "varlink-util.h"
|
||||
#include "verbs.h"
|
||||
#include "web-util.h"
|
||||
|
||||
#define REPORT_GEOIP_DEFAULT_REFRESH_USEC (30 * USEC_PER_MINUTE)
|
||||
|
||||
COMMAND(
|
||||
"systemd-report-geoip\0",
|
||||
"Report the approximate geographic location of the system as a metric.",
|
||||
.man_pages = "systemd-report-geoip@.service(8)\0",
|
||||
);
|
||||
|
||||
char *arg_endpoint = NULL;
|
||||
usec_t arg_refresh_usec = REPORT_GEOIP_DEFAULT_REFRESH_USEC;
|
||||
|
||||
STATIC_DESTRUCTOR_REGISTER(arg_endpoint, freep);
|
||||
|
||||
static int config_parse_endpoint(
|
||||
const char *unit,
|
||||
const char *filename,
|
||||
unsigned line,
|
||||
const char *section,
|
||||
unsigned section_line,
|
||||
const char *lvalue,
|
||||
int ltype,
|
||||
const char *rvalue,
|
||||
void *data,
|
||||
void *userdata) {
|
||||
|
||||
char **s = ASSERT_PTR(data);
|
||||
|
||||
assert(filename);
|
||||
assert(lvalue);
|
||||
assert(rvalue);
|
||||
|
||||
if (isempty(rvalue)) {
|
||||
*s = mfree(*s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!http_url_is_valid(rvalue)) {
|
||||
log_syntax(unit, LOG_WARNING, filename, line, 0,
|
||||
"%s= URL is not valid, ignoring: %s", lvalue, rvalue);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return free_and_strdup_warn(s, rvalue);
|
||||
}
|
||||
|
||||
static int parse_config(void) {
|
||||
static const ConfigTableItem items[] = {
|
||||
{ "GeoIP", "Endpoint", config_parse_endpoint, 0, &arg_endpoint },
|
||||
{ "GeoIP", "RefreshSec", config_parse_sec, 0, &arg_refresh_usec },
|
||||
{}
|
||||
};
|
||||
|
||||
return config_parse_standard_file_with_dropins(
|
||||
"systemd/report-geoip.conf",
|
||||
"GeoIP\0",
|
||||
config_item_table_lookup, items,
|
||||
CONFIG_PARSE_WARN,
|
||||
/* userdata= */ NULL);
|
||||
}
|
||||
|
||||
static int parse_environment(void) {
|
||||
|
||||
const char *d = secure_getenv("SYSTEMD_REPORT_GEOIP_ENDPOINT");
|
||||
if (!d)
|
||||
return 0;
|
||||
|
||||
if (isempty(d)) {
|
||||
arg_endpoint = mfree(arg_endpoint);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!http_url_is_valid(d)) {
|
||||
log_warning("$SYSTEMD_REPORT_GEOIP_ENDPOINT is not a valid URL, ignoring: %s", d);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return free_and_strdup_warn(&arg_endpoint, d);
|
||||
}
|
||||
|
||||
static int vl_server(void) {
|
||||
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *vs = NULL;
|
||||
int r;
|
||||
|
||||
r = varlink_server_new(&vs, /* flags= */ 0, /* userdata= */ NULL);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to allocate Varlink server: %m");
|
||||
|
||||
r = sd_varlink_server_add_interface(vs, &vl_interface_io_systemd_Metrics);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to add Varlink interface: %m");
|
||||
|
||||
r = sd_varlink_server_bind_method_many(
|
||||
vs,
|
||||
"io.systemd.Metrics.List", vl_method_list_metrics,
|
||||
"io.systemd.Metrics.Describe", vl_method_describe_metrics);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to bind Varlink methods: %m");
|
||||
|
||||
r = sd_varlink_server_loop_auto(vs);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to run Varlink event loop: %m");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_argv(int argc, char *argv[]) {
|
||||
int r;
|
||||
|
||||
assert(argc >= 0);
|
||||
assert(argv);
|
||||
|
||||
OptionParser opts = { argc, argv };
|
||||
|
||||
FOREACH_OPTION_OR_RETURN(c, &opts)
|
||||
switch (c) {
|
||||
OPTION_COMMON_HELP:
|
||||
return command_print_help();
|
||||
|
||||
OPTION_COMMON_VERSION:
|
||||
return version();
|
||||
|
||||
OPTION_COMMON_INTROSPECT_CLI:
|
||||
return introspect_cli(SD_JSON_FORMAT_OFF);
|
||||
}
|
||||
|
||||
if (option_parser_get_n_args(&opts) > 0)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
|
||||
"This program takes no arguments.");
|
||||
|
||||
r = sd_varlink_invocation(SD_VARLINK_ALLOW_ACCEPT);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m");
|
||||
if (r == 0)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
|
||||
"This program can only run as a Varlink service.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int run(int argc, char *argv[]) {
|
||||
int r;
|
||||
|
||||
LIBCURL_NOTE(required);
|
||||
|
||||
log_setup();
|
||||
|
||||
r = parse_argv(argc, argv);
|
||||
if (r <= 0)
|
||||
return r;
|
||||
|
||||
(void) parse_config();
|
||||
(void) parse_environment();
|
||||
|
||||
return vl_server();
|
||||
}
|
||||
|
||||
DEFINE_MAIN_FUNCTION(run);
|
||||
@@ -0,0 +1,686 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <math.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "sd-json.h"
|
||||
#include "sd-varlink.h"
|
||||
|
||||
#include "alloc-util.h"
|
||||
#include "curl-util.h"
|
||||
#include "fd-util.h"
|
||||
#include "fileio.h"
|
||||
#include "in-addr-util.h"
|
||||
#include "iovec-util.h"
|
||||
#include "json-util.h"
|
||||
#include "lock-util.h"
|
||||
#include "log.h"
|
||||
#include "metrics.h"
|
||||
#include "mkdir.h"
|
||||
#include "path-lookup.h"
|
||||
#include "path-util.h"
|
||||
#include "report-geoip.h"
|
||||
#include "stat-util.h"
|
||||
#include "string-util.h"
|
||||
#include "time-util.h"
|
||||
#include "version.h"
|
||||
#include "web-util.h"
|
||||
|
||||
#define METRIC_IO_SYSTEMD_GEOIP_PREFIX "io.systemd.GeoIP."
|
||||
|
||||
/* The cache lives below the service's StateDirectory= (as passed via $STATE_DIRECTORY), so that it survives
|
||||
* reboots, falling back to this path below /var/lib/ when started directly from the command line. All
|
||||
* instances of the socket activated service share it, hence access is serialized via a lock file next to it. */
|
||||
#define REPORT_GEOIP_STATE_DIR_SUFFIX "systemd/report-geoip"
|
||||
#define REPORT_GEOIP_CACHE_FILE_NAME "cache.json"
|
||||
|
||||
/* Upper bound on the size of the answer we accept from the server, and of the cache file we are willing to
|
||||
* read back. The actual answer is a couple of hundred bytes. */
|
||||
#define REPORT_GEOIP_ANSWER_MAX (64U * 1024U)
|
||||
|
||||
/* The overall Varlink call is bounded by the client's timeout (30s for systemd-report), hence keep the
|
||||
* network timeout well below that, so that we can still fall back to the cache in time. */
|
||||
#define REPORT_GEOIP_NETWORK_TIMEOUT_USEC (10 * USEC_PER_SEC)
|
||||
|
||||
/* Follow HTTP redirects, but only a few of them. */
|
||||
#define REPORT_GEOIP_MAX_REDIRECTS 5L
|
||||
|
||||
typedef enum GeoIPField {
|
||||
GEOIP_FIELD_IP,
|
||||
GEOIP_FIELD_ASN,
|
||||
GEOIP_FIELD_AS_NAME,
|
||||
GEOIP_FIELD_AS_DOMAIN,
|
||||
GEOIP_FIELD_CONTINENT_CODE,
|
||||
GEOIP_FIELD_CONTINENT_NAME,
|
||||
GEOIP_FIELD_COUNTRY_CODE,
|
||||
GEOIP_FIELD_COUNTRY_NAME,
|
||||
GEOIP_FIELD_REGION_CODE,
|
||||
GEOIP_FIELD_REGION_NAME,
|
||||
GEOIP_FIELD_CITY,
|
||||
GEOIP_FIELD_ZIP_CODE,
|
||||
GEOIP_FIELD_TIME_ZONE,
|
||||
GEOIP_FIELD_LATITUDE,
|
||||
GEOIP_FIELD_LONGITUDE,
|
||||
GEOIP_FIELD_TIMESTAMP,
|
||||
_GEOIP_FIELD_MAX,
|
||||
_GEOIP_FIELD_INVALID = -EINVAL,
|
||||
} GeoIPField;
|
||||
|
||||
typedef enum GeoIPValueType {
|
||||
GEOIP_VALUE_STRING, /* A free-form string, reported as is */
|
||||
GEOIP_VALUE_ADDRESS, /* An IP address, stored in parsed form and reported in normalized form */
|
||||
GEOIP_VALUE_DOUBLE, /* A floating point number */
|
||||
GEOIP_VALUE_TIMESTAMP, /* A CLOCK_REALTIME timestamp in µs */
|
||||
_GEOIP_VALUE_TYPE_MAX,
|
||||
_GEOIP_VALUE_TYPE_INVALID = -EINVAL,
|
||||
} GeoIPValueType;
|
||||
|
||||
/* The data we pick out of the server's JSON answer, plus when we acquired it. */
|
||||
typedef struct GeoIPData {
|
||||
struct in_addr_data ip;
|
||||
char *asn;
|
||||
char *as_name;
|
||||
char *as_domain;
|
||||
char *continent_code;
|
||||
char *continent_name;
|
||||
char *country_code;
|
||||
char *country_name;
|
||||
char *region_code;
|
||||
char *region_name;
|
||||
char *city;
|
||||
char *zip_code;
|
||||
char *time_zone;
|
||||
double latitude;
|
||||
double longitude;
|
||||
usec_t timestamp; /* CLOCK_REALTIME, when the data was downloaded (i.e. the mtime of the cache file) */
|
||||
} GeoIPData;
|
||||
|
||||
#define GEOIP_DATA_INIT \
|
||||
(GeoIPData) { \
|
||||
.ip.family = AF_UNSPEC, \
|
||||
.latitude = NAN, \
|
||||
.longitude = NAN, \
|
||||
.timestamp = USEC_INFINITY, \
|
||||
}
|
||||
|
||||
static void geoip_data_done(GeoIPData *d) {
|
||||
assert(d);
|
||||
|
||||
d->asn = mfree(d->asn);
|
||||
d->as_name = mfree(d->as_name);
|
||||
d->as_domain = mfree(d->as_domain);
|
||||
d->continent_code = mfree(d->continent_code);
|
||||
d->continent_name = mfree(d->continent_name);
|
||||
d->country_code = mfree(d->country_code);
|
||||
d->country_name = mfree(d->country_name);
|
||||
d->region_code = mfree(d->region_code);
|
||||
d->region_name = mfree(d->region_name);
|
||||
d->city = mfree(d->city);
|
||||
d->zip_code = mfree(d->zip_code);
|
||||
d->time_zone = mfree(d->time_zone);
|
||||
}
|
||||
|
||||
static int geoip_data_parse(const char *text, GeoIPData *ret) {
|
||||
/* Note that different providers return slightly different fields, hence the partially redundant
|
||||
* definitions here. Additional fields the server might return are ignored. */
|
||||
static const sd_json_dispatch_field dispatch_table[] = {
|
||||
{ "ip", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_in_addr_data, offsetof(GeoIPData, ip), SD_JSON_NULLABLE },
|
||||
{ "asn", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(GeoIPData, asn), SD_JSON_NULLABLE },
|
||||
{ "as_name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(GeoIPData, as_name), SD_JSON_NULLABLE },
|
||||
{ "as_domain", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(GeoIPData, as_domain), SD_JSON_NULLABLE },
|
||||
{ "continent_code", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(GeoIPData, continent_code), SD_JSON_NULLABLE },
|
||||
{ "continent", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(GeoIPData, continent_name), SD_JSON_NULLABLE },
|
||||
{ "continent_name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(GeoIPData, continent_name), SD_JSON_NULLABLE },
|
||||
{ "country_code", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(GeoIPData, country_code), SD_JSON_NULLABLE },
|
||||
{ "country", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(GeoIPData, country_name), SD_JSON_NULLABLE },
|
||||
{ "country_name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(GeoIPData, country_name), SD_JSON_NULLABLE },
|
||||
{ "region_code", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(GeoIPData, region_code), SD_JSON_NULLABLE },
|
||||
{ "region", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(GeoIPData, region_name), SD_JSON_NULLABLE },
|
||||
{ "region_name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(GeoIPData, region_name), SD_JSON_NULLABLE },
|
||||
{ "city", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(GeoIPData, city), SD_JSON_NULLABLE },
|
||||
{ "zip_code", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(GeoIPData, zip_code), SD_JSON_NULLABLE },
|
||||
{ "time_zone", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(GeoIPData, time_zone), SD_JSON_NULLABLE },
|
||||
{ "latitude", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_double, offsetof(GeoIPData, latitude), SD_JSON_NULLABLE },
|
||||
{ "longitude", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_double, offsetof(GeoIPData, longitude), SD_JSON_NULLABLE },
|
||||
{},
|
||||
};
|
||||
|
||||
int r;
|
||||
|
||||
assert(text);
|
||||
assert(ret);
|
||||
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
|
||||
r = sd_json_parse(text, SD_JSON_PARSE_MUST_BE_OBJECT, &v, /* reterr_line= */ NULL, /* reterr_column= */ NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
_cleanup_(geoip_data_done) GeoIPData d = GEOIP_DATA_INIT;
|
||||
r = sd_json_dispatch(v, dispatch_table, SD_JSON_ALLOW_EXTENSIONS|SD_JSON_PERMISSIVE|SD_JSON_LOG, &d);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
*ret = TAKE_STRUCT(d);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static size_t write_callback(char *buf, size_t size, size_t nmemb, void *userdata) {
|
||||
struct iovec *answer = ASSERT_PTR(userdata);
|
||||
|
||||
assert(size == 1); /* The docs say that this is always true. */
|
||||
|
||||
if (nmemb == 0)
|
||||
return 0;
|
||||
|
||||
/* The answer is accumulated in a single contiguous buffer, so that the number of chunks the server
|
||||
* delivers it in doesn't matter, only the total size does. */
|
||||
size_t new_size = size_add(answer->iov_len, nmemb);
|
||||
if (new_size > REPORT_GEOIP_ANSWER_MAX) {
|
||||
log_debug("Server answer too long (%zu > %u), refusing.", new_size, REPORT_GEOIP_ANSWER_MAX);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (memchr(buf, 0, nmemb)) {
|
||||
log_debug("Server answer contains an embedded NUL, refusing.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!iovec_append(answer, &IOVEC_MAKE(buf, nmemb))) {
|
||||
log_oom_debug();
|
||||
return 0; /* Returning < nmemb signals failure */
|
||||
}
|
||||
|
||||
return nmemb;
|
||||
}
|
||||
|
||||
static int geoip_fetch(const char *endpoint, char **ret) {
|
||||
_cleanup_(iovec_done) struct iovec answer = {};
|
||||
_cleanup_(curl_slist_free_allp) struct curl_slist *header = NULL;
|
||||
char error[CURL_ERROR_SIZE] = {};
|
||||
int r;
|
||||
|
||||
assert(endpoint);
|
||||
assert(ret);
|
||||
|
||||
/* Downloads the JSON answer from the configured endpoint, and returns it as NUL-terminated string. */
|
||||
|
||||
/* The endpoint from the configuration file is validated when parsed, but the compile-time default is
|
||||
* not, hence check here, so that both take the same path. */
|
||||
if (!http_url_is_valid(endpoint))
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Endpoint URL '%s' is not valid, refusing.", endpoint);
|
||||
|
||||
r = dlopen_curl(LOG_DEBUG);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = curl_append_to_header(&header, STRV_MAKE("Accept: application/json"));
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
_cleanup_(curl_easy_cleanupp) CURL *curl = sym_curl_easy_init();
|
||||
if (!curl)
|
||||
return -ENOSR;
|
||||
|
||||
if (!easy_setopt(curl, LOG_DEBUG, CURLOPT_URL, endpoint))
|
||||
return -EXFULL;
|
||||
|
||||
if (!easy_setopt(curl, LOG_DEBUG, CURLOPT_HTTPGET, 1L))
|
||||
return -EXFULL;
|
||||
|
||||
/* Only ever talk HTTP(S), for the initial request as well as for any redirects we follow. */
|
||||
#if LIBCURL_VERSION_NUM >= 0x075500 /* libcurl 7.85.0 */
|
||||
if (!easy_setopt(curl, LOG_DEBUG, CURLOPT_PROTOCOLS_STR, "HTTP,HTTPS"))
|
||||
return -EXFULL;
|
||||
|
||||
if (!easy_setopt(curl, LOG_DEBUG, CURLOPT_REDIR_PROTOCOLS_STR, "HTTP,HTTPS"))
|
||||
return -EXFULL;
|
||||
#else
|
||||
if (!easy_setopt(curl, LOG_DEBUG, CURLOPT_PROTOCOLS, CURLPROTO_HTTP|CURLPROTO_HTTPS))
|
||||
return -EXFULL;
|
||||
|
||||
if (!easy_setopt(curl, LOG_DEBUG, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP|CURLPROTO_HTTPS))
|
||||
return -EXFULL;
|
||||
#endif
|
||||
|
||||
if (!easy_setopt(curl, LOG_DEBUG, CURLOPT_FOLLOWLOCATION, 1L))
|
||||
return -EXFULL;
|
||||
|
||||
if (!easy_setopt(curl, LOG_DEBUG, CURLOPT_MAXREDIRS, REPORT_GEOIP_MAX_REDIRECTS))
|
||||
return -EXFULL;
|
||||
|
||||
if (!easy_setopt(curl, LOG_DEBUG, CURLOPT_TIMEOUT_MS,
|
||||
(long) DIV_ROUND_UP(REPORT_GEOIP_NETWORK_TIMEOUT_USEC, USEC_PER_MSEC)))
|
||||
return -EXFULL;
|
||||
|
||||
if (!easy_setopt(curl, LOG_DEBUG, CURLOPT_NOSIGNAL, 1L))
|
||||
return -EXFULL;
|
||||
|
||||
if (!easy_setopt(curl, LOG_DEBUG, CURLOPT_ERRORBUFFER, error))
|
||||
return -EXFULL;
|
||||
|
||||
if (!easy_setopt(curl, LOG_DEBUG, CURLOPT_WRITEFUNCTION, write_callback))
|
||||
return -EXFULL;
|
||||
|
||||
if (!easy_setopt(curl, LOG_DEBUG, CURLOPT_WRITEDATA, &answer))
|
||||
return -EXFULL;
|
||||
|
||||
if (!easy_setopt(curl, LOG_DEBUG, CURLOPT_HTTPHEADER, header))
|
||||
return -EXFULL;
|
||||
|
||||
(void) easy_setopt(curl, LOG_DEBUG, CURLOPT_USERAGENT, "systemd-report-geoip " GIT_VERSION);
|
||||
|
||||
if (DEBUG_LOGGING)
|
||||
(void) easy_setopt(curl, LOG_DEBUG, CURLOPT_VERBOSE, 1L);
|
||||
|
||||
CURLcode code = sym_curl_easy_perform(curl);
|
||||
if (code != CURLE_OK)
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(EIO),
|
||||
"Download from '%s' failed: %s", endpoint,
|
||||
empty_to_null(&error[0]) ?: sym_curl_easy_strerror(code));
|
||||
|
||||
long status;
|
||||
code = sym_curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
|
||||
if (code != CURLE_OK)
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(EUCLEAN),
|
||||
"Failed to retrieve response code: %s", sym_curl_easy_strerror(code));
|
||||
|
||||
if (status < 200 || status >= 300)
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(EIO),
|
||||
"Download from '%s' failed with HTTP status %ld.", endpoint, status);
|
||||
if (status != 200)
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(EIO),
|
||||
"Unexpected HTTP status from '%s': %ld.", endpoint, status);
|
||||
|
||||
r = make_cstring(answer.iov_base, answer.iov_len, MAKE_CSTRING_REFUSE_TRAILING_NUL, ret);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to turn acquired data into C string: %m");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int geoip_cache_path(char **ret) {
|
||||
int r;
|
||||
|
||||
assert(ret);
|
||||
|
||||
_cleanup_free_ char *dir = NULL;
|
||||
r = state_directory(RUNTIME_SCOPE_SYSTEM, REPORT_GEOIP_STATE_DIR_SUFFIX, &dir);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
_cleanup_free_ char *path = path_join(dir, REPORT_GEOIP_CACHE_FILE_NAME);
|
||||
if (!path)
|
||||
return -ENOMEM;
|
||||
|
||||
*ret = TAKE_PTR(path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int geoip_cache_load(const char *path, char **ret_text, usec_t *ret_mtime) {
|
||||
int r;
|
||||
|
||||
assert(path);
|
||||
assert(ret_text);
|
||||
assert(ret_mtime);
|
||||
|
||||
/* Reads the cache file, and returns its modification time, i.e. when the data was downloaded. */
|
||||
|
||||
_cleanup_close_ int fd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY);
|
||||
if (fd < 0)
|
||||
return -errno;
|
||||
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) < 0)
|
||||
return -errno;
|
||||
|
||||
r = stat_verify_regular(&st);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
_cleanup_free_ char *text = NULL;
|
||||
r = read_full_file_full(
|
||||
fd,
|
||||
/* filename= */ NULL,
|
||||
/* offset= */ UINT64_MAX,
|
||||
REPORT_GEOIP_ANSWER_MAX,
|
||||
READ_FULL_FILE_FAIL_WHEN_LARGER,
|
||||
/* bind_name= */ NULL,
|
||||
&text,
|
||||
/* ret_size= */ NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
*ret_text = TAKE_PTR(text);
|
||||
*ret_mtime = timespec_load(&st.st_mtim);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int geoip_prepare_cache(char **ret_cache_path, LockFile *ret_lock_file) {
|
||||
int r;
|
||||
|
||||
assert(ret_cache_path);
|
||||
assert(ret_lock_file);
|
||||
|
||||
_cleanup_free_ char *cache_path = NULL;
|
||||
r = geoip_cache_path(&cache_path);
|
||||
if (r < 0) {
|
||||
log_warning_errno(r, "Failed to determine cache file path, ignoring: %m");
|
||||
*ret_cache_path = NULL;
|
||||
*ret_lock_file = LOCK_FILE_INIT;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Create the parent directory before we try to create the lock file. Ignore failures. */
|
||||
r = mkdir_parents(cache_path, 0755);
|
||||
if (r < 0)
|
||||
log_warning_errno(r, "Failed to create parent directory of '%s', ignoring: %m", cache_path);
|
||||
|
||||
/* Serialize concurrent instances, so that only one of them talks to the network and the others pick
|
||||
* up the cache it leaves behind. Note that we don't fail here, since caching is just an
|
||||
* optimization here, it's not load bearing, and not having the lock won't corrupt the cache
|
||||
* as we write the cache file via atomic fs updates. Not having the lock just means we might
|
||||
* possibly go to the network unnecessarily. (When running this backend for testing purposes
|
||||
* we won't be able to lock here, since we likely lack privileges, hence the graceful
|
||||
* handling here.) */
|
||||
_cleanup_(release_lock_file) LockFile lock = LOCK_FILE_INIT;
|
||||
r = make_lock_file_for(cache_path, LOCK_EX, &lock);
|
||||
if (r < 0)
|
||||
log_warning_errno(r, "Failed to take lock on '%s', ignoring: %m", cache_path);
|
||||
|
||||
*ret_cache_path = TAKE_PTR(cache_path);
|
||||
*ret_lock_file = TAKE_GENERIC(lock, LockFile, LOCK_FILE_INIT);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int geoip_data_acquire(GeoIPData *ret) {
|
||||
int r;
|
||||
|
||||
assert(ret);
|
||||
|
||||
/* Acquires the geolocation data, preferably from the cache if it is recent enough, otherwise from
|
||||
* the network. A stale cache is used as fallback if the network is unavailable. Note that we
|
||||
* consider the cache optional, hence any failure of the cache is handled gracefully. */
|
||||
|
||||
const char *endpoint = arg_endpoint ?: GEOIP_ENDPOINT; /* compile-time default, see -Dgeoip-endpoint= */
|
||||
|
||||
_cleanup_(release_lock_file) LockFile lock = LOCK_FILE_INIT;
|
||||
_cleanup_free_ char *cache_path = NULL;
|
||||
r = geoip_prepare_cache(&cache_path, &lock);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
_cleanup_free_ char *cached = NULL;
|
||||
usec_t mtime = USEC_INFINITY, age = USEC_INFINITY;
|
||||
if (cache_path) {
|
||||
/* First, read cache if we have one */
|
||||
r = geoip_cache_load(cache_path, &cached, &mtime);
|
||||
if (r < 0)
|
||||
log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r,
|
||||
"Failed to read cache file '%s', ignoring: %m", cache_path);
|
||||
else
|
||||
age = usec_sub_unsigned(now(CLOCK_REALTIME), mtime); /* A cache from the future counts as brand new. */
|
||||
|
||||
if (age < arg_refresh_usec) {
|
||||
assert(cached);
|
||||
|
||||
_cleanup_(geoip_data_done) GeoIPData d = GEOIP_DATA_INIT;
|
||||
r = geoip_data_parse(cached, &d);
|
||||
if (r < 0)
|
||||
log_warning_errno(r, "Failed to parse cache file '%s', ignoring: %m", cache_path);
|
||||
else {
|
||||
log_debug("Using cached geolocation data (age: %s).", FORMAT_TIMESPAN(age, USEC_PER_SEC));
|
||||
d.timestamp = mtime;
|
||||
*ret = TAKE_STRUCT(d);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The cache is unusable, don't fall back to it below either. */
|
||||
cached = mfree(cached);
|
||||
}
|
||||
}
|
||||
|
||||
/* Second: Cache wasn't available or not fresh, let's get the data from the network */
|
||||
_cleanup_free_ char *fresh = NULL;
|
||||
r = geoip_fetch(endpoint, &fresh);
|
||||
if (r < 0)
|
||||
log_warning_errno(r, "Failed to download geolocation data from '%s', ignoring: %m", endpoint);
|
||||
else {
|
||||
_cleanup_(geoip_data_done) GeoIPData d = GEOIP_DATA_INIT;
|
||||
|
||||
r = geoip_data_parse(fresh, &d);
|
||||
if (r < 0)
|
||||
log_warning_errno(r, "Failed to parse geolocation data from '%s', ignoring: %m", endpoint);
|
||||
else {
|
||||
/* Sample the timestamp only now, i.e. after the (potentially slow) download completed,
|
||||
* and stamp the cache file with precisely this value, so that the mtime we report on
|
||||
* later requests matches the timestamp we report right now. */
|
||||
usec_t n = now(CLOCK_REALTIME);
|
||||
|
||||
/* Only cache what we managed to parse, so that we never fall back to garbage. */
|
||||
if (cache_path) {
|
||||
r = write_string_file_full(AT_FDCWD, cache_path, fresh,
|
||||
WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_MKDIR_0755,
|
||||
TIMESPEC_STORE(n), /* label_fn= */ NULL);
|
||||
if (r < 0)
|
||||
log_warning_errno(r, "Failed to write cache file '%s', ignoring: %m", cache_path);
|
||||
}
|
||||
|
||||
d.timestamp = n;
|
||||
*ret = TAKE_STRUCT(d);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Third, the network didn't work, but we have some stale cache data, let's use that then, because we
|
||||
* have nothing better. */
|
||||
if (cached) {
|
||||
_cleanup_(geoip_data_done) GeoIPData d = GEOIP_DATA_INIT;
|
||||
|
||||
r = geoip_data_parse(cached, &d);
|
||||
if (r < 0)
|
||||
log_warning_errno(r, "Failed to parse cache file '%s', ignoring: %m", cache_path);
|
||||
else {
|
||||
log_notice("Using stale cached geolocation data (age: %s).", FORMAT_TIMESPAN(age, USEC_PER_SEC));
|
||||
d.timestamp = mtime;
|
||||
*ret = TAKE_STRUCT(d);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
log_debug("No geolocation data available, reporting nothing.");
|
||||
*ret = GEOIP_DATA_INIT;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int geoip_generate(const MetricFamily mf[static _GEOIP_FIELD_MAX], sd_varlink *link, void *userdata) {
|
||||
/* Describes how each member of GeoIPData is turned into a metric. The order must match the metric
|
||||
* family table below. */
|
||||
static const struct {
|
||||
GeoIPValueType value_type;
|
||||
size_t offset; /* Offset of the corresponding member in GeoIPData */
|
||||
} field_table[_GEOIP_FIELD_MAX] = {
|
||||
[GEOIP_FIELD_IP] = { GEOIP_VALUE_ADDRESS, offsetof(GeoIPData, ip) },
|
||||
[GEOIP_FIELD_ASN] = { GEOIP_VALUE_STRING, offsetof(GeoIPData, asn) },
|
||||
[GEOIP_FIELD_AS_NAME] = { GEOIP_VALUE_STRING, offsetof(GeoIPData, as_name) },
|
||||
[GEOIP_FIELD_AS_DOMAIN] = { GEOIP_VALUE_STRING, offsetof(GeoIPData, as_domain) },
|
||||
[GEOIP_FIELD_CONTINENT_CODE] = { GEOIP_VALUE_STRING, offsetof(GeoIPData, continent_code) },
|
||||
[GEOIP_FIELD_CONTINENT_NAME] = { GEOIP_VALUE_STRING, offsetof(GeoIPData, continent_name) },
|
||||
[GEOIP_FIELD_COUNTRY_CODE] = { GEOIP_VALUE_STRING, offsetof(GeoIPData, country_code) },
|
||||
[GEOIP_FIELD_COUNTRY_NAME] = { GEOIP_VALUE_STRING, offsetof(GeoIPData, country_name) },
|
||||
[GEOIP_FIELD_REGION_CODE] = { GEOIP_VALUE_STRING, offsetof(GeoIPData, region_code) },
|
||||
[GEOIP_FIELD_REGION_NAME] = { GEOIP_VALUE_STRING, offsetof(GeoIPData, region_name) },
|
||||
[GEOIP_FIELD_CITY] = { GEOIP_VALUE_STRING, offsetof(GeoIPData, city) },
|
||||
[GEOIP_FIELD_ZIP_CODE] = { GEOIP_VALUE_STRING, offsetof(GeoIPData, zip_code) },
|
||||
[GEOIP_FIELD_TIME_ZONE] = { GEOIP_VALUE_STRING, offsetof(GeoIPData, time_zone) },
|
||||
[GEOIP_FIELD_LATITUDE] = { GEOIP_VALUE_DOUBLE, offsetof(GeoIPData, latitude) },
|
||||
[GEOIP_FIELD_LONGITUDE] = { GEOIP_VALUE_DOUBLE, offsetof(GeoIPData, longitude) },
|
||||
[GEOIP_FIELD_TIMESTAMP] = { GEOIP_VALUE_TIMESTAMP, offsetof(GeoIPData, timestamp) },
|
||||
};
|
||||
|
||||
int r;
|
||||
|
||||
assert(mf && mf[0].name);
|
||||
assert(link);
|
||||
|
||||
_cleanup_(geoip_data_done) GeoIPData d = GEOIP_DATA_INIT;
|
||||
r = geoip_data_acquire(&d);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
for (GeoIPField f = 0; f < _GEOIP_FIELD_MAX; f++) {
|
||||
const void *member = (const uint8_t*) &d + field_table[f].offset;
|
||||
|
||||
switch (field_table[f].value_type) {
|
||||
|
||||
case GEOIP_VALUE_STRING: {
|
||||
const char *v = *(const char* const*) member;
|
||||
if (isempty(v))
|
||||
continue;
|
||||
|
||||
if (!string_is_safe(v, STRING_ALLOW_BACKSLASHES|STRING_ALLOW_QUOTES|STRING_ALLOW_GLOBS)) {
|
||||
log_debug("Value of metric '%s' contains unsafe characters, ignoring.", mf[f].name);
|
||||
continue;
|
||||
}
|
||||
|
||||
r = metric_build_send_string(mf + f, link, /* object= */ NULL, v, /* fields= */ NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
break;
|
||||
}
|
||||
|
||||
case GEOIP_VALUE_ADDRESS: {
|
||||
const struct in_addr_data *a = member;
|
||||
if (!IN_SET(a->family, AF_INET, AF_INET6))
|
||||
continue;
|
||||
|
||||
/* Report the canonical formatting of the parsed address, never the string as we got it. */
|
||||
r = metric_build_send_string(mf + f, link, /* object= */ NULL, IN_ADDR_TO_STRING(a->family, &a->address), /* fields= */ NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
break;
|
||||
}
|
||||
|
||||
case GEOIP_VALUE_DOUBLE: {
|
||||
double v = *(const double*) member;
|
||||
if (!isfinite(v))
|
||||
continue;
|
||||
|
||||
r = metric_build_send_double(mf + f, link, /* object= */ NULL, v, /* fields= */ NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
break;
|
||||
}
|
||||
|
||||
case GEOIP_VALUE_TIMESTAMP: {
|
||||
usec_t v = *(const usec_t*) member;
|
||||
if (v == USEC_INFINITY)
|
||||
continue;
|
||||
|
||||
r = metric_build_send_unsigned(mf + f, link, /* object= */ NULL, v, /* fields= */ NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
assert_not_reached();
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The metrics we report. Only the first entry carries the generating function, which reports all of them in
|
||||
* one go. The order must match the field table in geoip_generate() above. */
|
||||
static const MetricFamily metric_family_table[_GEOIP_FIELD_MAX + 1] = {
|
||||
[GEOIP_FIELD_IP] = {
|
||||
METRIC_IO_SYSTEMD_GEOIP_PREFIX "PublicAddress",
|
||||
"Public IP address of the system as seen by the geolocation service, in normalized form",
|
||||
METRIC_FAMILY_TYPE_STRING,
|
||||
.generate = geoip_generate,
|
||||
},
|
||||
[GEOIP_FIELD_ASN] = {
|
||||
METRIC_IO_SYSTEMD_GEOIP_PREFIX "ASN",
|
||||
"Autonomous System Number of the IP address",
|
||||
METRIC_FAMILY_TYPE_STRING,
|
||||
},
|
||||
[GEOIP_FIELD_AS_NAME] = {
|
||||
METRIC_IO_SYSTEMD_GEOIP_PREFIX "ASName",
|
||||
"Associated name of the ASN",
|
||||
METRIC_FAMILY_TYPE_STRING,
|
||||
},
|
||||
[GEOIP_FIELD_AS_DOMAIN] = {
|
||||
METRIC_IO_SYSTEMD_GEOIP_PREFIX "ASDomain",
|
||||
"Associated domain of the ASN",
|
||||
METRIC_FAMILY_TYPE_STRING,
|
||||
},
|
||||
[GEOIP_FIELD_CONTINENT_CODE] = {
|
||||
METRIC_IO_SYSTEMD_GEOIP_PREFIX "ContinentCode",
|
||||
"Code of the continent the system is located in",
|
||||
METRIC_FAMILY_TYPE_STRING,
|
||||
},
|
||||
[GEOIP_FIELD_CONTINENT_NAME] = {
|
||||
METRIC_IO_SYSTEMD_GEOIP_PREFIX "ContinentName",
|
||||
"Name of the continent the system is located in",
|
||||
METRIC_FAMILY_TYPE_STRING,
|
||||
},
|
||||
[GEOIP_FIELD_COUNTRY_CODE] = {
|
||||
METRIC_IO_SYSTEMD_GEOIP_PREFIX "CountryCode",
|
||||
"ISO 3166-1 alpha-2 code of the country the system is located in",
|
||||
METRIC_FAMILY_TYPE_STRING,
|
||||
},
|
||||
[GEOIP_FIELD_COUNTRY_NAME] = {
|
||||
METRIC_IO_SYSTEMD_GEOIP_PREFIX "CountryName",
|
||||
"Name of the country the system is located in",
|
||||
METRIC_FAMILY_TYPE_STRING,
|
||||
},
|
||||
[GEOIP_FIELD_REGION_CODE] = {
|
||||
METRIC_IO_SYSTEMD_GEOIP_PREFIX "RegionCode",
|
||||
"Code of the region (state, province, …) the system is located in",
|
||||
METRIC_FAMILY_TYPE_STRING,
|
||||
},
|
||||
[GEOIP_FIELD_REGION_NAME] = {
|
||||
METRIC_IO_SYSTEMD_GEOIP_PREFIX "RegionName",
|
||||
"Name of the region (state, province, …) the system is located in",
|
||||
METRIC_FAMILY_TYPE_STRING,
|
||||
},
|
||||
[GEOIP_FIELD_CITY] = {
|
||||
METRIC_IO_SYSTEMD_GEOIP_PREFIX "City",
|
||||
"Name of the city the system is located in",
|
||||
METRIC_FAMILY_TYPE_STRING,
|
||||
},
|
||||
[GEOIP_FIELD_ZIP_CODE] = {
|
||||
METRIC_IO_SYSTEMD_GEOIP_PREFIX "ZipCode",
|
||||
"Postal code of the area the system is located in",
|
||||
METRIC_FAMILY_TYPE_STRING,
|
||||
},
|
||||
[GEOIP_FIELD_TIME_ZONE] = {
|
||||
METRIC_IO_SYSTEMD_GEOIP_PREFIX "Timezone",
|
||||
"IANA time zone name of the area the system is located in",
|
||||
METRIC_FAMILY_TYPE_STRING,
|
||||
},
|
||||
[GEOIP_FIELD_LATITUDE] = {
|
||||
METRIC_IO_SYSTEMD_GEOIP_PREFIX "Latitude",
|
||||
"Latitude of the system's location in decimal degrees",
|
||||
METRIC_FAMILY_TYPE_GAUGE,
|
||||
},
|
||||
[GEOIP_FIELD_LONGITUDE] = {
|
||||
METRIC_IO_SYSTEMD_GEOIP_PREFIX "Longitude",
|
||||
"Longitude of the system's location in decimal degrees",
|
||||
METRIC_FAMILY_TYPE_GAUGE,
|
||||
},
|
||||
[GEOIP_FIELD_TIMESTAMP] = {
|
||||
METRIC_IO_SYSTEMD_GEOIP_PREFIX "Timestamp",
|
||||
"CLOCK_REALTIME microseconds at which the geolocation data was acquired from the service",
|
||||
METRIC_FAMILY_TYPE_GAUGE,
|
||||
},
|
||||
[_GEOIP_FIELD_MAX] = {}, /* terminator */
|
||||
};
|
||||
|
||||
int vl_method_list_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
|
||||
return metrics_method_list(metric_family_table, link, parameters, flags, /* userdata= */ NULL);
|
||||
}
|
||||
|
||||
int vl_method_describe_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
|
||||
return metrics_method_describe(metric_family_table, link, parameters, flags, /* userdata= */ NULL);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# This file is part of systemd.
|
||||
#
|
||||
# systemd is free software; you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Lesser General Public License as published by the Free
|
||||
# Software Foundation; either version 2.1 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Entries in this file show the compile time defaults. Local configuration
|
||||
# should be created by either modifying this file (or a copy of it placed in
|
||||
# /etc/ if the original file is shipped in /usr/), or by creating "drop-ins" in
|
||||
# the /etc/systemd/report-geoip.conf.d/ directory. The latter is generally
|
||||
# recommended. Defaults can be restored by simply deleting the main
|
||||
# configuration file and all drop-ins located in /etc/.
|
||||
#
|
||||
# Use 'systemd-analyze cat-config systemd/report-geoip.conf' to display the full config.
|
||||
#
|
||||
# See report-geoip.conf(5) for details.
|
||||
|
||||
[GeoIP]
|
||||
#Endpoint={{GEOIP_ENDPOINT}}
|
||||
#RefreshSec=30min
|
||||
@@ -0,0 +1,10 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
#pragma once
|
||||
|
||||
#include "forward.h"
|
||||
|
||||
extern char *arg_endpoint;
|
||||
extern usec_t arg_refresh_usec;
|
||||
|
||||
int vl_method_list_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata);
|
||||
int vl_method_describe_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata);
|
||||
+40
@@ -52,6 +52,41 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
self.wfile.write(response)
|
||||
|
||||
def do_GET(self):
|
||||
# Mimic the JSON answer of a freegeoip-style geolocation service, for
|
||||
# testing systemd-report-geoip. The address is deliberately not in
|
||||
# canonical form (and might not even be an address), to verify that
|
||||
# the client validates and normalizes it.
|
||||
if self.path != '/json/':
|
||||
self.send_error(404, 'Not Found')
|
||||
return
|
||||
|
||||
response = json.dumps(
|
||||
{
|
||||
'ip': args.geoip_address,
|
||||
'asn': 'AS3320',
|
||||
'as_name': 'Deutsche Telekom AG',
|
||||
'as_domain': 'telekom.de',
|
||||
'continent_code': 'EU',
|
||||
'continent_name': 'Europe',
|
||||
'country_code': 'DE',
|
||||
'country_name': 'Germany',
|
||||
'region_code': 'BE',
|
||||
'region_name': 'State of Berlin',
|
||||
'city': 'Berlin',
|
||||
'zip_code': '10785',
|
||||
'time_zone': 'Europe/Berlin',
|
||||
'latitude': 52.5061,
|
||||
'longitude': 13.3684,
|
||||
'metro_code': 0,
|
||||
}
|
||||
).encode()
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.send_header('Content-Length', len(response))
|
||||
self.end_headers()
|
||||
self.wfile.write(response)
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
print(f'{self.address_string()} - {fmt % args}')
|
||||
|
||||
@@ -60,6 +95,11 @@ parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--port', type=int, default=8089)
|
||||
parser.add_argument('--cert', help='TLS certificate file')
|
||||
parser.add_argument('--key', help='TLS private key file')
|
||||
parser.add_argument(
|
||||
'--geoip-address',
|
||||
default='2001:0DB8:0000:0000:0000:0000:0000:0001',
|
||||
help='IP address to return in the geolocation answer',
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
server = HTTPServer(('', args.port), Handler)
|
||||
|
||||
@@ -4,6 +4,9 @@ set -eux
|
||||
set -o pipefail
|
||||
shopt -s nullglob
|
||||
|
||||
# shellcheck source=test/units/test-control.sh
|
||||
. "$(dirname "$0")"/test-control.sh
|
||||
|
||||
# Unset $PAGER so we don't have to use --no-pager everywhere
|
||||
export PAGER=
|
||||
|
||||
@@ -669,6 +672,128 @@ rm -f /run/systemd/report.files/fifoshadowed /var/lib/systemd/report.files/fifos
|
||||
rm -f /run/systemd/report.files/maskedfile /var/lib/systemd/report.files/maskedfile /var/lib/systemd/report.files/dirshadowed
|
||||
rmdir /run/systemd/report.files/dirshadowed
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Test the systemd-report-geoip@.service metric provider: the geolocation data
|
||||
# is downloaded from the configured endpoint (our fake server here), cached, and
|
||||
# exposed as io.systemd.GeoIP.* metrics. The provider is only built with libcurl
|
||||
# support, so skip if unavailable.
|
||||
geoip_cleanup() {
|
||||
set +e
|
||||
systemctl stop systemd-report-geoip.socket fake-geoip-server fake-geoip-server-bogus
|
||||
rm -rf /run/systemd/report-geoip.conf.d
|
||||
rm -f /var/lib/systemd/report-geoip/cache.json
|
||||
set -e
|
||||
}
|
||||
|
||||
testcase_geoip() {
|
||||
local geoip_metrics geoip_describe family
|
||||
|
||||
if ! systemctl cat systemd-report-geoip.socket &>/dev/null; then
|
||||
echo "systemd-report-geoip.socket is not installed, skipping geoip metrics test."
|
||||
return 0
|
||||
fi
|
||||
|
||||
trap geoip_cleanup RETURN
|
||||
|
||||
systemd-run -p Type=notify --unit=fake-geoip-server "$FAKE_SERVER" --port=8092
|
||||
|
||||
mkdir -p /run/systemd/report-geoip.conf.d
|
||||
cat >/run/systemd/report-geoip.conf.d/99-test.conf <<EOF
|
||||
[GeoIP]
|
||||
Endpoint=http://localhost:8092/json/
|
||||
RefreshSec=1h
|
||||
EOF
|
||||
rm -f /var/lib/systemd/report-geoip/cache.json
|
||||
|
||||
systemctl start systemd-report-geoip.socket
|
||||
varlinkctl info /run/systemd/report/io.systemd.GeoIP
|
||||
varlinkctl list-methods /run/systemd/report/io.systemd.GeoIP
|
||||
|
||||
geoip_metrics="$(varlinkctl call --more /run/systemd/report/io.systemd.GeoIP io.systemd.Metrics.List {})"
|
||||
geoip_value() { echo "$geoip_metrics" | jq --seq -r "select(.name == \"$1\") | .value | tostring"; }
|
||||
# Numbers are emitted in exponent notation with full precision, and jq preserves (and compares) the
|
||||
# literal, so compare them numerically with a tolerance rather than textually.
|
||||
geoip_number_is() { echo "$geoip_metrics" | jq --seq -r "select(.name == \"$1\") | (.value - $2 | fabs) < 1e-9 | tostring" | grep -wx true >/dev/null; }
|
||||
|
||||
# The address must be reported in normalized form, not as the server sent it.
|
||||
[ "$(geoip_value io.systemd.GeoIP.PublicAddress)" = "2001:db8::1" ]
|
||||
[ "$(geoip_value io.systemd.GeoIP.ASN)" = "AS3320" ]
|
||||
[ "$(geoip_value io.systemd.GeoIP.ASName)" = "Deutsche Telekom AG" ]
|
||||
[ "$(geoip_value io.systemd.GeoIP.ASDomain)" = "telekom.de" ]
|
||||
[ "$(geoip_value io.systemd.GeoIP.ContinentCode)" = "EU" ]
|
||||
[ "$(geoip_value io.systemd.GeoIP.ContinentName)" = "Europe" ]
|
||||
[ "$(geoip_value io.systemd.GeoIP.CountryCode)" = "DE" ]
|
||||
[ "$(geoip_value io.systemd.GeoIP.CountryName)" = "Germany" ]
|
||||
[ "$(geoip_value io.systemd.GeoIP.RegionCode)" = "BE" ]
|
||||
[ "$(geoip_value io.systemd.GeoIP.RegionName)" = "State of Berlin" ]
|
||||
[ "$(geoip_value io.systemd.GeoIP.City)" = "Berlin" ]
|
||||
[ "$(geoip_value io.systemd.GeoIP.ZipCode)" = "10785" ]
|
||||
[ "$(geoip_value io.systemd.GeoIP.Timezone)" = "Europe/Berlin" ]
|
||||
geoip_number_is io.systemd.GeoIP.Latitude 52.5061
|
||||
geoip_number_is io.systemd.GeoIP.Longitude 13.3684
|
||||
# Freshly downloaded: the acquisition timestamp is "now", i.e. not older than a minute.
|
||||
[ "$(geoip_value io.systemd.GeoIP.Timestamp)" -gt "$(( ($(date +%s) - 60) * 1000000 ))" ]
|
||||
|
||||
# The obsolete metro_code field must not be reported.
|
||||
(! echo "$geoip_metrics" | grep -i metro >/dev/null)
|
||||
|
||||
# The answer must have been cached.
|
||||
test -s /var/lib/systemd/report-geoip/cache.json
|
||||
jq . /var/lib/systemd/report-geoip/cache.json
|
||||
|
||||
# Describe must list all sixteen metric families, with the right types.
|
||||
geoip_describe="$(varlinkctl call --more /run/systemd/report/io.systemd.GeoIP io.systemd.Metrics.Describe {})"
|
||||
[ "$(echo "$geoip_describe" | jq --seq -r '.name' | grep -c '^io\.systemd\.GeoIP\.')" = 16 ]
|
||||
for family in PublicAddress ASN ASName ASDomain ContinentCode ContinentName CountryCode CountryName \
|
||||
RegionCode RegionName City ZipCode Timezone; do
|
||||
echo "$geoip_describe" | jq --seq -r "select(.name == \"io.systemd.GeoIP.$family\") | .type" | grep -wx string >/dev/null
|
||||
done
|
||||
for family in Latitude Longitude Timestamp; do
|
||||
echo "$geoip_describe" | jq --seq -r "select(.name == \"io.systemd.GeoIP.$family\") | .type" | grep -wx gauge >/dev/null
|
||||
done
|
||||
|
||||
# With the endpoint gone the (still fresh) cache must be used, and the acquisition timestamp must then
|
||||
# be the cache file's modification time.
|
||||
systemctl stop fake-geoip-server
|
||||
geoip_metrics="$(varlinkctl call --more /run/systemd/report/io.systemd.GeoIP io.systemd.Metrics.List {})"
|
||||
[ "$(geoip_value io.systemd.GeoIP.PublicAddress)" = "2001:db8::1" ]
|
||||
[ "$(geoip_value io.systemd.GeoIP.ASN)" = "AS3320" ]
|
||||
[ "$(geoip_value io.systemd.GeoIP.City)" = "Berlin" ]
|
||||
[ "$(( $(geoip_value io.systemd.GeoIP.Timestamp) / 1000000 ))" = "$(stat -c %Y /var/lib/systemd/report-geoip/cache.json)" ]
|
||||
|
||||
# Force a refresh: the download fails, so the stale cache must be used.
|
||||
cat >/run/systemd/report-geoip.conf.d/99-test.conf <<EOF
|
||||
[GeoIP]
|
||||
Endpoint=http://localhost:8092/json/
|
||||
RefreshSec=0
|
||||
EOF
|
||||
geoip_metrics="$(varlinkctl call --more /run/systemd/report/io.systemd.GeoIP io.systemd.Metrics.List {})"
|
||||
[ "$(geoip_value io.systemd.GeoIP.PublicAddress)" = "2001:db8::1" ]
|
||||
[ "$(geoip_value io.systemd.GeoIP.City)" = "Berlin" ]
|
||||
|
||||
# An endpoint returning something that isn't an IP address: the address must
|
||||
# be skipped, but the other fields still be reported.
|
||||
systemd-run -p Type=notify --unit=fake-geoip-server-bogus \
|
||||
"$FAKE_SERVER" --port=8091 --geoip-address="not an address"
|
||||
cat >/run/systemd/report-geoip.conf.d/99-test.conf <<EOF
|
||||
[GeoIP]
|
||||
Endpoint=http://localhost:8091/json/
|
||||
RefreshSec=0
|
||||
EOF
|
||||
geoip_metrics="$(varlinkctl call --more /run/systemd/report/io.systemd.GeoIP io.systemd.Metrics.List {})"
|
||||
[ -z "$(geoip_value io.systemd.GeoIP.PublicAddress)" ]
|
||||
[ "$(geoip_value io.systemd.GeoIP.City)" = "Berlin" ]
|
||||
systemctl stop fake-geoip-server-bogus
|
||||
|
||||
# An unreachable endpoint and no cache: nothing is reported, which the metrics helpers signal with
|
||||
# the NoSuchMetric sentinel error (as any source without metrics does).
|
||||
rm -f /var/lib/systemd/report-geoip/cache.json
|
||||
(! varlinkctl call --more /run/systemd/report/io.systemd.GeoIP io.systemd.Metrics.List {} 2>&1) | grep NoSuchMetric >/dev/null
|
||||
}
|
||||
|
||||
# Only the geoip test is organized as a test case so far, the others still run inline.
|
||||
run_testcases
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Test report signing through the plain software backend, driven entirely via
|
||||
# the io.systemd.Report Varlink interface (the GenerateSigned method). The plain
|
||||
|
||||
@@ -841,6 +841,14 @@ units = [
|
||||
'file' : 'systemd-report-files@.service.in',
|
||||
'conditions' : ['ENABLE_REPORT'],
|
||||
},
|
||||
{
|
||||
'file' : 'systemd-report-geoip.socket',
|
||||
'conditions' : ['ENABLE_REPORT', 'HAVE_LIBCURL'],
|
||||
},
|
||||
{
|
||||
'file' : 'systemd-report-geoip@.service.in',
|
||||
'conditions' : ['ENABLE_REPORT', 'HAVE_LIBCURL'],
|
||||
},
|
||||
{
|
||||
'file' : 'systemd-report-hwmon.socket',
|
||||
'conditions' : ['ENABLE_REPORT'],
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# This file is part of systemd.
|
||||
#
|
||||
# systemd is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation; either version 2.1 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
[Unit]
|
||||
Description=Report GeoIP Metrics Socket
|
||||
Documentation=man:systemd-report-geoip@.service(8)
|
||||
DefaultDependencies=no
|
||||
Before=sockets.target shutdown.target
|
||||
Conflicts=shutdown.target
|
||||
|
||||
[Socket]
|
||||
ListenStream=/run/systemd/report/io.systemd.GeoIP
|
||||
FileDescriptorName=varlink
|
||||
SocketMode=0666
|
||||
Accept=yes
|
||||
MaxConnectionsPerSource=16
|
||||
RemoveOnStop=yes
|
||||
XAttrEntryPoint=user.varlink=entrypoint
|
||||
XAttrListen=user.varlink=listen
|
||||
XAttrAccept=user.varlink=server
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
@@ -0,0 +1,42 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# This file is part of systemd.
|
||||
#
|
||||
# systemd is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation; either version 2.1 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
[Unit]
|
||||
Description=Report GeoIP Metrics
|
||||
Documentation=man:systemd-report-geoip@.service(8)
|
||||
DefaultDependencies=no
|
||||
Conflicts=shutdown.target
|
||||
Before=shutdown.target
|
||||
|
||||
[Service]
|
||||
CapabilityBoundingSet=
|
||||
DeviceAllow=
|
||||
DynamicUser=yes
|
||||
LockPersonality=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
PrivateDevices=yes
|
||||
PrivateIPC=yes
|
||||
PrivateTmp=disconnected
|
||||
ProtectControlGroups=yes
|
||||
ProtectHome=yes
|
||||
ProtectHostname=yes
|
||||
ProtectKernelLogs=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectSystem=strict
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
RestrictNamespaces=yes
|
||||
RestrictRealtime=yes
|
||||
RestrictSUIDSGID=yes
|
||||
StateDirectory=systemd/report-geoip
|
||||
SystemCallArchitectures=native
|
||||
SystemCallErrorNumber=EPERM
|
||||
SystemCallFilter=@system-service
|
||||
|
||||
ExecStart={{LIBEXECDIR}}/systemd-report-geoip
|
||||
Reference in new issue
Block a user