diff --git a/docs/README.md b/docs/README.md
index 6736d33db15c3bfe8cde5af70eef19ede43e0d3d..dd7407733647209c197fc31f324e41093487b6bc 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -10,8 +10,6 @@ by a vast number of open source and commercial products - it even runs on the
 International Space Station! Mongoose makes embedded network programming fast,
 robust, and easy.
 
-## Concept
-
 Mongoose has three basic data structures:
 
 - `struct mg_mgr` - an event manager that holds all active connections
@@ -1213,7 +1211,7 @@ The glob pattern matching rules are as follows:
 - any other character matches itself
 
 
-### mg\_next\_comma\_entry()
+### mg\_comma()
 
 ```c
 bool mg_comma(struct mg_str *s, struct mg_str *k, struct mg_str *v);
@@ -1374,3 +1372,25 @@ uint32_t mg_crc32(uint32_t crc, const uint8_t *buf, size_t len);
 
 Calculate CRC32 checksum for a given buffer. An initial `crc` value should
 be `0`.
+
+### mg\_check\_ip\_acl()
+
+```c
+int mg_check_ip_acl(struct mg_str acl, uint32_t remote_ip);
+```
+
+Check IPv4 address `remote_ip` against the IP ACL `acl`. Parameters:
+
+- `acl` - an ACL string, e.g. `-0.0.0.0/0,+1.2.3.4`
+- `remote_ip` - IPv4 address in network byte order
+
+Return value: 1 if `remote_ip` is allowed, 0 if not, and <0 if `acl` is
+invalid.
+
+Usage example:
+
+```
+  if (mg_check_ip_acl(mg_str("-0.0.0.0/0,+1.2.3.4"), c->peer.ip) != 1) {
+    LOG(LL_INFO, ("NOT ALLOWED!"));
+  }
+```
diff --git a/mongoose.c b/mongoose.c
index c8f4c1606d114818047b972ae36c67d70db5f1a8..508f737c53214bb084a727dfaeec46c3d8763a33 100644
--- a/mongoose.c
+++ b/mongoose.c
@@ -4249,7 +4249,7 @@ uint32_t mg_ntohl(uint32_t net) {
 uint16_t mg_ntohs(uint16_t net) {
   uint8_t data[2] = {0, 0};
   memcpy(&data, &net, sizeof(data));
-  return (uint16_t)((uint16_t) data[1] | (((uint16_t) data[0]) << 8));
+  return (uint16_t) ((uint16_t) data[1] | (((uint16_t) data[0]) << 8));
 }
 
 char *mg_hexdump(const void *buf, size_t len) {
@@ -4292,10 +4292,9 @@ char *mg_hex(const void *buf, size_t len, char *to) {
 }
 
 static unsigned char mg_unhex_nimble(unsigned char c) {
-  return (c >= '0' && c <= '9')
-             ? (unsigned char) (c - '0')
-             : (c >= 'A' && c <= 'F') ? (unsigned char) (c - '7')
-                                      : (unsigned char) (c - 'W');
+  return (c >= '0' && c <= '9')   ? (unsigned char) (c - '0')
+         : (c >= 'A' && c <= 'F') ? (unsigned char) (c - '7')
+                                  : (unsigned char) (c - 'W');
 }
 
 unsigned long mg_unhexn(const char *s, size_t len) {
@@ -4388,6 +4387,36 @@ uint32_t mg_crc32(uint32_t crc, const char *buf, size_t len) {
   return ~crc;
 }
 
+static int isbyte(int n) {
+  return n >= 0 && n <= 255;
+}
+
+static int parse_net(const char *spec, uint32_t *net, uint32_t *mask) {
+  int n, a, b, c, d, slash = 32, len = 0;
+  if ((sscanf(spec, "%d.%d.%d.%d/%d%n", &a, &b, &c, &d, &slash, &n) == 5 ||
+       sscanf(spec, "%d.%d.%d.%d%n", &a, &b, &c, &d, &n) == 4) &&
+      isbyte(a) && isbyte(b) && isbyte(c) && isbyte(d) && slash >= 0 &&
+      slash < 33) {
+    len = n;
+    *net = ((uint32_t) a << 24) | ((uint32_t) b << 16) | ((uint32_t) c << 8) |
+           (uint32_t) d;
+    *mask = slash ? (uint32_t) (0xffffffffU << (32 - slash)) : (uint32_t) 0;
+  }
+  return len;
+}
+
+int mg_check_ip_acl(struct mg_str acl, uint32_t remote_ip) {
+  struct mg_str k, v;
+  int allowed = acl.len == 0 ? '+' : '-';  // If any ACL is set, deny by default
+  while (mg_comma(&acl, &k, &v)) {
+    uint32_t net, mask;
+    if (k.ptr[0] != '+' && k.ptr[0] != '-') return -1;
+    if (parse_net(&k.ptr[1], &net, &mask) == 0) return -2;
+    if ((remote_ip & mask) == net) allowed = k.ptr[0];
+  }
+  return allowed == '+';
+}
+
 double mg_time(void) {
 #if MG_ARCH == MG_ARCH_WIN32
   SYSTEMTIME sysnow;
diff --git a/mongoose.h b/mongoose.h
index e46b563619903cc61777c293f86dc980aa47be95..e4c0139329aa4ed6d5abbc7428c616400654cb4c 100644
--- a/mongoose.h
+++ b/mongoose.h
@@ -512,14 +512,15 @@ void mg_log_set_callback(void (*fn)(const void *, size_t, void *), void *param);
 struct mg_timer {
   unsigned long period_ms;  // Timer period in milliseconds
   unsigned flags;           // Possible flags values below
-#define MG_TIMER_REPEAT 1   // Call function periodically, otherwise run once
-#define MG_TIMER_RUN_NOW 2  // Call immediately when timer is set
   void (*fn)(void *);       // Function to call
   void *arg;                // Function argument
   unsigned long expire;     // Expiration timestamp in milliseconds
   struct mg_timer *next;    // Linkage in g_timers list
 };
 
+#define MG_TIMER_REPEAT 1   // Call function periodically, otherwise run once
+#define MG_TIMER_RUN_NOW 2  // Call immediately when timer is set
+
 extern struct mg_timer *g_timers;  // Global list of timers
 
 void mg_timer_init(struct mg_timer *, unsigned long ms, unsigned,
@@ -547,6 +548,7 @@ unsigned long mg_unhexn(const char *s, size_t len);
 int mg_asprintf(char **buf, size_t size, const char *fmt, ...);
 int mg_vasprintf(char **buf, size_t size, const char *fmt, va_list ap);
 int64_t mg_to64(struct mg_str str);
+int mg_check_ip_acl(struct mg_str acl, uint32_t remote_ip);
 double mg_time(void);
 unsigned long mg_millis(void);
 void mg_usleep(unsigned long usecs);
diff --git a/src/util.c b/src/util.c
index 4747f1529ae59021bc8e660ae5769258bbf63560..e098212ee39b56ab25e3462b1934d9a5d4759524 100644
--- a/src/util.c
+++ b/src/util.c
@@ -127,7 +127,7 @@ uint32_t mg_ntohl(uint32_t net) {
 uint16_t mg_ntohs(uint16_t net) {
   uint8_t data[2] = {0, 0};
   memcpy(&data, &net, sizeof(data));
-  return (uint16_t)((uint16_t) data[1] | (((uint16_t) data[0]) << 8));
+  return (uint16_t) ((uint16_t) data[1] | (((uint16_t) data[0]) << 8));
 }
 
 char *mg_hexdump(const void *buf, size_t len) {
@@ -170,10 +170,9 @@ char *mg_hex(const void *buf, size_t len, char *to) {
 }
 
 static unsigned char mg_unhex_nimble(unsigned char c) {
-  return (c >= '0' && c <= '9')
-             ? (unsigned char) (c - '0')
-             : (c >= 'A' && c <= 'F') ? (unsigned char) (c - '7')
-                                      : (unsigned char) (c - 'W');
+  return (c >= '0' && c <= '9')   ? (unsigned char) (c - '0')
+         : (c >= 'A' && c <= 'F') ? (unsigned char) (c - '7')
+                                  : (unsigned char) (c - 'W');
 }
 
 unsigned long mg_unhexn(const char *s, size_t len) {
@@ -266,6 +265,36 @@ uint32_t mg_crc32(uint32_t crc, const char *buf, size_t len) {
   return ~crc;
 }
 
+static int isbyte(int n) {
+  return n >= 0 && n <= 255;
+}
+
+static int parse_net(const char *spec, uint32_t *net, uint32_t *mask) {
+  int n, a, b, c, d, slash = 32, len = 0;
+  if ((sscanf(spec, "%d.%d.%d.%d/%d%n", &a, &b, &c, &d, &slash, &n) == 5 ||
+       sscanf(spec, "%d.%d.%d.%d%n", &a, &b, &c, &d, &n) == 4) &&
+      isbyte(a) && isbyte(b) && isbyte(c) && isbyte(d) && slash >= 0 &&
+      slash < 33) {
+    len = n;
+    *net = ((uint32_t) a << 24) | ((uint32_t) b << 16) | ((uint32_t) c << 8) |
+           (uint32_t) d;
+    *mask = slash ? (uint32_t) (0xffffffffU << (32 - slash)) : (uint32_t) 0;
+  }
+  return len;
+}
+
+int mg_check_ip_acl(struct mg_str acl, uint32_t remote_ip) {
+  struct mg_str k, v;
+  int allowed = acl.len == 0 ? '+' : '-';  // If any ACL is set, deny by default
+  while (mg_comma(&acl, &k, &v)) {
+    uint32_t net, mask;
+    if (k.ptr[0] != '+' && k.ptr[0] != '-') return -1;
+    if (parse_net(&k.ptr[1], &net, &mask) == 0) return -2;
+    if ((remote_ip & mask) == net) allowed = k.ptr[0];
+  }
+  return allowed == '+';
+}
+
 double mg_time(void) {
 #if MG_ARCH == MG_ARCH_WIN32
   SYSTEMTIME sysnow;
diff --git a/src/util.h b/src/util.h
index 4a39198615a9f27a9a652ded2c9a208bb5e7ffc2..0dd10d3bd74dc9a74a4f829cde719899e285dfaa 100644
--- a/src/util.h
+++ b/src/util.h
@@ -19,6 +19,7 @@ unsigned long mg_unhexn(const char *s, size_t len);
 int mg_asprintf(char **buf, size_t size, const char *fmt, ...);
 int mg_vasprintf(char **buf, size_t size, const char *fmt, va_list ap);
 int64_t mg_to64(struct mg_str str);
+int mg_check_ip_acl(struct mg_str acl, uint32_t remote_ip);
 double mg_time(void);
 unsigned long mg_millis(void);
 void mg_usleep(unsigned long usecs);
diff --git a/test/unit_test.c b/test/unit_test.c
index e481df121f4ad50f3a3d71ea416017308d58931b..fb75ea26394844a7765dbebbda7353ae286720d2 100644
--- a/test/unit_test.c
+++ b/test/unit_test.c
@@ -1461,8 +1461,22 @@ static void test_udp(void) {
   ASSERT(mgr.conns == NULL);
 }
 
+static void test_check_ip_acl(void) {
+  uint32_t ip = 0x01020304;
+  ASSERT(mg_check_ip_acl(mg_str(NULL), ip) == 1);
+  ASSERT(mg_check_ip_acl(mg_str(""), ip) == 1);
+  ASSERT(mg_check_ip_acl(mg_str("invalid"), ip) == -1);
+  ASSERT(mg_check_ip_acl(mg_str("+hi"), ip) == -2);
+  ASSERT(mg_check_ip_acl(mg_str("+//"), ip) == -2);
+  ASSERT(mg_check_ip_acl(mg_str("-0.0.0.0/0"), ip) == 0);
+  ASSERT(mg_check_ip_acl(mg_str("-0.0.0.0/0,+1.0.0.0/8"), ip) == 1);
+  ASSERT(mg_check_ip_acl(mg_str("-0.0.0.0/0,+1.2.3.4"), ip) == 1);
+  ASSERT(mg_check_ip_acl(mg_str("-0.0.0.0/0,+1.0.0.0/16"), ip) == 0);
+}
+
 int main(void) {
   mg_log_set("3");
+  test_check_ip_acl();
   test_udp();
   test_pipe();
   test_packed();