diff --git a/client/client.c b/client/client.c
index 397db73d4..db9af133d 100644
--- a/client/client.c
+++ b/client/client.c
@@ -38,8 +38,8 @@
 
 #define SERVER_READ_BUF_LEN 4096
 
-static char *opt_list = "s:vrl";
-static int verbose, restricted, once;
+static char *opt_list = "s:vrlq";
+static int verbose, restricted, once, quiet;
 static char *init_cmd;
 
 static char *server_path = PATH_CONTROL_SOCKET;
@@ -84,6 +84,9 @@ parse_args(int argc, char **argv)
       case 'r':
 	restricted = 1;
 	break;
+      case 'q':
+	quiet = 1;
+	break;
       case 'l':
 	if (!server_changed)
 	  server_path = xbasename(server_path);
@@ -281,7 +284,7 @@ server_got_reply(char *x)
            sscanf(x, "%d", &code) == 1 && code >= 0 && code < 10000 &&
            (x[4] == ' ' || x[4] == '-'))
     {
-      if (code)
+      if (code && !(quiet && code < 1000))
         PRINTF(len, "%s\n", verbose ? x : x+5);
 
       last_code = code;
diff --git a/lib/printf.c b/lib/printf.c
index 0d2f95e80..6167ca818 100644
--- a/lib/printf.c
+++ b/lib/printf.c
@@ -10,6 +10,7 @@
 #include "nest/bird.h"
 #include "lib/macro.h"
 #include "lib/string.h"
+#include "lib/timer.h"
 
 #include <errno.h>
 
@@ -34,6 +35,7 @@ static int skip_atoi(const char **s)
 #define LEFT	16		/* left justified */
 #define SPECIAL	32		/* 0x */
 #define LARGE	64		/* use 'ABCDEF' instead of 'abcdef' */
+#define JSON	128		/* use JSON escaping, null type */
 
 static char * number(char * str, u64 num, uint base, int size, int precision,
 	int type, int remains)
@@ -124,6 +126,68 @@ static char * number(char * str, u64 num, uint base, int size, int precision,
 	return str;
 }
 
+char *json_string(char *str, char *s, int remains)
+{
+	if (!s) {
+		if (remains < 4)
+			return NULL;
+		*str++ = 'n';
+		*str++ = 'u';
+		*str++ = 'l';
+		*str++ = 'l';
+		return str;
+	}
+
+	if (remains < 2)
+		return NULL;
+	*str++ = '"';
+	--remains;
+
+	char spec_char;
+	char *start;
+
+	for (start=str; *s; ++s, remains-=(str-start), start=str) {
+		if (!remains)
+			return NULL;
+		switch (*s) {
+		case '"':
+		case '\\':
+			spec_char = *s;
+		special:
+			if (remains < 2)
+				return NULL;
+			*str++ = '\\';
+			*str++ = spec_char;
+			continue;
+		case '\n':
+			spec_char = 'n';
+			goto special;
+		case '\r':
+			spec_char = 'r';
+			goto special;
+		case '\t':
+			spec_char = 't';
+			goto special;
+		}
+		if (*s >= ' ') {
+			*str++ = *s;
+			continue;
+		}
+		if (remains < 6)
+			return NULL;
+		*str++ = '\\';
+		*str++ = 'u';
+		str = number(str, (unsigned char)(*s), 16, 4, 0, ZEROPAD, remains-2);
+		if (!str)
+			return NULL;
+		continue;
+	}
+	if (!remains)
+		return NULL;
+	*str++ = '"';
+	return str;
+}
+
 /**
  * bvsnprintf - BIRD's vsnprintf()
  * @buf: destination buffer
@@ -186,6 +250,7 @@ int bvsnprintf(char *buf, int size, const char *fmt, va_list args)
 				case ' ': flags |= SPACE; goto repeat;
 				case '#': flags |= SPECIAL; goto repeat;
 				case '0': flags |= ZEROPAD; goto repeat;
+				case 'j': flags |= JSON; goto repeat;
 			}
 
 		/* get field width */
@@ -227,6 +292,11 @@ int bvsnprintf(char *buf, int size, const char *fmt, va_list args)
 		/* default base */
 		base = 10;
 
+		if (flags & JSON) {
+			flags &= SIGN|JSON;
+			field_width = -1;
+		}
+
 		if (field_width > size)
 			return -1;
 		switch (*fmt) {
@@ -265,10 +335,12 @@ int bvsnprintf(char *buf, int size, const char *fmt, va_list args)
 			}
 		case 's':
 			s = va_arg(args, char *);
-			if (!s)
+			if (!s && !(flags & JSON))
 				s = "<NULL>";
 
 		str:
+			if (flags & JSON)
+				goto json;
 			len = strlen(s);
 			if (precision >= 0 && len > precision)
 				len = precision;
@@ -284,6 +356,12 @@ int bvsnprintf(char *buf, int size, const char *fmt, va_list args)
 				*str++ = ' ';
 			continue;
 
+		json:
+			str = json_string(str, s, size);
+			if (!str)
+				return -1;
+			continue;
+
 		case 'V': {
 			const char *vfmt = va_arg(args, const char *);
 			va_list *vargs = va_arg(args, va_list *);
@@ -316,6 +394,14 @@ int bvsnprintf(char *buf, int size, const char *fmt, va_list args)
 			}
 			continue;
 
+		case 'b':
+			num = va_arg(args, uint);
+			if (num)
+				s = "true";
+			else
+				s = "false";
+			goto str;
+
 		/* IP address */
 		case 'I':
 			if (fmt[1] == '4') {
@@ -352,6 +438,11 @@ int bvsnprintf(char *buf, int size, const char *fmt, va_list args)
 		/* Interface scope after link-local IP address */
 		case 'J':
 			iface = va_arg(args, struct iface *);
+			if (flags & JSON) {
+				s = iface ? iface->name : NULL;
+				goto json;
+			}
+
 			if (!iface)
 				continue;
 			if (!size)
@@ -388,6 +479,8 @@ int bvsnprintf(char *buf, int size, const char *fmt, va_list args)
 
 		case 't':
 			t = va_arg(args, btime);
+			if (flags & JSON)
+				t = tm_real_time(t);
 			t1 = t TO_S;
 			t2 = t - t1 S;
 
diff --git a/lib/timer.c b/lib/timer.c
index 85519f441..4fa5201ad 100644
--- a/lib/timer.c
+++ b/lib/timer.c
@@ -244,6 +244,14 @@ tm_parse_time(const char *x)
   return ts S + usec;
 }
 
+btime
+tm_real_time(btime t)
+{
+  btime dt = current_time() - t;
+  btime rt = current_real_time() - dt;
+  return rt;
+}
+
 /**
  * tm_format_time - convert date and time to textual representation
  * @x: destination buffer of size %TM_DATETIME_BUFFER_SIZE
diff --git a/lib/timer.h b/lib/timer.h
index f52694c89..fa8a1638b 100644
--- a/lib/timer.h
+++ b/lib/timer.h
@@ -136,6 +136,7 @@ struct timeformat {
 #define TM_DATETIME_BUFFER_SIZE 32	/* Buffer size required by tm_format_time() */
 
 btime tm_parse_time(const char *x);
+btime tm_real_time(btime t);
 void tm_format_time(char *x, struct timeformat *fmt, btime t);
 int tm_format_real_time(char *x, size_t max, const char *fmt, btime t);
 
diff --git a/nest/config.Y b/nest/config.Y
index 681d0edd9..f25463a13 100644
--- a/nest/config.Y
+++ b/nest/config.Y
@@ -166,6 +166,7 @@ CF_KEYWORDS(CHECK, LINK)
 CF_KEYWORDS(CORK, SORTED, TRIE, MIN, MAX, ROA, DIGEST, ROUTE, REFRESH, SETTLE, TIME, GC, THRESHOLD, PERIOD)
 CF_KEYWORDS(MPLS_LABEL, MPLS_POLICY, MPLS_CLASS)
 CF_KEYWORDS(ASPA_PROVIDERS)
+CF_KEYWORDS(JSON)
 
 /* For r_args_channel */
 CF_KEYWORDS(IPV4, IPV4_MC, IPV4_MPLS, IPV6, IPV6_MC, IPV6_MPLS, IPV6_SADR, VPN4, VPN4_MC, VPN4_MPLS, VPN6, VPN6_MC, VPN6_MPLS, ROA4, ROA6, FLOW4, FLOW6, MPLS, PRI, SEC, ASPA)
@@ -979,6 +980,10 @@ CF_CLI(DUMP PROTOCOLS, text,, [[Dump protocol information]])
 CF_CLI(DUMP FILTER ALL, text,, [[Dump all filters in linearized form]])
 { cmd_dump_file(this_cli, $4, "filter bytecode", filters_dump_all); } ;
 
+CF_CLI_HELP(JSON, ..., [[Dump status information as JSON]])
+CF_CLI(JSON PROTOCOLS,,, [[Dump protocol status information as JSON]])
+{ protos_json_all(); } ;
+
 CF_CLI(EVAL, term, <expr>, [[Evaluate an expression]])
 { cmd_eval(f_linearize($2, 1)); } ;
 
diff --git a/nest/proto.c b/nest/proto.c
index be347ea3e..4a04a9235 100644
--- a/nest/proto.c
+++ b/nest/proto.c
@@ -2223,6 +2223,20 @@ protos_dump_all(struct dump_request *dreq)
   }
 }
 
+void
+protos_json_all()
+{
+  cli_msg(-2002, "{\"version\":1,\"protocols\":{");
+  int cnt = 0;
+  WALK_TLIST(proto, p, &global_proto_list) PROTO_LOCKED_FROM_MAIN(p)
+  {
+    proto_cmd_show_json(p, cnt);
+    ++cnt;
+  }
+  cli_msg(-2002, "}}");
+  cli_msg(0, "");
+}
+
 /**
  * proto_build - make a single protocol available
  * @p: the protocol
@@ -2706,6 +2720,61 @@ channel_show_stats(struct channel *c)
 #undef SON
 }
 
+static void
+channel_show_stats_json(struct channel *c)
+{
+  if (c->channel_state == CS_DOWN) {
+    cli_msg(-1006, "\"Stats\":null");
+    return;
+  }
+
+  struct channel_import_stats *ch_is = &c->import_stats;
+  struct channel_export_stats *ch_es = &c->export_stats;
+  struct rt_import_stats *rt_is = c->in_req.hook ? &c->in_req.hook->stats : NULL;
+  struct rt_export_stats *rt_es = &c->out_req.stats;
+
+#define SON(ie, item)	((ie) ? (ie)->item : 0)
+#define SCI(item) SON(ch_is, item)
+#define SCE(item) SON(ch_es, item)
+#define SRI(item) SON(rt_is, item)
+#define SRE(item) SON(rt_es, item)
+
+  u32 rx_routes = c->rx_limit.count;
+  u32 in_routes = c->in_limit.count;
+  u32 out_routes = c->out_limit.count;
+
+  cli_msg(-1006, "\"Stats\":{");
+
+  cli_msg(-1006, "\"Routes\":{\"imported\":%ju,\"exported\":%ju,\"preferred\":%ju",
+	in_routes, out_routes, SRI(pref));
+  if (c->in_keep)
+    cli_msg(-1006, ",\"filtered\":%ju", (rx_routes - in_routes));
+  else
+    cli_msg(-1006, ",\"filtered\":null");
+
+  cli_msg(-1006, "},\"Route change\":{");
+  cli_msg(-1006, "\"Import updates\":{\"received\":%ju,\"rejected\":%ju,\"filtered\":%ju,\"ignored\":%ju,\"RX limit\":%ju,\"limit\":%ju,\"accepted\":%ju}",
+	  SCI(updates_received), SCI(updates_invalid),
+	  SCI(updates_filtered), SRI(updates_ignored),
+	  SCI(updates_limited_rx), SCI(updates_limited_in),
+	  SRI(updates_accepted));
+  cli_msg(-1006, ",\"Import withdraws\":{\"received\":%ju,\"rejected\":%ju,\"ignored\":%ju,\"accepted\":%ju}",
+	  SCI(withdraws_received), SCI(withdraws_invalid),
+	  SRI(withdraws_ignored), SRI(withdraws_accepted));
+  cli_msg(-1006, ",\"Export updates\":{\"received\":%ju,\"rejected\":%ju,\"filtered\":%ju,\"ignored\":%ju,\"limit\":%ju,\"accepted\":%ju}",
+	  SRE(updates_received), SCE(updates_rejected),	SCE(updates_filtered),
+	  SCE(updates_ignored), SCE(updates_limited), SCE(updates_accepted));
+  cli_msg(-1006, ",\"Export withdraws\":{\"received\":%ju,\"ignored\":%ju,\"accepted\":%ju}",
+	  SRE(withdraws_received), SCE(withdraws_ignored), SCE(withdraws_accepted));
+  cli_msg(-1006, "}}");
+
+#undef SRI
+#undef SRE
+#undef SCI
+#undef SCE
+#undef SON
+}
+
 void
 channel_show_limit(struct limit *l, const char *dsc, int active, int action)
 {
@@ -2716,6 +2785,17 @@ channel_show_limit(struct limit *l, const char *dsc, int active, int action)
   cli_msg(-1006, "      Action:       %s", channel_limit_name[action]);
 }
 
+void
+channel_show_limit_json(struct limit *l, const char *dsc, int active, int action)
+{
+  if (!l->action) {
+    cli_msg(-1006, "%js:null", dsc);
+    return;
+  }
+  cli_msg(-1006, "%js:{\"max\":%jd,\"active\":%jb", dsc, l->max, active);
+  cli_msg(-1006, ",\"Action\":%js}", channel_limit_name[action]);
+}
+
 void
 channel_show_info(struct channel *c)
 {
@@ -2741,6 +2821,35 @@ channel_show_info(struct channel *c)
     channel_show_stats(c);
 }
 
+void
+channel_show_info_json(struct channel *c)
+{
+  cli_msg(-1006, "\"State\":%js", c_states[c->channel_state]);
+  cli_msg(-1006, ",\"Import state\":%js", rt_import_state_name(rt_import_get_state(c->in_req.hook)));
+  cli_msg(-1006, ",\"Export state\":%js", rt_export_state_name(rt_export_get_state(&c->out_req)));
+  cli_msg(-1006, ",\"Table\":%js", c->table->name);
+  cli_msg(-1006, ",\"Preference\":%jd", c->preference);
+  cli_msg(-1006, ",\"Input filter\":%js", filter_name(c->in_filter));
+  cli_msg(-1006, ",\"Output filter\":%js", filter_name(c->out_filter));
+
+  if (_graceful_recovery_context.grc_state == GRS_ACTIVE) {
+    cli_msg(-1006, ",\"GR recovery\":{\"pending\":%jb,\"waiting\":%jb}",
+	    OBSREF_GET(c->gr_lock), c->gr_wait);
+  } else {
+    cli_msg(-1006, ",\"GR recovery\":null");
+  }
+
+  cli_msg(-1006, ",");
+  channel_show_limit_json(&c->rx_limit, "Receive limit", c->limit_active & (1 << PLD_RX), c->limit_actions[PLD_RX]);
+  cli_msg(-1006, ",");
+  channel_show_limit_json(&c->in_limit, "Import limit", c->limit_active & (1 << PLD_IN), c->limit_actions[PLD_IN]);
+  cli_msg(-1006, ",");
+  channel_show_limit_json(&c->out_limit, "Export limit", c->limit_active & (1 << PLD_OUT), c->limit_actions[PLD_OUT]);
+
+  cli_msg(-1006, ",");
+  channel_show_stats_json(c);
+}
+
 void
 channel_cmd_debug(struct channel *c, uint mask)
 {
@@ -2809,6 +2918,58 @@ proto_cmd_show(struct proto *p, uintptr_t verbose, int cnt)
   }
 }
 
+void
+proto_cmd_show_json(struct proto *p, int cnt)
+{
+  byte buf[256];
+
+  /* Not first protocol - show comma */
+  if (cnt)
+    cli_msg(-2002, ",");
+
+  buf[0] = 0;
+  if (p->proto->get_status)
+    p->proto->get_status(p, buf);
+
+  cli_msg(-1002, "%js:{\"Proto\":%js,\"Table\":%js,\"State\":%js,\"Since\":%jt,\"Info\":%js",
+	  p->name,
+	  p->proto->name,
+	  p->main_channel ? p->main_channel->table->name : NULL,
+	  proto_state_name(p),
+	  p->last_state_change,
+	  buf);
+
+  cli_msg(-1006, ",\"Description\":%js", p->cf->dsc);
+  cli_msg(-1006, ",\"Message\":%js", p->message);
+  cli_msg(-1006, ",\"Created\":%jt", p->last_reconfiguration);
+  if (p->last_restart > p->last_reconfiguration)
+    cli_msg(-1006, ",\"Last autorestart\":%jt", p->last_restart);
+  else
+    cli_msg(-1006, ",\"Last autorestart\":null");
+  cli_msg(-1006, ",\"Router ID\":%jR", p->cf->router_id);
+  cli_msg(-1006, ",\"VRF\":%js", p->vrf ? p->vrf->name : NULL);
+
+  if (p->proto->show_proto_info_json)
+    p->proto->show_proto_info_json(p);
+  else
+  {
+    struct channel *c;
+    cli_msg(-1006, ",\"Channels\":{");
+    int ch_cnt = 0;
+    WALK_LIST(c, p->channels) {
+      if (ch_cnt)
+        cli_msg(-1006, ",");
+      ++ch_cnt;
+      cli_msg(-1006, "%js:{", c->name);
+      channel_show_info_json(c);
+      cli_msg(-1006, "}");
+    }
+    cli_msg(-1006, "}");
+  }
+
+  cli_msg(-1006, "}");
+}
+
 void
 proto_cmd_disable(struct proto *p, uintptr_t arg, int cnt UNUSED)
 {
diff --git a/nest/protocol.h b/nest/protocol.h
index f5d8a7571..4414dd7e6 100644
--- a/nest/protocol.h
+++ b/nest/protocol.h
@@ -70,6 +70,7 @@ struct protocol {
   void (*get_status)(struct proto *, byte *buf); /* Get instance status (for `show protocols' command) */
 //  int (*get_attr)(const struct eattr *, byte *buf, int buflen);	/* ASCIIfy dynamic attribute (returns GA_*) */
   void (*show_proto_info)(struct proto *);	/* Show protocol info (for `show protocols all' command) */
+  void (*show_proto_info_json)(struct proto *);	/* Dump protocol info (for `json protocols' command) */
   void (*copy_config)(struct proto_config *, struct proto_config *);	/* Copy config from given protocol instance */
 };
 
@@ -81,6 +82,7 @@ struct proto * proto_spawn(struct proto_config *cf, uint disabled);
 bool proto_disable(struct proto *p);
 bool proto_enable(struct proto *p);
 void protos_dump_all(struct dump_request *);
+void protos_json_all(void);
 
 #define GA_UNKNOWN	0		/* Attribute not recognized */
 #define GA_NAME		1		/* Result = name */
@@ -265,10 +267,13 @@ static inline void proto_send_event(struct proto *p, event *e)
 { ev_send(proto_event_list(p), e); }
 
 void channel_show_limit(struct limit *l, const char *dsc, int active, int action);
+void channel_show_limit_json(struct limit *l, const char *dsc, int active, int action);
 void channel_show_info(struct channel *c);
+void channel_show_info_json(struct channel *c);
 void channel_cmd_debug(struct channel *c, uint mask);
 
 void proto_cmd_show(struct proto *, uintptr_t, int);
+void proto_cmd_show_json(struct proto *, int);
 void proto_cmd_disable(struct proto *, uintptr_t, int);
 void proto_cmd_enable(struct proto *, uintptr_t, int);
 void proto_cmd_restart(struct proto *, uintptr_t, int);
diff --git a/proto/bgp/bgp.c b/proto/bgp/bgp.c
index 394759255..127051ca4 100644
--- a/proto/bgp/bgp.c
+++ b/proto/bgp/bgp.c
@@ -3423,6 +3423,23 @@ bgp_show_afis(int code, char *s, u32 *afis, uint count)
   cli_msg(code, b.start);
 }
 
+static void
+bgp_show_afis_json(int code, u32 *afis, uint count)
+{
+  cli_msg(code, "[");
+
+  for (u32 *af = afis; af < (afis + count); af++)
+  {
+    if (af != afis)
+      cli_msg(code, ",");
+    const struct bgp_af_desc *desc = bgp_get_af_desc(*af);
+    cli_msg(code, "{\"AFI\":%ju,\"SAFI\":%ju,\"desc\":%js}",
+            BGP_AFI(*af), BGP_SAFI(*af), desc ? desc->name : NULL);
+  }
+
+  cli_msg(code, "]");
+}
+
 const char *
 bgp_format_role_name(u8 role)
 {
@@ -3565,6 +3582,147 @@ bgp_show_capabilities(struct bgp_proto *p UNUSED, struct bgp_caps *caps)
     cli_msg(-1006, "      Role: %s", bgp_format_role_name(caps->role));
 }
 
+static void
+bgp_show_capabilities_json(struct bgp_proto *p UNUSED, struct bgp_caps *caps)
+{
+  struct bgp_af_caps *ac;
+  uint any_mp_bgp = 0;
+  uint any_gr_able = 0;
+  uint any_add_path = 0;
+  uint any_ext_next_hop = 0;
+  uint any_llgr_able = 0;
+  u32 *afl1 = alloca(caps->af_count * sizeof(u32));
+  u32 *afl2 = alloca(caps->af_count * sizeof(u32));
+  uint afn1, afn2;
+
+  WALK_AF_CAPS(caps, ac)
+  {
+    any_mp_bgp |= ac->ready;
+    any_gr_able |= ac->gr_able;
+    any_add_path |= ac->add_path;
+    any_ext_next_hop |= ac->ext_next_hop;
+    any_llgr_able |= ac->llgr_able;
+  }
+
+  cli_msg(-1006, "\"Multiprotocol\":%jb", any_mp_bgp);
+  if (any_mp_bgp)
+  {
+    afn1 = 0;
+    WALK_AF_CAPS(caps, ac)
+      if (ac->ready)
+	afl1[afn1++] = ac->afi;
+
+    cli_msg(-1006, ",\"AF announced\":");
+    bgp_show_afis_json(-1006, afl1, afn1);
+  }
+
+  cli_msg(-1006, ",\"Route refresh\":%jb", caps->route_refresh);
+
+  cli_msg(-1006, ",\"Extended next hop\":%jb", any_ext_next_hop);
+  if (any_ext_next_hop)
+  {
+    afn1 = 0;
+    WALK_AF_CAPS(caps, ac)
+      if (ac->ext_next_hop)
+	afl1[afn1++] = ac->afi;
+
+    cli_msg(-1006, ",\"IPv6 nexthop\":");
+    bgp_show_afis_json(-1006, afl1, afn1);
+  }
+
+  cli_msg(-1006, ",\"Extended message\":%jb", caps->ext_messages);
+  cli_msg(-1006, ",\"Graceful restart aware\":%jb", caps->gr_aware);
+
+  if (any_gr_able)
+  {
+    cli_msg(-1006, ",\"Graceful restart\":{");
+    /* Continues from gr_aware */
+    cli_msg(-1006, "\"Restart time\":%ju", caps->gr_time);
+    cli_msg(-1006, ",\"Restart recovery\":%jb", caps->gr_flags & BGP_GRF_RESTART);
+
+    afn1 = afn2 = 0;
+    WALK_AF_CAPS(caps, ac)
+    {
+      if (ac->gr_able)
+	afl1[afn1++] = ac->afi;
+
+      if (ac->gr_af_flags & BGP_GRF_FORWARDING)
+	afl2[afn2++] = ac->afi;
+    }
+
+    cli_msg(-1006, ",\"AF supported\":");
+    bgp_show_afis_json(-1006, afl1, afn1);
+    cli_msg(-1006, ",\"AF preserved\":");
+    bgp_show_afis_json(-1006, afl2, afn2);
+
+    cli_msg(-1006, "}");
+  }
+
+  cli_msg(-1006, ",\"4-octet AS numbers\":%jb", caps->as4_support);
+  cli_msg(-1006, ",\"ADD-PATH aware\":%jb", any_add_path);
+
+  if (any_add_path)
+  {
+    cli_msg(-1006, ",\"ADD-PATH\":{");
+
+    afn1 = afn2 = 0;
+    WALK_AF_CAPS(caps, ac)
+    {
+      if (ac->add_path & BGP_ADD_PATH_RX)
+	afl1[afn1++] = ac->afi;
+
+      if (ac->add_path & BGP_ADD_PATH_TX)
+	afl2[afn2++] = ac->afi;
+    }
+
+    cli_msg(-1006, "\"RX\":");
+    bgp_show_afis_json(-1006, afl1, afn1);
+    cli_msg(-1006, ",\"TX\":");
+    bgp_show_afis_json(-1006, afl2, afn2);
+
+    cli_msg(-1006, "}");
+  }
+
+  cli_msg(-1006, ",\"Enhanced refresh\":%jb", caps->enhanced_refresh);
+  cli_msg(-1006, ",\"Long-lived graceful restart aware\":%jb", caps->llgr_aware);
+
+  if (any_llgr_able)
+  {
+    cli_msg(-1006, ",\"Long-lived graceful restart\":{");
+
+    u32 stale_time = 0;
+
+    afn1 = afn2 = 0;
+    WALK_AF_CAPS(caps, ac)
+    {
+      stale_time = MAX(stale_time, ac->llgr_time);
+
+      if (ac->llgr_able && ac->llgr_time)
+	afl1[afn1++] = ac->afi;
+
+      if (ac->llgr_flags & BGP_GRF_FORWARDING)
+	afl2[afn2++] = ac->afi;
+    }
+
+    /* Continues from llgr_aware */
+    cli_msg(-1006, "\"LL stale time\":%ju", stale_time);
+
+    cli_msg(-1006, ",\"AF supported\":");
+    bgp_show_afis_json(-1006, afl1, afn1);
+    cli_msg(-1006, ",\"AF preserved\":");
+    bgp_show_afis_json(-1006, afl2, afn2);
+
+    cli_msg(-1006, "}");
+  }
+
+  cli_msg(-1006, ",\"Hostname\":%js", caps->hostname);
+
+  if (caps->role != BGP_ROLE_UNDEFINED)
+    cli_msg(-1006, ",\"Role\":%js", bgp_format_role_name(caps->role));
+  else
+    cli_msg(-1006, ",\"Role\":null");
+}
+
 static void
 bgp_show_proto_info(struct proto *P)
 {
@@ -3719,6 +3877,177 @@ bgp_show_proto_info(struct proto *P)
   }
 }
 
+static void
+bgp_show_proto_info_json(struct proto *P)
+{
+  struct bgp_proto *p = (struct bgp_proto *) P;
+
+  cli_msg(-1006, ",\"BGP state\":%js", bgp_state_dsc(p));
+
+  if (bgp_is_dynamic(p) && p->cf->remote_range)
+    cli_msg(-1006, ",\"Neighbor range\":%jN", p->cf->remote_range);
+  else
+    cli_msg(-1006, ",\"Neighbor address IP\":%jI,\"Neighbor address iface\":%jJ", p->remote_ip, p->cf->iface);
+
+  if (p->conn == &p->outgoing_conn)
+    cli_msg(-1006, ",\"Neighbor port\":%ju", p->cf->remote_port);
+
+  cli_msg(-1006, ",\"Neighbor AS\":%ju", p->remote_as);
+  cli_msg(-1006, ",\"Local AS\":%ju", p->cf->local_as);
+
+  cli_msg(-1006, ",\"Neighbor graceful restart active\":%jb", p->gr_active_num);
+
+  if (P->proto_state == PS_START)
+  {
+    struct bgp_conn *oc = &p->outgoing_conn;
+
+    if ((bgp_start_state(p) < BSS_CONNECT) &&
+	(tm_active(p->startup_timer)))
+      cli_msg(-1006, ",\"Error wait timer\":%t,\"Error wait delay\":%ju",
+	      tm_remains(p->startup_timer), p->startup_delay);
+
+    if ((oc->state == BS_ACTIVE) &&
+	(tm_active(oc->connect_timer)))
+      cli_msg(-1006, ",\"Connect delay timer\":%t,\"Connect delay time\":%ju",
+	      tm_remains(oc->connect_timer), p->cf->connect_delay_time);
+
+    if (p->gr_active_num && tm_active(p->gr_timer))
+      cli_msg(-1006, ",\"Restart timer\":%t",
+	      tm_remains(p->gr_timer));
+  }
+  else if (P->proto_state == PS_UP)
+  {
+    cli_msg(-1006, ",\"Neighbor ID\":%jR", p->remote_id);
+    cli_msg(-1006, ",\"Local capabilities\":{");
+    bgp_show_capabilities_json(p, p->conn->local_caps);
+    cli_msg(-1006, "},\"Neighbor capabilities\":{");
+    bgp_show_capabilities_json(p, p->conn->remote_caps);
+    cli_msg(-1006, "},\"Session\":{\"internal\":%jb,\"multihop\":%jb,\"route-reflecor\":%jb,\"route-server\":%jb,\"AS4\":%jb}",
+	    p->is_internal,
+	    p->cf->multihop,
+	    p->rr_client,
+	    p->rs_client,
+	    p->as4_session);
+    cli_msg(-1006, ",\"Source address\":%jI", p->local_ip);
+    cli_msg(-1006, ",\"Hold timer remains\":%t,\"Hold time\":%ju",
+	    tm_remains(p->conn->hold_timer), p->conn->hold_time);
+    cli_msg(-1006, ",\"Keepalive timer remains\":%t,\"Keepalive time\":%ju",
+	    tm_remains(p->conn->keepalive_timer), p->conn->keepalive_time);
+    cli_msg(-1006, ",\"TX pending bytes\":%jd",
+	    p->conn->sk->tpos - p->conn->sk->ttx);
+    cli_msg(-1006, ",\"Send hold timer remains\":%t,\"Send hold time\":%ju",
+	    tm_remains(p->conn->send_hold_timer), p->conn->send_hold_time);
+
+    if (EMPTY_LIST(p->ao.keys))
+    {
+      cli_msg(-1006, ",\"TCP-AO\":null");
+    }
+    else
+    {
+      struct ao_info info;
+      sk_get_ao_info(p->conn->sk, &info);
+
+      cli_msg(-1006, ",\"TCP-AO\":{");
+      cli_msg(-1006, "\"Current key\":%ji", info.current_key);
+      cli_msg(-1006, ",\"RNext key\":%ji", info.rnext_key);
+      cli_msg(-1006, ",\"Good packets\":%jlu", info.pkt_good);
+      cli_msg(-1006, ",\"Bad packets\":%jlu", info.pkt_bad);
+      cli_msg(-1006, "}");
+    }
+  }
+
+  struct bgp_stats *s = &p->stats;
+  cli_msg(-1006, ",\"FSM established transitions\":%ju",
+	  s->fsm_established_transitions);
+  cli_msg(-1006, ",\"Rcvd messages\":{\"total\":%ju,\"updates\":%ju,\"bytes\":%jlu}",
+	  s->rx_messages, s->rx_updates, s->rx_bytes);
+  cli_msg(-1006, ",\"Sent messages\":{\"total\":%ju,\"updates\":%ju,\"bytes\":%jlu}",
+	  s->tx_messages, s->tx_updates, s->tx_bytes);
+  cli_msg(-1006, ",\"Last rcvd update elapsed time\":%t",
+	  p->last_rx_update ? (current_time() - p->last_rx_update) : 0);
+
+  if ((p->last_error_class != BE_NONE) &&
+      (p->last_error_class != BE_MAN_DOWN))
+  {
+    const char *err1 = bgp_err_classes[p->last_error_class];
+    const char *err2 = bgp_last_errmsg(p);
+    cli_msg(-1006, ",\"Last error\":[%js,%js]", err1, err2);
+  }
+
+  {
+    cli_msg(-1006, ",\"Channels\":{");
+    int cnt = 0;
+    struct bgp_channel *c;
+    WALK_LIST(c, p->p.channels)
+    {
+      if (cnt)
+        cli_msg(-1006, "},");
+      ++cnt;
+
+      cli_msg(-1006, "%js:{", c->c.name);
+      channel_show_info_json(&c->c);
+
+      if (c->c.class != &channel_bgp)
+	continue;
+
+      if (p->gr_active_num)
+	cli_msg(-1006, ",\"Neighbor GR\":%js", bgp_gr_states[c->gr_active]);
+
+      if (c->stale_timer && tm_active(c->stale_timer))
+	cli_msg(-1006, ",\"LL stale timer\":%t", tm_remains(c->stale_timer));
+
+      if (c->cf->gw_mode == GW_DIRECT)
+        cli_msg(-1006, ",\"Gateway mode\":\"direct\"");
+      else if (c->cf->gw_mode == GW_RECURSIVE)
+        cli_msg(-1006, ",\"Gateway mode\":\"recursive\"");
+      else
+        cli_msg(-1006, ",\"Gateway mode\":null");
+
+      if (c->c.channel_state == CS_UP)
+      {
+	cli_msg(-1006, ",\"BGP Next hop\":%jI", c->next_hop_addr);
+	if (!ipa_zero(c->link_addr))
+	  cli_msg(-1006, ",\"BGP Next hop link\":%jI", c->link_addr);
+      }
+
+      /* After channel is deconfigured, these pointers are no longer valid */
+      if (!p->p.reconfiguring || (c->c.channel_state != CS_DOWN))
+      {
+	if (c->igp_table_ip4)
+	  cli_msg(-1006, ",\"IGP IPv4 table\":%js", c->igp_table_ip4->name);
+
+	if (c->igp_table_ip6)
+	  cli_msg(-1006, ",\"IGP IPv6 table\":%js", c->igp_table_ip6->name);
+
+	if (c->base_table)
+	  cli_msg(-1006, ",\"Base table\":%js", c->base_table->name);
+      }
+
+      if (!c->tx)
+	continue;
+
+      BGP_PTX_LOCK(c->tx, tx);
+
+      uint bucket_cnt = 0;
+      uint prefix_cnt = 0;
+      struct bgp_bucket *buck;
+      struct bgp_prefix *px;
+      WALK_LIST(buck, tx->bucket_queue)
+      {
+	bucket_cnt++;
+	WALK_LIST(px, buck->prefixes)
+	  if (px->cur)
+	    prefix_cnt++;
+      }
+
+      cli_msg(-1006, ",\"Pending\":{\"attribute sets\":%ju,\"total prefixes to send\":%ju}",
+	 bucket_cnt, prefix_cnt);
+    }
+    cli_msg(-1006, "}}");
+  }
+}
+
+
 const struct channel_class channel_bgp = {
   .channel_size =	sizeof(struct bgp_channel),
   .config_size =	sizeof(struct bgp_channel_config),
@@ -3743,6 +4072,7 @@ struct protocol proto_bgp = {
   .reconfigure = 	bgp_reconfigure,
   .copy_config = 	bgp_copy_config,
   .get_status = 	bgp_get_status,
+  .show_proto_info_json = 	bgp_show_proto_info_json,
   .show_proto_info = 	bgp_show_proto_info
 };
 
diff --git a/proto/bmp/bmp.c b/proto/bmp/bmp.c
index baed8bf5f..86a492dec 100644
--- a/proto/bmp/bmp.c
+++ b/proto/bmp/bmp.c
@@ -1584,6 +1584,31 @@ bmp_show_proto_info(struct proto *P)
   }
 }
 
+static void
+bmp_show_proto_info_json(struct proto *P)
+{
+  struct bmp_proto *p = (void *) P;
+
+  if (P->proto_state != PS_DOWN_XX)
+  {
+    cli_msg(-1006, ",\"Station address\":%jI", p->station_ip);
+    cli_msg(-1006, ",\"Station port\":%ju", p->station_port);
+    if (ipa_zero(p->local_addr))
+      cli_msg(-1006, ",\"Local address\":null");
+    else
+      cli_msg(-1006, ",\"Local address\":%jI", p->local_addr);
+
+    cli_msg(-1006, ",\"Last error\":%jM", p->sock_err);
+
+    cli_msg(-1006, ",\"Pending TX count\":%ju,\"Pending TX limit\":%ju",
+	p->tx_pending_count * (u64) page_size,
+	p->tx_pending_limit * (u64) page_size);
+
+    cli_msg(-1006, ",\"Session TX\":%ju", p->tx_sent);
+    cli_msg(-1006, ",\"Total TX\":%ju", p->tx_sent_total);
+  }
+}
+
 struct protocol proto_bmp = {
   .name = "BMP",
   .template = "bmp%d",
@@ -1595,6 +1620,7 @@ struct protocol proto_bmp = {
   .shutdown = bmp_shutdown,
   .reconfigure = bmp_reconfigure,
   .get_status = bmp_get_status,
+  .show_proto_info_json = bmp_show_proto_info_json,
   .show_proto_info = bmp_show_proto_info,
 };
 
diff --git a/proto/pipe/pipe.c b/proto/pipe/pipe.c
index 0420ca140..1278f6367 100644
--- a/proto/pipe/pipe.c
+++ b/proto/pipe/pipe.c
@@ -266,6 +266,69 @@ pipe_show_stats(struct pipe_proto *p)
 	  rs2i->withdraws_ignored, rs2i->withdraws_accepted);
 }
 
+static void
+pipe_show_stats_json(struct pipe_proto *p)
+{
+  if (p->p.proto_state == PS_DOWN_XX) {
+    cli_msg(-1006, "\"Stats\":null");
+    return;
+  }
+
+  struct channel_import_stats *s1i = &p->pri->import_stats;
+  struct channel_export_stats *s1e = &p->pri->export_stats;
+  struct channel_import_stats *s2i = &p->sec->import_stats;
+  struct channel_export_stats *s2e = &p->sec->export_stats;
+
+  struct rt_import_stats *rs1i = p->pri->in_req.hook ? &p->pri->in_req.hook->stats : NULL;
+  struct rt_export_stats *rs1e = &p->pri->out_req.stats;
+  struct rt_import_stats *rs2i = p->sec->in_req.hook ? &p->sec->in_req.hook->stats : NULL;
+  struct rt_export_stats *rs2e = &p->sec->out_req.stats;
+
+  u32 pri_routes = p->pri->in_limit.count;
+  u32 sec_routes = p->sec->in_limit.count;
+
+  /*
+   * Pipe stats (as anything related to pipes) are a bit tricky. There
+   * are two sets of stats - s1 for ahook to the primary routing and
+   * s2 for the ahook to the secondary routing table. The user point
+   * of view is that routes going from the primary routing table to
+   * the secondary routing table are 'exported', while routes going in
+   * the other direction are 'imported'.
+   *
+   * Each route going through a pipe is, technically, first exported
+   * to the pipe and then imported from that pipe and such operations
+   * are counted in one set of stats according to the direction of the
+   * route propagation. Filtering is done just in the first part
+   * (export). Therefore, we compose stats for one directon for one
+   * user direction from both import and export stats, skipping
+   * immediate and irrelevant steps (exp_updates_accepted,
+   * imp_updates_received, imp_updates_filtered, ...).
+   *
+   * Rule of thumb is that stats s1 have the correct 'polarity'
+   * (imp/exp), while stats s2 have switched 'polarity'.
+   */
+
+  cli_msg(-1006, "\"Stats\":{");
+
+  cli_msg(-1006, "\"Routes\":{\"imported\":%ju,\"exported\":%ju,\"preferred\":null,\"filtered\":null}",
+	  pri_routes, sec_routes);
+
+  cli_msg(-1006, ",\"Route change\":{");
+  cli_msg(-1006, "\"Import updates\":{\"received\":%ju,\"rejected\":%ju,\"filtered\":%ju,\"ignored\":%ju,\"accepted\":%ju}",
+	  rs2e->updates_received, s2e->updates_rejected + s1i->updates_invalid,
+	  s2e->updates_filtered, rs1i->updates_ignored, rs1i->updates_accepted);
+  cli_msg(-1006, ",\"Import withdraws\":{\"received\":%ju,\"rejected\":%ju,\"ignored\":%ju,\"accepted\":%ju}",
+	  rs2e->withdraws_received, s1i->withdraws_invalid,
+	  rs1i->withdraws_ignored, rs1i->withdraws_accepted);
+  cli_msg(-1006, ",\"Export updates\":{\"received\":%ju,\"rejected\":%ju,\"filtered\":%ju,\"ignored\":%ju,\"accepted\":%ju}",
+	  rs1e->updates_received, s1e->updates_rejected + s2i->updates_invalid,
+	  s1e->updates_filtered, rs2i->updates_ignored, rs2i->updates_accepted);
+  cli_msg(-1006, ",\"Export withdraws\":{\"received\":%ju,\"rejected\":%ju,\"ignored\":%ju,\"accepted\":%ju}",
+	  rs1e->withdraws_received, s2i->withdraws_invalid,
+	  rs2i->withdraws_ignored, rs2i->withdraws_accepted);
+  cli_msg(-1006, "}}");
+}
+
 static void
 pipe_show_proto_info(struct proto *P)
 {
@@ -290,6 +353,29 @@ pipe_show_proto_info(struct proto *P)
     pipe_show_stats(p);
 }
 
+static void
+pipe_show_proto_info_json(struct proto *P)
+{
+  struct pipe_proto *p = (void *) P;
+
+  cli_msg(-1006, ",\"Channels\":{\"main\":{");
+  cli_msg(-1006, "\"Table\":%js", p->pri->table->name);
+  cli_msg(-1006, ",\"Peer table\":%js", p->sec->table->name);
+  cli_msg(-1006, ",\"Import state\":%js", rt_export_state_name(rt_export_get_state(&p->sec->out_req)));
+  cli_msg(-1006, ",\"Export state\":%js", rt_export_state_name(rt_export_get_state(&p->pri->out_req)));
+  cli_msg(-1006, ",\"Import filter\":%js", filter_name(p->sec->out_filter));
+  cli_msg(-1006, ",\"Export filter\":%js", filter_name(p->pri->out_filter));
+  cli_msg(-1006, ",");
+  channel_show_limit_json(&p->pri->in_limit, "Import limit:",
+      (p->pri->limit_active & (1 << PLD_IN)), p->pri->limit_actions[PLD_IN]);
+  cli_msg(-1006, ",");
+  channel_show_limit_json(&p->sec->in_limit, "Export limit:",
+      (p->sec->limit_active & (1 << PLD_IN)), p->sec->limit_actions[PLD_IN]);
+  cli_msg(-1006, "},");
+  pipe_show_stats_json(p);
+  cli_msg(-1006, "}");
+}
+
 void
 pipe_update_debug(struct proto *P)
 {
@@ -310,6 +396,7 @@ struct protocol proto_pipe = {
   .reconfigure =	pipe_reconfigure,
   .copy_config = 	pipe_copy_config,
   .get_status = 	pipe_get_status,
+  .show_proto_info_json = 	pipe_show_proto_info_json,
   .show_proto_info = 	pipe_show_proto_info
 };
 
diff --git a/proto/rpki/rpki.c b/proto/rpki/rpki.c
index fe6c75dd0..cb72bc5cb 100644
--- a/proto/rpki/rpki.c
+++ b/proto/rpki/rpki.c
@@ -929,6 +929,16 @@ rpki_show_proto_info_timer(const char *name, uint num, timer *t)
     cli_msg(-1006, "  %-16s: ---", name);
 }
 
+static void
+rpki_show_proto_info_timer_json(const char *name, uint num, timer *t)
+{
+  cli_msg(-1006, "%js:", name);
+  if (tm_active(t))
+    cli_msg(-1006, "{\"remains\":%t,\"interval\":%ju}", tm_remains(t), num);
+  else
+    cli_msg(-1006, "null");
+}
+
 static void
 rpki_show_proto_info(struct proto *P)
 {
@@ -1008,6 +1018,97 @@ rpki_show_proto_info(struct proto *P)
   }
 }
 
+static void
+rpki_show_proto_info_json(struct proto *P)
+{
+  struct rpki_proto *p = (struct rpki_proto *) P;
+  struct rpki_config *cf = (void *) p->p.cf;
+  struct rpki_cache *cache = p->cache;
+
+  if ((P->proto_state == PS_DOWN_XX) || (P->proto_state == PS_FLUSH))
+    return;
+
+  if (cache)
+  {
+    const char *transport_name = NULL;
+
+    switch (cf->tr_config.type)
+    {
+#if HAVE_LIBSSH
+    case RPKI_TR_SSH:
+      transport_name = "SSHv2";
+      break;
+#endif
+    case RPKI_TR_TCP:;
+      struct rpki_tr_tcp_config *tcp_cf = (void *) cf->tr_config.spec;
+      if (tcp_cf->auth_type == RPKI_TCP_AUTH_MD5)
+	transport_name = "TCP-MD5";
+      else
+	transport_name = "Unprotected over TCP";
+      break;
+    };
+
+    cli_msg(-1006, ",\"Cache server\":%js", cf->hostname);
+    cli_msg(-1006, ",\"Cache port\":%ju", cf->port);
+
+    cli_msg(-1006, ",\"Status\":%js", rpki_cache_state_to_str(cache->state));
+    cli_msg(-1006, ",\"Transport\":%js", transport_name);
+    cli_msg(-1006, ",\"Protocol version\":%ju", cache->version);
+
+    if (cache->request_session_id)
+      cli_msg(-1006, ",\"Session ID\":null");
+    else
+      cli_msg(-1006, ",\"Session ID\":%ju", cache->session_id);
+
+    if (cache->last_update)
+    {
+      cli_msg(-1006, ",\"Serial number\":%ju", cache->serial_num);
+      cli_msg(-1006, ",\"Last update\":%t", current_time() - cache->last_update);
+    }
+    else
+    {
+      cli_msg(-1006, ",\"Serial number\":null");
+      cli_msg(-1006, ",\"Last update\":null");
+    }
+
+    cli_msg(-1006, ",\"timers\":{");
+    rpki_show_proto_info_timer_json("Refresh timer", cache->refresh_interval, cache->refresh_timer);
+    cli_msg(-1006, ",");
+    rpki_show_proto_info_timer_json("Retry timer", cache->retry_interval, cache->retry_timer);
+    cli_msg(-1006, ",");
+    rpki_show_proto_info_timer_json("Expire timer", cache->expire_interval, cache->expire_timer);
+    cli_msg(-1006, "},\"Channels\":{");
+
+    if (p->roa4_channel) {
+      cli_msg(-1006, "\"roa4\":{");
+      channel_show_info_json(p->roa4_channel);
+      cli_msg(-1006, "}");
+    } else {
+      cli_msg(-1006, "\"roa4\":null");
+    }
+
+    cli_msg(-1006, ",");
+    if (p->roa6_channel) {
+      cli_msg(-1006, "\"roa6\":{");
+      channel_show_info_json(p->roa6_channel);
+      cli_msg(-1006, "}");
+    } else {
+      cli_msg(-1006, "\"roa6\":null");
+    }
+
+    cli_msg(-1006, ",");
+    if (p->aspa_channel) {
+      cli_msg(-1006, "\"aspa\":{");
+      channel_show_info_json(p->aspa_channel);
+      cli_msg(-1006, "}");
+    } else {
+      cli_msg(-1006, "\"aspa\":null");
+    }
+
+    cli_msg(-1006, "}");
+  }
+}
+
 
 /*
  * 	RPKI Protocol Configuration
@@ -1080,6 +1181,7 @@ struct protocol proto_rpki = {
   .start = 		rpki_start,
   .postconfig = 	rpki_postconfig,
   .channel_mask =	(NB_ROA4 | NB_ROA6 | NB_ASPA),
+  .show_proto_info_json =	rpki_show_proto_info_json,
   .show_proto_info =	rpki_show_proto_info,
   .shutdown = 		rpki_shutdown,
   .copy_config = 	rpki_copy_config,
