Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
L
libvmod-acltools
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
uplex-varnish
libvmod-acltools
Commits
f54521b3
Unverified
Commit
f54521b3
authored
Mar 08, 2023
by
Nils Goroll
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Initial public release
parent
f7253ecd
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
652 additions
and
30 deletions
+652
-30
.gitignore
.gitignore
+1
-1
INSTALL.rst
INSTALL.rst
+32
-0
README.rst
README.rst
+90
-0
configure.ac
configure.ac
+2
-2
vmod_acltools.c
src/vmod_acltools.c
+245
-4
vmod_acltools.vcc
src/vmod_acltools.vcc
+43
-12
vmod_acltools.vtc
src/vtc/vmod_acltools.vtc
+239
-11
No files found.
.gitignore
View file @
f54521b3
...
...
@@ -30,7 +30,7 @@ stamp-h1
# vmodtool
vcc_*_if.[ch]
vmod_
*
.rst
vmod_
acltools
.rst
# man
...
...
INSTALL.rst
0 → 100644
View file @
f54521b3
INSTALLATION
============
Building from source
~~~~~~~~~~~~~~~~~~~~
The VMOD is built on a system where an instance of Varnish is
installed, and the auto-tools will attempt to locate the Varnish
instance, and then pull in libraries and other support files from
there.
Quick start
-----------
This sequence should be enough in typical setups:
1. ``./bootstrap`` (for git-installation)
3. ``make``
4. ``make check`` (regression tests)
5. ``make install`` (may require root: sudo make install)
Alternative configs
-------------------
If you have installed Varnish to a non-standard directory, call
``autogen.sh`` and ``configure`` with ``PKG_CONFIG_PATH`` pointing to
the appropriate path. For example, when varnishd configure was called
with ``--prefix=$PREFIX``, use::
PKG_CONFIG_PATH=${PREFIX}/lib/pkgconfig
ACLOCAL_PATH=${PREFIX}/share/aclocal
export PKG_CONFIG_PATH ACLOCAL_PATH
README.rst
0 → 100644
View file @
f54521b3
=================================================
Access Control List (ACL) Tools for Varnish-Cache
=================================================
.. role:: ref(emphasis)
This project provides additional tools for Access Control Lists (ACLs)
in Varnish-Cache.
PROJECT RESOURCES
=================
* The primary repository is at
https://code.uplex.de/uplex-varnish/libvmod-acltools
This server does not accept user registrations, so please use ...
* the mirror at https://gitlab.com/uplex/varnish/libvmod-acltools for issues,
merge requests and all other interactions.
INTRODUCTION
============
For now, this project adds a single missing function to Varnish-Cache:
To create dynamic Access Control Lists with a single entry from
strings in VCL.
Example::
import acltools;
sub vcl_recv {
if (client.ip ~ acltools.dyn_single(req.http.acl)) {
return (synth(200, "matched"));
} else {
return (synth(403, "denied"));
}
}
In this example, an ACL gets created from the `acl` request header
header and is then used for a match of `client.ip`.
The acl entry is expected in the format::
<ip>[/<mask>]
.. _vmod_acltools.man.rst: src/vmod_acltools.man.rst
See the :ref:`vmod_acltools(3)` man page for more details. If you are
reading this file on line, it should also be available as
`vmod_acltools.man.rst`_.
INSTALLATION
============
.. _INSTALL.rst: https://code.uplex.de/uplex-varnish/libvmod-acltools/blob/master/INSTALL.rst
See `INSTALL.rst`_.
SUPPORT
=======
.. _gitlab.com issues: https://gitlab.com/uplex/varnish/libvmod-acltools/-/issues
To report bugs, use `gitlab.com issues`_.
For enquiries about professional service and support, please contact
info@uplex.de\ .
CONTRIBUTING
============
.. _merge requests on gitlab.com: https://gitlab.com/uplex/varnish/libvmod-acltools/-/merge_requests
To contribute to the project, please use `merge requests on gitlab.com`_.
To support the project's development and maintenance, there are
several options:
.. _paypal: https://www.paypal.com/donate/?hosted_button_id=BTA6YE2H5VSXA
.. _github sponsor: https://github.com/sponsors/nigoroll
* Donate money through `paypal`_. If you wish to receive a commercial
invoice, please add your details (address, email, any requirements
on the invoice text) to the message sent with your donation.
* Become a `github sponsor`_.
* Contact info@uplex.de to receive a commercial invoice for SWIFT payment.
configure.ac
View file @
f54521b3
AC_PREREQ([2.68])
AC_INIT([libvmod-acltools], [0.
1
])
AC_INIT([libvmod-acltools], [0.
2
])
AC_CONFIG_MACRO_DIR([m4])
AC_CONFIG_AUX_DIR([build-aux])
AC_CONFIG_HEADERS([config.h])
...
...
@@ -13,7 +13,7 @@ AC_ARG_WITH([rst2man],
[RST2MAN="$withval"],
[AC_CHECK_PROGS(RST2MAN, [rst2man rst2man.py], [])])
VARNISH_PREREQ([
6.0
.0])
VARNISH_PREREQ([
7.2
.0])
VARNISH_VMODS([acltools])
AM_PROG_AR
...
...
src/vmod_acltools.c
View file @
f54521b3
#include "config.h"
#include <limits.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <cache/cache.h>
#include <vsa.h>
#include <vtcp.h>
#include "vcc_acltools_if.h"
VCL_STRING
vmod_hello
(
VRT_CTX
)
/* XXX vcc_interface.h */
typedef
int
acl_match_f
(
VRT_CTX
,
const
VCL_IP
);
struct
vrt_acl
{
unsigned
magic
;
#define VRT_ACL_MAGIC 0x78329d96
acl_match_f
*
match
;
const
char
*
name
;
};
/* XXX vss.h */
const
struct
suckaddr
*
VSS_ResolveFirst
(
void
*
dst
,
const
char
*
addr
,
const
char
*
port
,
int
family
,
int
socktype
,
int
flags
);
int
acl_fail_f
(
VRT_CTX
,
const
VCL_IP
ip
)
{
(
void
)
ctx
;
(
void
)
ip
;
return
(
0
);
}
struct
vrt_acl
acl_fail
=
{
.
magic
=
VRT_ACL_MAGIC
,
.
match
=
acl_fail_f
,
.
name
=
"acltools.fail"
};
struct
acl_dyn_single
{
unsigned
magic
;
#define ACL_DYN_SINGLE_MAGIC 0xb30f5ac9
unsigned
mask
;
struct
vrt_acl
acl
;
char
vsa
[];
};
int
dyn_single_match_f
(
VRT_CTX
,
const
VCL_IP
ip
)
{
struct
vmod_priv
*
priv_task
;
const
struct
acl_dyn_single
*
ads
;
unsigned
const
char
*
aip
,
*
aacl
;
unsigned
b
,
m
;
int
fip
,
facl
;
priv_task
=
VRT_priv_task_get
(
ctx
,
dyn_single_match_f
);
if
(
priv_task
==
NULL
)
{
VRT_fail
(
ctx
,
"No priv_task for dynamic ACL match"
);
return
(
0
);
}
CAST_OBJ_NOTNULL
(
ads
,
priv_task
->
priv
,
ACL_DYN_SINGLE_MAGIC
);
fip
=
VSA_GetPtr
(
ip
,
&
aip
);
facl
=
VSA_GetPtr
((
struct
suckaddr
*
)
ads
->
vsa
,
&
aacl
);
if
(
fip
!=
facl
)
{
VSLb
(
ctx
->
vsl
,
SLT_Error
,
"acltools.dyn_single: address family mismatch: "
"ip: %s, acl: %s"
,
fip
==
PF_INET
?
"IPv4"
:
"IPv6"
,
facl
==
PF_INET
?
"IPv4"
:
"IPv6"
);
return
(
0
);
}
b
=
ads
->
mask
/
8
;
m
=
ads
->
mask
%
8
;
if
(
memcmp
(
aip
,
aacl
,
b
))
goto
nomatch
;
if
(
m
==
0
)
goto
match
;
m
=
0xff00
>>
m
;
m
&=
0xff
;
if
((
aip
[
b
]
&
m
)
==
(
aacl
[
b
]
&
m
))
goto
match
;
nomatch:
VSLbs
(
ctx
->
vsl
,
SLT_VCL_acl
,
TOSTRANDS
(
2
,
"NO_MATCH"
,
ads
->
acl
.
name
));
return
(
0
);
match:
VSLbs
(
ctx
->
vsl
,
SLT_VCL_acl
,
TOSTRANDS
(
2
,
"MATCH"
,
ads
->
acl
.
name
));
return
(
1
);
}
#define skipws(s) while( \
*(s) == ' ' || \
*(s) == '\t'|| \
*(s) == '\r'|| \
*(s) == '\n') \
(s)++
VCL_ACL
vmod_dyn_single
(
VRT_CTX
,
struct
VARGS
(
dyn_single
)
*
args
)
{
struct
acl_dyn_single
*
ads
;
const
struct
suckaddr
*
vsa
;
char
*
p
,
*
pp
,
ipmask
[
VTCP_ADDRBUFSIZE
+
4
];
const
char
*
a
;
unsigned
const
char
*
addr
;
struct
vmod_priv
*
priv_task
;
unsigned
long
mask
=
ULONG_MAX
;
unsigned
maxmask
;
uintptr_t
sn
;
int
quot
=
0
;
int
fam
;
VCL_ACL
fb
;
if
(
args
->
valid_fallback
&&
args
->
fallback
!=
NULL
&&
args
->
fallback
->
magic
==
VRT_ACL_MAGIC
)
fb
=
args
->
fallback
;
else
fb
=
&
acl_fail
;
if
(
args
->
ipmask
==
NULL
)
{
VSLb
(
ctx
->
vsl
,
SLT_Error
,
"acltools.dyn_single: empty ipmask"
);
return
(
fb
);
}
a
=
args
->
ipmask
;
skipws
(
a
);
if
(
a
[
0
]
==
'\0'
)
{
VSLb
(
ctx
->
vsl
,
SLT_Error
,
"acltools.dyn_single: empty ipmask"
);
return
(
fb
);
}
skipws
(
a
);
if
(
*
a
==
'"'
)
{
a
++
;
quot
=
1
;
}
if
(
strlen
(
a
)
>
sizeof
ipmask
-
1
)
{
VSLb
(
ctx
->
vsl
,
SLT_Error
,
"acltools.dyn_single: too long: %s"
,
args
->
ipmask
);
return
(
fb
);
}
(
void
)
strcpy
(
ipmask
,
a
);
a
=
NULL
;
p
=
ipmask
;
if
(
quot
)
{
p
=
strchr
(
p
,
'"'
);
if
(
p
==
NULL
)
{
VSLb
(
ctx
->
vsl
,
SLT_Error
,
"acltools.dyn_single: unmatched quote for %s"
,
args
->
ipmask
);
return
(
fb
);
}
*
p
=
'\0'
;
p
++
;
skipws
(
p
);
}
p
=
strchr
(
p
,
'/'
);
if
(
p
!=
NULL
)
{
*
p
=
'\0'
;
p
++
;
// ok to point to \0 now
skipws
(
p
);
pp
=
NULL
;
mask
=
strtoul
(
p
,
&
pp
,
0
);
if
(
pp
!=
NULL
)
{
skipws
(
pp
);
if
(
*
pp
!=
'\0'
)
{
VSLb
(
ctx
->
vsl
,
SLT_Error
,
"acltools.dyn_single: "
"garbage after mask in %s"
,
args
->
ipmask
);
return
(
fb
);
}
}
}
sn
=
WS_Snapshot
(
ctx
->
ws
);
ads
=
WS_Alloc
(
ctx
->
ws
,
sizeof
*
ads
+
vsa_suckaddr_len
);
if
(
ads
==
NULL
)
{
VRT_fail
(
ctx
,
"acltools.dyn_single: insufficient workspace"
);
return
(
fb
);
}
INIT_OBJ
(
ads
,
ACL_DYN_SINGLE_MAGIC
);
vsa
=
VSS_ResolveFirst
(
ads
->
vsa
,
ipmask
,
"0"
,
AF_UNSPEC
,
SOCK_STREAM
,
args
->
resolve
?
0
:
AI_NUMERICHOST
|
AI_NUMERICSERV
);
if
(
vsa
==
NULL
)
{
VSLb
(
ctx
->
vsl
,
SLT_Error
,
"acltools.dyn_single: conversion failed for %s"
,
ipmask
);
WS_Reset
(
ctx
->
ws
,
sn
);
return
(
fb
);
}
assert
((
void
*
)
vsa
==
(
void
*
)
ads
->
vsa
);
fam
=
VSA_GetPtr
(
vsa
,
&
addr
);
switch
(
fam
)
{
case
PF_INET
:
maxmask
=
32
;
break
;
case
PF_INET6
:
maxmask
=
128
;
break
;
default:
WRONG
(
"address family"
);
}
if
(
mask
==
ULONG_MAX
)
mask
=
maxmask
;
else
if
(
mask
>
maxmask
)
{
VSLb
(
ctx
->
vsl
,
SLT_Error
,
"acltools.dyn_single: mask %lu too long for %s"
,
mask
,
args
->
ipmask
);
WS_Reset
(
ctx
->
ws
,
sn
);
return
(
fb
);
}
ads
->
mask
=
mask
;
ads
->
acl
.
magic
=
VRT_ACL_MAGIC
;
ads
->
acl
.
match
=
dyn_single_match_f
;
ads
->
acl
.
name
=
args
->
ipmask
;
priv_task
=
VRT_priv_task
(
ctx
,
dyn_single_match_f
);
if
(
priv_task
==
NULL
)
{
VRT_fail
(
ctx
,
"acltools.dyn_single: no task available"
);
return
(
fb
);
}
priv_task
->
priv
=
ads
;
CHECK_OBJ_NOTNULL
(
ctx
,
VRT_CTX_MAGIC
);
return
(
"vmod-acltools"
);
return
(
&
ads
->
acl
);
}
src/vmod_acltools.vcc
View file @
f54521b3
$Module acltools 3 "Varnish
acltools
Module"
$Module acltools 3 "Varnish
Access Control List (ACL)
Module"
DESCRIPTION
===========
This
VCC file was generated by VCDK, it is used to for both the VMOD
interface and its manual using reStructuredText
.
This
vmod contains additional functions for Access Control Lists
(ACLs)
.
XXX: document vmod-acltools
$Function ACL dyn_single(STRING ipmask, [ACL fallback],
BOOL resolve = 0)
Returns a dynamic ACL **for immediate use** (see **NOTE** below) with
a single entry constructed from the *ipmask* string, which must be of
format::
<ip>[/<mask>]
or, for compatibility with built-in static ACLs::
"<ip>"[/<mask>]
.. _`std.ip()`: https://varnish-cache.org/docs/trunk/reference/vmod_std.html#std-ip
*<ip>* undergoes the same parsing as the *s* argument of
`std.ip()`_. If *<ip>* is an IPv4 address, *<mask>*, if present, must
be an integer between 0 and 32. If *<ip>* is an IPv6 address,
*<mask>*, if present, must be an integer between 0 and 128. If
*<mask>* is not present, it defaults to the maximum mask (32 or 128,
respectively).
If *<ip>* is not a valid address or *<mask>* is invalid for the
address, the *fallback* argument is returned, if present. If
*fallback* is not given with invalid input, the ACL match will always
fail.
If the *resolve* argument is ``true``, *<ip>* will be resolved as a
DNS name.
Example
::
import acltools;
sub vcl_deliver {
set resp.http.Hello = acltools.hello();
sub vcl_recv {
if (client.ip ~ acltools.dyn_single(req.http.acl) {
return (synth(200, "matched"));
} else {
return (synth(403, "denied"));
}
}
XXX: define vmod-acltools interface
$Function STRING hello()
Description
Hello world for vmod-acltools
**NOTE** Due to current limitations in the Varnish-Cache APIs, there
only ever exists one ACL created by this function, and any match is
always on the ACL last created. In particular, ACLs created by this
function can not be saved in variables for later use, they always need
to be matched immediately.
SEE ALSO
========
...
...
src/vtc/vmod_acltools.vtc
View file @
f54521b3
varnishtest "test vmod-acltools"
server s1 {
rxreq
txresp
varnish v1 -proto PROXY -vcl {
import acltools;
backend none none;
acl fallback {
"1.2.3.4"/32;
"102:304:506::d0e:f10"/128;
}
sub vcl_recv {
if (req.url == "/") {
if (client.ip ~ acltools.dyn_single(req.http.acl)) {
return (synth(200, "matched"));
} else {
return (synth(403, "denied"));
}
} else if (req.url == "/fallback") {
if (client.ip ~ acltools.dyn_single(req.http.acl,
fallback=fallback)) {
return (synth(200, "matched"));
} else {
return (synth(403, "denied"));
}
}
return (synth(404));
}
} -start
logexpect l1 -v v1 -g session -m -q {Proxy ~ "5.6.7.8"} {
expect * * Error "acltools.dyn_single: empty ipmask"
expect * * Error "acltools.dyn_single: empty ipmask"
expect * * Error "acltools.dyn_single: too long: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef1234"
expect * * Error {acltools.dyn_single: unmatched quote for "1.2.3.4/32}
expect * * Error {acltools.dyn_single: garbage after mask in 1.2.3.4/32"}
expect * * Error "acltools.dyn_single: conversion failed for 1.2.3.x"
expect * * Error "acltools.dyn_single: mask 33 too long for 1.2.3.4/33"
expect * * Error "acltools.dyn_single: address family mismatch: ip: IPv4, acl: IPv6"
} -start
client c1 -proxy1 "1.2.3.4:1111 5.6.7.8:5678" {
txreq
rxresp
expect resp.status == 403
txreq -hdr "acl: 1.2.3.0/24"
rxresp
expect resp.status == 200
txreq -hdr "acl: 1.2.3.0/25"
rxresp
expect resp.status == 200
txreq -hdr "acl: 1.2.3.4/30"
rxresp
expect resp.status == 200
txreq -hdr "acl: 1.2.3.6/31"
rxresp
expect resp.status == 403
txreq -hdr "acl: 1.2.3.4/32"
rxresp
expect resp.status == 200
txreq -hdr {acl: "1.2.3.4"/32}
rxresp
expect resp.status == 200
txreq -hdr "acl: 1.2.3.4 "
rxresp
expect resp.status == 200
## SLT_error cases
# empty
txreq -hdr "acl: "
rxresp
expect resp.status == 403
# too long
txreq -hdr "acl: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef1234"
rxresp
expect resp.status == 403
# unmatched quote
txreq -hdr {acl: "1.2.3.4/32}
rxresp
expect resp.status == 403
# garbage
txreq -hdr {acl: 1.2.3.4/32"}
rxresp
expect resp.status == 403
# failed
txreq -hdr {acl: 1.2.3.x/32}
rxresp
expect resp.status == 403
# mask too long
txreq -hdr {acl: 1.2.3.4/33}
rxresp
expect resp.status == 403
# family mismatch
txreq -hdr {acl: [::1]/128}
rxresp
expect resp.status == 403
} -start
logexpect l2 -v v1 -g session -m -q {Proxy ~ "6.7.8.9"} {
expect * * Error "acltools.dyn_single: empty ipmask"
expect * * Error "acltools.dyn_single: empty ipmask"
expect * * Error "acltools.dyn_single: too long: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef1234"
expect * * Error {acltools.dyn_single: unmatched quote for "1.2.3.4/32}
expect * * Error {acltools.dyn_single: garbage after mask in 1.2.3.4/32"}
expect * * Error "acltools.dyn_single: conversion failed for 1.2.3.x"
expect * * Error "acltools.dyn_single: mask 33 too long for 1.2.3.4/33"
} -start
varnish v1 -vcl+backend {
import acltools;
# for fallback, the error cases are 200
client c2 -proxy1 "1.2.3.4:1111 6.7.8.9:5678" {
# no acl
txreq -url "/fallback"
rxresp
expect resp.status == 200
txreq -url "/fallback" -hdr "acl: 1.2.3.0/24"
rxresp
expect resp.status == 200
txreq -url "/fallback" -hdr "acl: 1.2.3.0/25"
rxresp
expect resp.status == 200
txreq -url "/fallback" -hdr "acl: 1.2.3.4/30"
rxresp
expect resp.status == 200
txreq -url "/fallback" -hdr "acl: 1.2.3.6/31"
rxresp
expect resp.status == 403
txreq -url "/fallback" -hdr "acl: 1.2.3.4/32"
rxresp
expect resp.status == 200
txreq -url "/fallback" -hdr {acl: "1.2.3.4"/32}
rxresp
expect resp.status == 200
sub vcl_deliver {
set resp.http.Hello = acltools.hello();
}
txreq -url "/fallback" -hdr "acl: 1.2.3.4 "
rxresp
expect resp.status == 200
## SLT_error cases
# empty
txreq -url "/fallback" -hdr "acl: "
rxresp
expect resp.status == 200
# too long
txreq -url "/fallback" -hdr "acl: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef1234"
rxresp
expect resp.status == 200
# unmatched quote
txreq -url "/fallback" -hdr {acl: "1.2.3.4/32}
rxresp
expect resp.status == 200
# garbage
txreq -url "/fallback" -hdr {acl: 1.2.3.4/32"}
rxresp
expect resp.status == 200
# failed
txreq -url "/fallback" -hdr {acl: 1.2.3.x/32}
rxresp
expect resp.status == 200
# mask too long
txreq -url "/fallback" -hdr {acl: 1.2.3.4/33}
rxresp
expect resp.status == 200
} -start
client c
1
{
client c
3 -proxy2 "[102:304:506::d0e:f10]:2314 [8182:8384:8586::8d8e:8f80]:2828"
{
txreq
rxresp
expect resp.status == 403
txreq -hdr "acl: 102:304:506::d0e:f/112"
rxresp
expect resp.status == 200
txreq -hdr "acl: 102:304:506::d0e:f/113"
rxresp
expect resp.status == 200
txreq -hdr "acl: 102:304:506::d0e:f10/126"
rxresp
expect resp.status == 200
txreq -hdr "acl: 102:304:506::d0e:f16/127"
rxresp
expect resp.status == 403
txreq -hdr "acl: 102:304:506::d0e:f10/128"
rxresp
expect resp.status == 200
txreq -hdr {acl: "102:304:506::d0e:f10"/128}
rxresp
expect resp.status == 200
txreq -hdr "acl: 102:304:506::d0e:f10 "
rxresp
expect resp.status == 200
expect resp.http.Hello == "vmod-acltools"
} -run
## SLT_error cases
# empty
txreq -hdr "acl: "
rxresp
expect resp.status == 403
# too long
txreq -hdr "acl: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef1234"
rxresp
expect resp.status == 403
# unmatched quote
txreq -hdr {acl: "102:304:506::d0e:4/32}
rxresp
expect resp.status == 403
# garbage
txreq -hdr {acl: 102:304:506::d0e:4/32"}
rxresp
expect resp.status == 403
# failed
txreq -hdr {acl: 102:304:506::d0e:x/32}
rxresp
expect resp.status == 403
# mask too long
txreq -hdr {acl: 102:304:506::d0e:4/129}
rxresp
expect resp.status == 403
# family mismatch
txreq -hdr {acl: 127.0.0.1/32}
rxresp
expect resp.status == 403
} -start
client c1 -wait
client c2 -wait
client c3 -wait
logexpect l1 -wait
logexpect l2 -wait
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment