diff mbox series

[v5,1/3] meta: Introduce new conditions 'time', 'day' and 'hour'

Message ID 20190707205531.6628-1-a@juaristi.eus
State Changes Requested
Delegated to: Pablo Neira
Headers show
Series [v5,1/3] meta: Introduce new conditions 'time', 'day' and 'hour' | expand

Commit Message

Ander Juaristi July 7, 2019, 8:55 p.m. UTC
These keywords introduce new checks for a timestamp, an absolute date (which is converted to a timestamp),
an hour in the day (which is converted to the number of seconds since midnight) and a day of week.

When converting an ISO date (eg. 2019-06-06 17:00) to a timestamp,
we need to substract it the GMT difference in seconds, that is, the value
of the 'tm_gmtoff' field in the tm structure. This is because the kernel
doesn't know about time zones. And hence the kernel manages different timestamps
than those that are advertised in userspace when running, for instance, date +%s.

The same conversion needs to be done when converting hours (e.g 17:00) to seconds since midnight
as well.

The result needs to be computed modulo 86400 in case GMT offset (difference in seconds from UTC)
is negative.

We also introduce a new command line option (-t, --seconds) to show the actual
timestamps when printing the values, rather than the ISO dates, or the hour.

Some usage examples:

	time < "2019-06-06 17:00" drop;
	time < "2019-06-06 17:20:20" drop;
	time < 12341234 drop;
	day "Sat" drop;
	day 6 drop;
	hour >= 17:00 drop;
	hour >= "17:00:01" drop;
	hour >= 63000 drop;

We need to convert an ISO date to a timestamp
without taking into account the time zone offset, since comparison will
be done in kernel space and there is no time zone information there.

Overwriting TZ is portable, but will cause problems when parsing a
ruleset that has 'time' and 'hour' rules. Parsing an 'hour' type must
not do time zone conversion, but that will be automatically done if TZ has
been overwritten to UTC.

Hence, we use timegm() to parse the 'time' type, even though it's not portable.
Overwriting TZ seems to be a much worse solution.

Finally, be aware that timestamps are converted to nanoseconds when
transferring to the kernel (as comparison is done with nanosecond
precision), and back to seconds when retrieving them for printing.

Signed-off-by: Ander Juaristi <a@juaristi.eus>
---
 include/datatype.h                  |   3 +
 include/linux/netfilter/nf_tables.h |   6 +
 include/meta.h                      |   3 +
 include/nftables.h                  |   5 +
 include/nftables/libnftables.h      |   4 +-
 src/datatype.c                      |   3 +
 src/main.c                          |  11 +-
 src/meta.c                          | 320 ++++++++++++++++++++++++++++
 src/parser_bison.y                  |   4 +
 src/scanner.l                       |   4 +-
 tests/py/ip/meta.t.payload          |  84 ++++++++
 11 files changed, 444 insertions(+), 3 deletions(-)

Comments

Florian Westphal July 14, 2019, 11:19 p.m. UTC | #1
Ander Juaristi <a@juaristi.eus> wrote:
> These keywords introduce new checks for a timestamp, an absolute date (which is converted to a timestamp),
> an hour in the day (which is converted to the number of seconds since midnight) and a day of week.

[ Pablo, please see below for a usability question ]

This patch causes following compiler warnings for me:
meta.c: In function 'hour_type_print':
meta.c:582:26: warning: '%02d' directive output may be truncated writing between 2 and 10 bytes into a region of size 9 [-Wformat-truncation=]
   snprintf(out, buflen, "%02d:%02d:%02d", hours, minutes, seconds);
                          ^~~~
meta.c:582:25: note: directive argument in the range [0, 2147483647]
   snprintf(out, buflen, "%02d:%02d:%02d", hours, minutes, seconds);
                         ^~~~~~~~~~~~~~~~
meta.c:582:25: note: directive argument in the range [0, 60]
In file included from /usr/include/stdio.h:867, from meta.c:17:
/usr/include/bits/stdio2.h:67:10: note: '__builtin___snprintf_chk' output between 9 and 26 bytes into a destination of size 9
   return __builtin___snprintf_chk (__s, __n, __USE_FORTIFY_LEVEL - 1,
          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
meta.c:580:26: warning: '%02d' directive output may be truncated writing between 2 and 10 bytes into a region of size 9 [-Wformat-truncation=]
   snprintf(out, buflen, "%02d:%02d", hours, minutes);
meta.c:580:25: note: directive argument in the range [0, 2147483647] snprintf(out, buflen, "%02d:%02d", hours, minutes);
                         ^~~~~~~~~~~
meta.c:580:25: note: directive argument in the range [0, 60] In file included from /usr/include/stdio.h:867, from meta.c:17:
/usr/include/bits/stdio2.h:67:10: note: '__builtin___snprintf_chk' output between 6 and 14 bytes into a destination of size 9
   return __builtin___snprintf_chk (__s, __n, __USE_FORTIFY_LEVEL - 1,
          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
There is no bug here, compiler doesn't know that those intergers only contains numbers
in the two digit range.

I suggest to "fix" this by increasing out[] to 32 bytes.

> diff --git a/src/meta.c b/src/meta.c
> index 1e8964e..00ff267 100644
> --- a/src/meta.c
> +++ b/src/meta.c
> @@ -37,6 +37,10 @@
>  #include <iface.h>
>  #include <json.h>
>  
> +#define _XOPEN_SOURCE
> +#define __USE_XOPEN
> +#include <time.h>
> +
>  static struct symbol_table *realm_tbl;
>  void realm_table_meta_init(void)
>  {
> @@ -383,6 +387,313 @@ const struct datatype ifname_type = {
>  	.basetype	= &string_type,
>  };
>  
> +static void date_type_print(const struct expr *expr, struct output_ctx *octx)
> +{
> +	char timestr[21];
> +	struct tm *tm, *cur_tm;
> +	uint64_t tstamp = mpz_get_uint64(expr->value);

I would suggest to always order in reverse xmas tree, i.e.

> +	uint64_t tstamp = mpz_get_uint64(expr->value);
> +	struct tm *tm, *cur_tm;
> +	char timestr[21];

just to get used to this.  The networking maintainer (David Miller) prefers
it this way for kernel patches (yes, this is an nftables patch but getting
used to this ordering is a good idea.

> +	/* Convert from nanoseconds to seconds */
> +	tstamp /= 1000000000L;

[..]

This looks good to me, but there are three usability issues.

The worst one first:

nft add rule filter input hour 23:15-00:22 counter

This works. But this fails:
nft add rule filter input hour 23:15-03:22  counter
Error: Range has zero or negative size

Because values are converted to UTC, the first one will be a range from
21:15 to 22:22 UTC, so left < right. Second one is not.

The obvious workaround:

meta hour < "04:22" will NOT match at 00:28 (GMT+2), as its still 22:28 in
the UTC time zone.

It will match once local time is past 0 hours UTC.

I suggest to try to fix this from the evaluation step, by
swapping left and right and inverting the match.

So 76500-8520 (left larger right) turns into "!= 8520-76500",
which appears to do the right thing.

shape and I have no idea how to fix this without using/relying on kernel time zone.

Even when relying on kernel time zone for everything, I don't see
how we can support cross-day ("22:23-00:42") matching, as the range is
invalid.

Second problem:
For same reason, "Weekday" matching is broken.
If its Monday (00:31), rule that asks for monday won't match, because
day is relative to UTC, and since its 22:31 UTC, its still Sunday.

This is unusable when in a time zone much further away.

Only solution I see is to change kernel patch to rely on
sys_tz, just like xt_time, with all the pain this brings.

Third problem, but NOT directly related to this patch, its due to
existing range expression forcing numeric output:

All time based ranges get printed like this:
meta hour 72600-72720
meta time 1563142680000000000-1563143400000000000

I think we should relax range printing and only force numeric for
the new time/hour/day when user did specify the --seconds option.

Ander, you can fix this by finding out where the range print
function gets called and by only propagating NFT_CTX_OUTPUT_NUMERIC_TIME
when user asked for it instead of doing so unconditionally.

Currently we always force numeric because something like
"ssh-http" or "daemon-ftpd" looks very silly, but in case of hours and
days I don't think it makes sense to do the raw-printing by default.

> +static struct error_record *hour_type_parse(const struct expr *sym,
> +					    struct expr **res)
> +{
> +	time_t ts;
> +	char *endptr;
> +	struct tm tm, *cur_tm;
> +	uint64_t result = 0;
> +	struct error_record *er;
> +
> +	memset(&tm, 0, sizeof(struct tm));
> +
> +	/* First, try to parse it as a number */
> +	result = strtoul(sym->identifier, (char **) &endptr, 10);
> +	if (*endptr == '\0' && endptr != sym->identifier)
> +		goto success;
> +
> +	result = 0;
> +
> +	/* Obtain current tm, so that we can substract tm_gmtoff */
> +	ts = time(NULL);
> +	cur_tm = localtime(&ts);
> +
> +	if (strptime(sym->identifier, "%T", &tm))
> +		goto convert;
> +	if (strptime(sym->identifier, "%R", &tm))
> +		goto convert;
> +
> +	if ((er = time_parse(&sym->location, sym->identifier, &result)) == NULL) {
> +		result /= 1000;
> +		goto convert;
> +	}

Seems this function accepts 'meta hour "04:22-23:15"'.   This should cause
an error instead of letting user wonder why things don't work as expected:

This passes the string "04:22-23:15" as "04:22".

So this should fail, as this contains invalid '-' character, respectively
has too many ":".

> +	[NFT_META_TIME_HOUR]	= META_TEMPLATE("hour", &hour_type,
> +						8 * BITS_PER_BYTE,
> +						BYTEORDER_HOST_ENDIAN),

Why does hour need 64bits of register space?  I think u32 is enough?

>  static bool meta_key_is_unqualified(enum nft_meta_keys key)
> diff --git a/src/parser_bison.y b/src/parser_bison.y
> index 670e91f..26b64da 100644
> --- a/src/parser_bison.y
> +++ b/src/parser_bison.y
> @@ -415,6 +415,7 @@ int nft_lex(void *, void *, void *);
>  %token IIFGROUP			"iifgroup"
>  %token OIFGROUP			"oifgroup"
>  %token CGROUP			"cgroup"
> +%token TIME			"time"
>  
>  %token CLASSID			"classid"
>  %token NEXTHOP			"nexthop"
> @@ -3886,6 +3887,9 @@ meta_key_unqualified	:	MARK		{ $$ = NFT_META_MARK; }
>  			|       OIFGROUP	{ $$ = NFT_META_OIFGROUP; }
>  			|       CGROUP		{ $$ = NFT_META_CGROUP; }
>  			|       IPSEC		{ $$ = NFT_META_SECPATH; }
> +			|       TIME		{ $$ = NFT_META_TIME; }
> +			|       DAY		{ $$ = NFT_META_TIME_DAY; }
> +			|       HOUR		{ $$ = NFT_META_TIME_HOUR; }
>  			;
>  
>  meta_stmt		:	META	meta_key	SET	stmt_expr
> diff --git a/src/scanner.l b/src/scanner.l
> index d1f6e87..bd28141 100644
> --- a/src/scanner.l
> +++ b/src/scanner.l
> @@ -411,7 +411,9 @@ addrstring	({macaddr}|{ip4addr}|{ip6addr})
>  "sack2"			{ return SACK2; }
>  "sack3"			{ return SACK3; }
>  "sack-permitted"	{ return SACK_PERMITTED; }
> -"timestamp"		{ return TIMESTAMP; }

Won't that break the tcp timestamp option?  I think this token needs
to stay where it is.

> +"time"			{ return TIME; }
> +"day"			{ return DAY; }
> +"hour"			{ return HOUR; }

Causes:
src/scanner.l:424: warning, rule cannot be matched
src/scanner.l:425: warning, rule cannot be matched

as DAY and HOUR rules are duplicates, just remove them.
 
> diff --git a/tests/py/ip/meta.t.payload b/tests/py/ip/meta.t.payload
> index 322c087..c9bc09f 100644
> --- a/tests/py/ip/meta.t.payload
> +++ b/tests/py/ip/meta.t.payload
> @@ -1,3 +1,87 @@
> +# meta time "1970-05-23 21:07:14" drop
> +ip meta-test input
> +  [ meta load unknown => reg 1 ]

This "unknown" is coming from libnftnl.
You need to fix up meta_key2str_array[] in src/expr/meta.c in libnftnl.

This should say "time", "time_day" or similar.
Also, shouldn't this come in patch #2 when those tests get added?

Alternatively, you can squash your three patches into one, it would
 be fine I think.
Florian Westphal July 14, 2019, 11:34 p.m. UTC | #2
Florian Westphal <fw@strlen.de> wrote:
> Ander Juaristi <a@juaristi.eus> wrote:
> This looks good to me, but there are three usability issues.
> 
> The worst one first:
> 
> nft add rule filter input hour 23:15-00:22 counter
> 
> This works. But this fails:
> nft add rule filter input hour 23:15-03:22  counter
> Error: Range has zero or negative size
> 
> Because values are converted to UTC, the first one will be a range from
> 21:15 to 22:22 UTC, so left < right. Second one is not.
> 
> The obvious workaround:
> 
> meta hour < "04:22" will NOT match at 00:28 (GMT+2), as its still 22:28 in
> the UTC time zone.
> 
> It will match once local time is past 0 hours UTC.
> 
> I suggest to try to fix this from the evaluation step, by
> swapping left and right and inverting the match.
> 
> So 76500-8520 (left larger right) turns into "!= 8520-76500",
> which appears to do the right thing.
> 
> shape and I have no idea how to fix this without using/relying on kernel time zone.

Argh, I reworded this and forgot to delete this half-sentence above.

> Even when relying on kernel time zone for everything, I don't see
> how we can support cross-day ("22:23-00:42") matching, as the range is
> invalid.

And that as well of course, swap and invert should work just fine.

> Second problem:
> Only solution I see is to change kernel patch to rely on
> sys_tz, just like xt_time, with all the pain this brings.

This stands, as the weekday is computed in the kernel, we will
need to bring sys_tz into this on the kernel side, the current
code uses UTC so we could be several hours off.

This can be restricted to the 'DAY' case of course.
Ander Juaristi July 18, 2019, 12:56 p.m. UTC | #3
On 15/7/19 1:34, Florian Westphal wrote:
>> Even when relying on kernel time zone for everything, I don't see
>> how we can support cross-day ("22:23-00:42") matching, as the range is
>> invalid.
> 
> And that as well of course, swap and invert should work just fine.
> 
>> Second problem:
>> Only solution I see is to change kernel patch to rely on
>> sys_tz, just like xt_time, with all the pain this brings.
> 
> This stands, as the weekday is computed in the kernel, we will
> need to bring sys_tz into this on the kernel side, the current
> code uses UTC so we could be several hours off.
> 
> This can be restricted to the 'DAY' case of course.
> 

I see... Thank you. You saved me hours of work figuring this out.

So, for the TIME case we just swap left and right, and for the DAY case,
just add (tz_minuteswest * 60) to the seconds before breaking it into 
day/mon/year?

And what does tz_dsttime do? gettimeofday(2) man says it is there for 
historical reasons and should be ignored on Linux. But I don't know what 
is it for in the kernel.
Florian Westphal July 18, 2019, 1:41 p.m. UTC | #4
Ander Juaristi <a@juaristi.eus> wrote:
> On 15/7/19 1:34, Florian Westphal wrote:
> > > Even when relying on kernel time zone for everything, I don't see
> > > how we can support cross-day ("22:23-00:42") matching, as the range is
> > > invalid.
> > 
> > And that as well of course, swap and invert should work just fine.
> > 
> > > Second problem:
> > > Only solution I see is to change kernel patch to rely on
> > > sys_tz, just like xt_time, with all the pain this brings.
> > 
> > This stands, as the weekday is computed in the kernel, we will
> > need to bring sys_tz into this on the kernel side, the current
> > code uses UTC so we could be several hours off.
> > 
> > This can be restricted to the 'DAY' case of course.
> > 
> 
> I see... Thank you. You saved me hours of work figuring this out.

Giving hints is what I am supposed to do :-)

> So, for the TIME case we just swap left and right, and for the DAY case,
> just add (tz_minuteswest * 60) to the seconds before breaking it into
> day/mon/year?

Yes, swap and eq -> not eq -- at least I think that should work and
would make something like 18:00-07:00 work.

> And what does tz_dsttime do? gettimeofday(2) man says it is there for
> historical reasons and should be ignored on Linux. But I don't know what is
> it for in the kernel.

The kernel has no idea what a time zone is, the tz_dsttime setting
comes from userspace, typically during boot via hwclock(8).

IIRC it only flags if we're in 'daylight saving time' or not,
i.e. 0 for CET and 1 in CEST case.
diff mbox series

Patch

diff --git a/include/datatype.h b/include/datatype.h
index 63617eb..1f46eb0 100644
--- a/include/datatype.h
+++ b/include/datatype.h
@@ -90,6 +90,9 @@  enum datatypes {
 	TYPE_CT_EVENTBIT,
 	TYPE_IFNAME,
 	TYPE_IGMP_TYPE,
+	TYPE_TIME_DATE,
+	TYPE_TIME_HOUR,
+	TYPE_TIME_DAY,
 	__TYPE_MAX
 };
 #define TYPE_MAX		(__TYPE_MAX - 1)
diff --git a/include/linux/netfilter/nf_tables.h b/include/linux/netfilter/nf_tables.h
index 7bdb234..ce621ed 100644
--- a/include/linux/netfilter/nf_tables.h
+++ b/include/linux/netfilter/nf_tables.h
@@ -793,6 +793,9 @@  enum nft_exthdr_attributes {
  * @NFT_META_SECPATH: boolean, secpath_exists (!!skb->sp)
  * @NFT_META_IIFKIND: packet input interface kind name (dev->rtnl_link_ops->kind)
  * @NFT_META_OIFKIND: packet output interface kind name (dev->rtnl_link_ops->kind)
+ * @NFT_META_TIME: a UNIX timestamp
+ * @NFT_META_TIME_DAY: day of week
+ * @NFT_META_TIME_HOUR: hour of day
  */
 enum nft_meta_keys {
 	NFT_META_LEN,
@@ -823,6 +826,9 @@  enum nft_meta_keys {
 	NFT_META_SECPATH,
 	NFT_META_IIFKIND,
 	NFT_META_OIFKIND,
+	NFT_META_TIME,
+	NFT_META_TIME_DAY,
+	NFT_META_TIME_HOUR,
 };
 
 /**
diff --git a/include/meta.h b/include/meta.h
index a49b4ff..a62a130 100644
--- a/include/meta.h
+++ b/include/meta.h
@@ -41,6 +41,9 @@  extern const struct datatype uid_type;
 extern const struct datatype devgroup_type;
 extern const struct datatype pkttype_type;
 extern const struct datatype ifname_type;
+extern const struct datatype date_type;
+extern const struct datatype hour_type;
+extern const struct datatype day_type;
 
 extern struct symbol_table *devgroup_tbl;
 
diff --git a/include/nftables.h b/include/nftables.h
index ed446e2..b807004 100644
--- a/include/nftables.h
+++ b/include/nftables.h
@@ -62,6 +62,11 @@  static inline bool nft_output_guid(const struct output_ctx *octx)
 	return octx->flags & NFT_CTX_OUTPUT_GUID;
 }
 
+static inline bool nft_output_seconds(const struct output_ctx *octx)
+{
+	return octx->flags & NFT_CTX_OUTPUT_NUMERIC_TIME;
+}
+
 static inline bool nft_output_numeric_proto(const struct output_ctx *octx)
 {
 	return octx->flags & NFT_CTX_OUTPUT_NUMERIC_PROTO;
diff --git a/include/nftables/libnftables.h b/include/nftables/libnftables.h
index e39c588..b84297b 100644
--- a/include/nftables/libnftables.h
+++ b/include/nftables/libnftables.h
@@ -52,9 +52,11 @@  enum {
 	NFT_CTX_OUTPUT_NUMERIC_PROTO	= (1 << 7),
 	NFT_CTX_OUTPUT_NUMERIC_PRIO     = (1 << 8),
 	NFT_CTX_OUTPUT_NUMERIC_SYMBOL	= (1 << 9),
+	NFT_CTX_OUTPUT_NUMERIC_TIME     = (1 << 10),
 	NFT_CTX_OUTPUT_NUMERIC_ALL	= (NFT_CTX_OUTPUT_NUMERIC_PROTO |
 					   NFT_CTX_OUTPUT_NUMERIC_PRIO |
-					   NFT_CTX_OUTPUT_NUMERIC_SYMBOL),
+					   NFT_CTX_OUTPUT_NUMERIC_SYMBOL |
+					   NFT_CTX_OUTPUT_NUMERIC_TIME),
 };
 
 unsigned int nft_ctx_output_get_flags(struct nft_ctx *ctx);
diff --git a/src/datatype.c b/src/datatype.c
index 6d6826e..0a00535 100644
--- a/src/datatype.c
+++ b/src/datatype.c
@@ -71,6 +71,9 @@  static const struct datatype *datatypes[TYPE_MAX + 1] = {
 	[TYPE_BOOLEAN]		= &boolean_type,
 	[TYPE_IFNAME]		= &ifname_type,
 	[TYPE_IGMP_TYPE]	= &igmp_type_type,
+	[TYPE_TIME_DATE]	= &date_type,
+	[TYPE_TIME_HOUR]	= &hour_type,
+	[TYPE_TIME_DAY]		= &day_type,
 };
 
 const struct datatype *datatype_lookup(enum datatypes type)
diff --git a/src/main.c b/src/main.c
index 9a50f30..73036a9 100644
--- a/src/main.c
+++ b/src/main.c
@@ -43,8 +43,9 @@  enum opt_vals {
 	OPT_NUMERIC_PRIO	= 'y',
 	OPT_NUMERIC_PROTO	= 'p',
 	OPT_INVALID		= '?',
+	OPT_SECONDS		= 't',
 };
-#define OPTSTRING	"hvcf:iI:jvnsNaeSupyp"
+#define OPTSTRING	"hvcf:iI:jvnsNaeSupypt"
 
 static const struct option options[] = {
 	{
@@ -114,6 +115,10 @@  static const struct option options[] = {
 		.name		= "numeric-priority",
 		.val		= OPT_NUMERIC_PRIO,
 	},
+	{
+		.name		= "seconds",
+		.val		= OPT_SECONDS,
+	},
 	{
 		.name		= NULL
 	}
@@ -143,6 +148,7 @@  static void show_help(const char *name)
 "  -a, --handle			Output rule handle.\n"
 "  -e, --echo			Echo what has been added, inserted or replaced.\n"
 "  -I, --includepath <directory>	Add <directory> to the paths searched for include files. Default is: %s\n"
+"  -t, --seconds                Show hour values in seconds since midnight.\n"
 "  --debug <level [,level...]>	Specify debugging level (scanner, parser, eval, netlink, mnl, proto-ctx, segtree, all)\n"
 "\n",
 	name, DEFAULT_INCLUDE_PATH);
@@ -282,6 +288,9 @@  int main(int argc, char * const *argv)
 		case OPT_GUID:
 			output_flags |= NFT_CTX_OUTPUT_GUID;
 			break;
+		case OPT_SECONDS:
+			output_flags |= NFT_CTX_OUTPUT_NUMERIC_TIME;
+			break;
 		case OPT_NUMERIC_PRIO:
 			output_flags |= NFT_CTX_OUTPUT_NUMERIC_PRIO;
 			break;
diff --git a/src/meta.c b/src/meta.c
index 1e8964e..00ff267 100644
--- a/src/meta.c
+++ b/src/meta.c
@@ -37,6 +37,10 @@ 
 #include <iface.h>
 #include <json.h>
 
+#define _XOPEN_SOURCE
+#define __USE_XOPEN
+#include <time.h>
+
 static struct symbol_table *realm_tbl;
 void realm_table_meta_init(void)
 {
@@ -383,6 +387,313 @@  const struct datatype ifname_type = {
 	.basetype	= &string_type,
 };
 
+static void date_type_print(const struct expr *expr, struct output_ctx *octx)
+{
+	char timestr[21];
+	struct tm *tm, *cur_tm;
+	uint64_t tstamp = mpz_get_uint64(expr->value);
+
+	/* Convert from nanoseconds to seconds */
+	tstamp /= 1000000000L;
+
+	if (!nft_output_seconds(octx)) {
+		/* Obtain current tm, to add tm_gmtoff to the timestamp */
+		cur_tm = localtime((time_t *) &tstamp);
+
+		if (cur_tm)
+			tstamp += cur_tm->tm_gmtoff;
+
+		if ((tm = gmtime((time_t *) &tstamp)) != NULL &&
+			strftime(timestr, sizeof(timestr) - 1, "%F %T", tm))
+			nft_print(octx, "\"%s\"", timestr);
+		else
+			nft_print(octx, "Error converting timestamp to printed time");
+
+		return;
+	}
+
+	expr_basetype(expr)->print(expr, octx);
+}
+
+static time_t parse_iso_date(const char *sym)
+{
+	time_t ts;
+	struct tm tm, *cur_tm;
+
+	memset(&tm, 0, sizeof(struct tm));
+
+	if (strptime(sym, "%F %T", &tm))
+		goto success;
+	if (strptime(sym, "%F %R", &tm))
+		goto success;
+	if (strptime(sym, "%F", &tm))
+		goto success;
+
+	return -1;
+
+success:
+	/*
+	 * Overwriting TZ is problematic if we're parsing hour types in this same process,
+	 * hence I'd rather use timegm() which doesn't take into account the TZ env variable,
+	 * even though it's Linux-specific.
+	 */
+	ts = timegm(&tm);
+
+	/* Obtain current tm as well (at the specified time), so that we can substract tm_gmtoff */
+	cur_tm = localtime(&ts);
+
+	if (ts == (time_t) -1 || cur_tm == NULL)
+		return ts;
+
+	/* Substract tm_gmtoff to get the current time */
+	return ts - cur_tm->tm_gmtoff;
+}
+
+static struct error_record *date_type_parse(const struct expr *sym,
+					    struct expr **res)
+{
+	time_t tstamp;
+	const char *endptr = sym->identifier;
+
+	if ((tstamp = parse_iso_date(sym->identifier)) != -1)
+		goto success;
+
+	tstamp = strtoul(sym->identifier, (char **) &endptr, 10);
+	if (*endptr == '\0' && endptr != sym->identifier)
+		goto success;
+
+	return error(&sym->location, "Cannot parse date");
+
+success:
+	/* Convert to nanoseconds */
+	tstamp *= 1000000000L;
+	*res = constant_expr_alloc(&sym->location, sym->dtype,
+				   BYTEORDER_HOST_ENDIAN,
+				   sizeof(uint64_t) * BITS_PER_BYTE,
+				   &tstamp);
+	return NULL;
+}
+
+static void day_type_print(const struct expr *expr, struct output_ctx *octx)
+{
+	const char *days[] = {
+		"Sunday",
+		"Monday",
+		"Tuesday",
+		"Wednesday",
+		"Thursday",
+		"Friday",
+		"Saturday"
+	};
+
+	uint8_t daynum = mpz_get_uint8(expr->value), numdays = array_size(days);
+
+	if (daynum >= numdays) {
+		nft_print(octx, "Unknown day");
+		return;
+	}
+
+	if (nft_output_seconds(octx))
+		nft_print(octx, "%d", daynum);
+	else
+		nft_print(octx, "\"%s\"", days[daynum]);
+}
+
+static int get_day_num_from_number(const char *sym)
+{
+	char c = *sym;
+
+	if (c >= '0' && c <= '6')
+		return (c - '0');
+
+	return -1;
+}
+
+static int get_day_num_from_string(const char *sym, int symlen)
+{
+	const char *days[] = {
+		"Sunday",
+		"Monday",
+		"Tuesday",
+		"Wednesday",
+		"Thursday",
+		"Friday",
+		"Saturday"
+	};
+
+	int daylen;
+	int daynum = -1, numdays = array_size(days);
+
+	for (int i = 0; i < numdays && daynum == -1; i++) {
+		daylen = strlen(days[i]);
+
+		if (strncasecmp(sym,
+				days[i],
+				min(symlen, daylen)) == 0)
+			daynum = i;
+	}
+
+	return daynum;
+}
+
+static struct error_record *day_type_parse(const struct expr *sym,
+					   struct expr **res)
+{
+	int daynum;
+	int symlen = strlen(sym->identifier);
+
+	if (symlen < 3) {
+		if (symlen != 1 ||
+			(daynum = get_day_num_from_number(sym->identifier)) == -1)
+			goto error_too_short_day;
+	} else {
+		if ((daynum = get_day_num_from_string(sym->identifier, symlen)) == -1)
+			goto error_generic;
+	}
+
+	*res = constant_expr_alloc(&sym->location, sym->dtype,
+				   BYTEORDER_HOST_ENDIAN,
+				   1 * BITS_PER_BYTE,
+				   &daynum);
+	return NULL;
+
+error_too_short_day:
+	return error(&sym->location, "Day name must be at least three characters long");
+
+error_generic:
+	return error(&sym->location, "Cannot parse day");
+}
+
+static void __hour_type_print_r(int hours, int minutes, int seconds, char *out, size_t buflen)
+{
+	if (minutes == 60)
+		return __hour_type_print_r(++hours, 0, seconds, out, buflen);
+	else if (minutes > 60)
+		return __hour_type_print_r((int) (minutes / 60), minutes % 60, seconds, out, buflen);
+
+	if (seconds == 60)
+		return __hour_type_print_r(hours, ++minutes, 0, out, buflen);
+	else if (seconds > 60)
+		return __hour_type_print_r(hours, (int) (seconds / 60), seconds % 60, out, buflen);
+
+	if (seconds == 0)
+		snprintf(out, buflen, "%02d:%02d", hours, minutes);
+	else
+		snprintf(out, buflen, "%02d:%02d:%02d", hours, minutes, seconds);
+}
+
+static void hour_type_print(const struct expr *expr, struct output_ctx *octx)
+{
+	char out[9];
+	time_t ts;
+	struct tm *cur_tm;
+	uint64_t seconds = mpz_get_uint64(expr->value);
+
+	if (!nft_output_seconds(octx)) {
+		/* Obtain current tm, so that we can add tm_gmtoff */
+		ts = time(NULL);
+		cur_tm = localtime(&ts);
+
+		if (cur_tm)
+			seconds = (seconds + cur_tm->tm_gmtoff) % 86400;
+
+		__hour_type_print_r(0, 0, seconds, out, sizeof(out));
+		nft_print(octx, "\"%s\"", out);
+
+		return;
+	}
+
+	expr_basetype(expr)->print(expr, octx);
+}
+
+static struct error_record *hour_type_parse(const struct expr *sym,
+					    struct expr **res)
+{
+	time_t ts;
+	char *endptr;
+	struct tm tm, *cur_tm;
+	uint64_t result = 0;
+	struct error_record *er;
+
+	memset(&tm, 0, sizeof(struct tm));
+
+	/* First, try to parse it as a number */
+	result = strtoul(sym->identifier, (char **) &endptr, 10);
+	if (*endptr == '\0' && endptr != sym->identifier)
+		goto success;
+
+	result = 0;
+
+	/* Obtain current tm, so that we can substract tm_gmtoff */
+	ts = time(NULL);
+	cur_tm = localtime(&ts);
+
+	if (strptime(sym->identifier, "%T", &tm))
+		goto convert;
+	if (strptime(sym->identifier, "%R", &tm))
+		goto convert;
+
+	if ((er = time_parse(&sym->location, sym->identifier, &result)) == NULL) {
+		result /= 1000;
+		goto convert;
+	}
+
+	return er;
+
+convert:
+	/* Convert the hour to the number of seconds since midnight */
+	if (result == 0)
+		result = tm.tm_hour * 3600 + tm.tm_min * 60 + tm.tm_sec;
+
+	/* Substract tm_gmtoff to get the current time */
+	if (cur_tm) {
+		if ((long int) result >= cur_tm->tm_gmtoff)
+			result = (result - cur_tm->tm_gmtoff) % 86400;
+		else
+			result = 86400 - cur_tm->tm_gmtoff + result;
+	}
+
+success:
+	*res = constant_expr_alloc(&sym->location, sym->dtype,
+				   BYTEORDER_HOST_ENDIAN,
+				   sizeof(uint64_t) * BITS_PER_BYTE,
+				   &result);
+	return NULL;
+}
+
+const struct datatype date_type = {
+	.type = TYPE_TIME_DATE,
+	.name = "time",
+	.desc = "Relative time of packet reception",
+	.byteorder = BYTEORDER_HOST_ENDIAN,
+	.size = sizeof(uint64_t) * BITS_PER_BYTE,
+	.basetype = &integer_type,
+	.print = date_type_print,
+	.parse = date_type_parse,
+};
+
+const struct datatype day_type = {
+	.type = TYPE_TIME_DAY,
+	.name = "day",
+	.desc = "Day of week of packet reception",
+	.byteorder = BYTEORDER_HOST_ENDIAN,
+	.size = 1 * BITS_PER_BYTE,
+	.basetype = &integer_type,
+	.print = day_type_print,
+	.parse = day_type_parse,
+};
+
+const struct datatype hour_type = {
+	.type = TYPE_TIME_HOUR,
+	.name = "hour",
+	.desc = "Hour of day of packet reception",
+	.byteorder = BYTEORDER_HOST_ENDIAN,
+	.size = sizeof(uint64_t) * BITS_PER_BYTE,
+	.basetype = &integer_type,
+	.print = hour_type_print,
+	.parse = hour_type_parse,
+};
+
 const struct meta_template meta_templates[] = {
 	[NFT_META_LEN]		= META_TEMPLATE("length",    &integer_type,
 						4 * 8, BYTEORDER_HOST_ENDIAN),
@@ -450,6 +761,15 @@  const struct meta_template meta_templates[] = {
 	[NFT_META_OIFKIND]	= META_TEMPLATE("oifkind",   &ifname_type,
 						IFNAMSIZ * BITS_PER_BYTE,
 						BYTEORDER_HOST_ENDIAN),
+	[NFT_META_TIME]		= META_TEMPLATE("time",   &date_type,
+						8 * BITS_PER_BYTE,
+						BYTEORDER_HOST_ENDIAN),
+	[NFT_META_TIME_DAY]	= META_TEMPLATE("day", &day_type,
+						1 * BITS_PER_BYTE,
+						BYTEORDER_HOST_ENDIAN),
+	[NFT_META_TIME_HOUR]	= META_TEMPLATE("hour", &hour_type,
+						8 * BITS_PER_BYTE,
+						BYTEORDER_HOST_ENDIAN),
 };
 
 static bool meta_key_is_unqualified(enum nft_meta_keys key)
diff --git a/src/parser_bison.y b/src/parser_bison.y
index 670e91f..26b64da 100644
--- a/src/parser_bison.y
+++ b/src/parser_bison.y
@@ -415,6 +415,7 @@  int nft_lex(void *, void *, void *);
 %token IIFGROUP			"iifgroup"
 %token OIFGROUP			"oifgroup"
 %token CGROUP			"cgroup"
+%token TIME			"time"
 
 %token CLASSID			"classid"
 %token NEXTHOP			"nexthop"
@@ -3886,6 +3887,9 @@  meta_key_unqualified	:	MARK		{ $$ = NFT_META_MARK; }
 			|       OIFGROUP	{ $$ = NFT_META_OIFGROUP; }
 			|       CGROUP		{ $$ = NFT_META_CGROUP; }
 			|       IPSEC		{ $$ = NFT_META_SECPATH; }
+			|       TIME		{ $$ = NFT_META_TIME; }
+			|       DAY		{ $$ = NFT_META_TIME_DAY; }
+			|       HOUR		{ $$ = NFT_META_TIME_HOUR; }
 			;
 
 meta_stmt		:	META	meta_key	SET	stmt_expr
diff --git a/src/scanner.l b/src/scanner.l
index d1f6e87..bd28141 100644
--- a/src/scanner.l
+++ b/src/scanner.l
@@ -411,7 +411,9 @@  addrstring	({macaddr}|{ip4addr}|{ip6addr})
 "sack2"			{ return SACK2; }
 "sack3"			{ return SACK3; }
 "sack-permitted"	{ return SACK_PERMITTED; }
-"timestamp"		{ return TIMESTAMP; }
+"time"			{ return TIME; }
+"day"			{ return DAY; }
+"hour"			{ return HOUR; }
 
 "kind"			{ return KIND; }
 "count"			{ return COUNT; }
diff --git a/tests/py/ip/meta.t.payload b/tests/py/ip/meta.t.payload
index 322c087..c9bc09f 100644
--- a/tests/py/ip/meta.t.payload
+++ b/tests/py/ip/meta.t.payload
@@ -1,3 +1,87 @@ 
+# meta time "1970-05-23 21:07:14" drop
+ip meta-test input
+  [ meta load unknown => reg 1 ]
+  [ cmp eq reg 1 0x74a8f400 0x002bd849 ]
+  [ immediate reg 0 drop ]
+
+# meta time 12341234 drop
+ip meta-test input
+  [ meta load unknown => reg 1 ]
+  [ cmp eq reg 1 0x74a8f400 0x002bd849 ]
+  [ immediate reg 0 drop ]
+
+# meta time "2019-06-21 17:00:00" drop
+ip meta-test input
+  [ meta load unknown => reg 1 ]
+  [ cmp eq reg 1 0x767d6000 0x15aa3ebc ]
+  [ immediate reg 0 drop ]
+
+# meta time "2019-07-01 00:00:00" drop
+ip meta-test input
+  [ meta load unknown => reg 1 ]
+  [ cmp eq reg 1 0xe750c000 0x15ad18e0 ]
+  [ immediate reg 0 drop ]
+
+# meta time "2019-07-01 00:01:00" drop
+ip meta-test input
+  [ meta load unknown => reg 1 ]
+  [ cmp eq reg 1 0xdf981800 0x15ad18ee ]
+  [ immediate reg 0 drop ]
+
+# meta time "2019-07-01 00:00:01" drop
+ip meta-test input
+  [ meta load unknown => reg 1 ]
+  [ cmp eq reg 1 0x22eb8a00 0x15ad18e1 ]
+  [ immediate reg 0 drop ]
+
+# meta day "Sat" drop
+ip test-ip4 input
+  [ meta load unknown => reg 1 ]
+  [ cmp eq reg 1 0x00000006 ]
+  [ immediate reg 0 drop ]
+
+# meta day "Saturday" drop
+ip test-ip4 input
+  [ meta load unknown => reg 1 ]
+  [ cmp eq reg 1 0x00000006 ]
+  [ immediate reg 0 drop ]
+
+# meta day 6 drop
+ip meta-test input
+  [ meta load unknown => reg 1 ]
+  [ cmp eq reg 1 0x00000006 ]
+  [ immediate reg 0 drop ]
+
+# meta hour "17:00" drop
+ip test-ip4 input
+  [ meta load unknown => reg 1 ]
+  [ cmp eq reg 1 0x0000d2f0 0x00000000 ]
+  [ immediate reg 0 drop ]
+
+# meta hour "17:00:00" drop
+ip test-ip4 input
+  [ meta load unknown => reg 1 ]
+  [ cmp eq reg 1 0x0000d2f0 0x00000000 ]
+  [ immediate reg 0 drop ]
+
+# meta hour "17:00:01" drop
+ip meta-test input
+  [ meta load unknown => reg 1 ]
+  [ cmp eq reg 1 0x0000d2f1 0x00000000 ]
+  [ immediate reg 0 drop ]
+
+# meta hour "00:00" drop
+ip meta-test input
+  [ meta load unknown => reg 1 ]
+  [ cmp eq reg 1 0x00013560 0x00000000 ]
+  [ immediate reg 0 drop ]
+
+# meta hour "00:01" drop
+ip meta-test input
+  [ meta load unknown => reg 1 ]
+  [ cmp eq reg 1 0x0001359c 0x00000000 ]
+  [ immediate reg 0 drop ]
+
 # icmp type echo-request
 ip test-ip4 input
   [ meta load l4proto => reg 1 ]