gcoap_forward_proxy: add capability to add upstream proxy
Contribution description

This allows to set an upstream proxy for the local forward proxy. I had a look at what @cgundogan did for https://doi.org/10.1145/3405656.3418718, however, his approach is very app-centric and I think this solution is much more streamlined (albeit, probably to @chrysn's horror, URI-based).
Testing procedure
I patched examples/gcoap/client.c to be able to set the proxy for the local forward proxy (if one is present) and capabilities to get some feedback from the gcoap_forward_proxy_upstream pseudo-module.
Patch
diff --git a/examples/gcoap/client.c b/examples/gcoap/client.c
index d22c62eac0..8ef6895f30 100644
--- a/examples/gcoap/client.c
+++ b/examples/gcoap/client.c
@@ -28,2 +28,3 @@
#include "net/gcoap.h"
+#include "net/gcoap/forward_proxy.h"
#include "net/utils.h"
@@ -238,3 +239,5 @@ int gcoap_cli_cmd(int argc, char **argv)
else if (strcmp(argv[1], "proxy") == 0) {
+ static char proxy[CONFIG_GCOAP_FORWARD_PROXY_UPSTREAM_URI_MAX];
if ((argc == 5) && (strcmp(argv[2], "set") == 0)) {
+ int res;
if (!_parse_endpoint(&_proxy_remote, argv[3], argv[4])) {
@@ -243,2 +246,7 @@ int gcoap_cli_cmd(int argc, char **argv)
}
+ sprintf(proxy, "coap://[%s]:%s/", argv[3], argv[4]);
+ if ((res = gcoap_forward_proxy_upstream_set(proxy)) < 0) {
+ printf("Could not set upstream proxy %s: %s\n", proxy, strerror(-res));
+ return 1;
+ }
_proxied = true;
@@ -249,4 +257,11 @@ int gcoap_cli_cmd(int argc, char **argv)
_proxied = false;
+ gcoap_forward_proxy_upstream_set(NULL);
return 0;
}
+ if (gcoap_forward_proxy_upstream_get(proxy, sizeof(proxy)) > 0) {
+ printf("proxy: %s\n", proxy);
+ }
+ else {
+ puts("proxy: not set");
+ }
printf("usage: %s proxy set <addr>[%%iface] <port>\n", argv[0]);
Test script
#! /usr/bin/env python3
import sys
from riotctrl.ctrl import RIOTCtrl
sys.path.append("dist/pythonlibs")
from riotctrl_shell.netif import IfconfigListParser, Ifconfig
def set_proxy(client, proxy):
res = client.cmd(f"coap proxy")
assert f"proxy: not set" in res
netifs = IfconfigListParser().parse(proxy.ifconfig_list())
proxy_addr = list(netifs.values())[0]["ipv6_addrs"][0]["addr"]
res = client.cmd(f"coap proxy set {proxy_addr} 5683")
assert f"coap proxy set {proxy_addr} 5683" in res
res = client.cmd(f"coap proxy")
assert f"proxy: coap://[{proxy_addr}]:5683" in res
if __name__ == "__main__":
RIOTCtrl.MAKE_ARGS = ("-j",)
env = {"USEMODULE": "gcoap_forward_proxy_upstream", "PORT": "tap0"}
ctrl0 = RIOTCtrl(application_directory="examples/gcoap", env=env)
env = {"USEMODULE": "gcoap_forward_proxy_upstream", "PORT": "tap1"}
ctrl1 = RIOTCtrl(application_directory="examples/gcoap", env=env)
env = {"USEMODULE": "gcoap_forward_proxy_upstream", "PORT": "tap2"}
ctrl2 = RIOTCtrl(application_directory="examples/gcoap", env=env)
env = {"USEMODULE": "gcoap_forward_proxy_upstream", "PORT": "tap3"}
ctrl3 = RIOTCtrl(application_directory="examples/gcoap", env=env)
ctrl0.make_run(["flash"])
with (
ctrl0.run_term(reset=False),
ctrl1.run_term(reset=False),
ctrl2.run_term(reset=False),
ctrl3.run_term(reset=False),
):
ctrl0.term.logfile = sys.stdout
ctrl1.term.logfile = sys.stdout
ctrl2.term.logfile = sys.stdout
ctrl3.term.logfile = sys.stdout
shell0 = Ifconfig(ctrl0)
shell1 = Ifconfig(ctrl1)
shell2 = Ifconfig(ctrl2)
shell3 = Ifconfig(ctrl3)
netifs = IfconfigListParser().parse(shell0.ifconfig_list())
server_addr = list(netifs.values())[0]["ipv6_addrs"][0]["addr"]
shell3.cmd(f"coap get {server_addr} 5683 /.well-known/core")
# comes in asynchronously, so we need to expect instead of checking result
shell3.riotctrl.term.expect_exact(
'</cli/stats>;ct=0;rt="count";obs,</riot/board>,</>'
)
set_proxy(shell2, shell1)
set_proxy(shell3, shell2)
shell3.cmd(f"coap get {server_addr} 5683 /.well-known/core")
# comes in asynchronously, so we need to expect instead of checking result
shell3.riotctrl.term.expect_exact(
'</cli/stats>;ct=0;rt="count";obs,</riot/board>,</>'
)
shell2.cmd("coap proxy unset")
res = shell2.cmd(f"coap proxy")
assert f"proxy: not set" in res
shell3.cmd(f"coap get {server_addr} 5683 /.well-known/core")
# comes in asynchronously, so we need to expect instead of checking result
shell3.riotctrl.term.expect_exact(
'</cli/stats>;ct=0;rt="count";obs,</riot/board>,</>'
)
With that Wireshark showed the following 12 CoAP packets:
The first two are a request-response pair for the unproxied GET request (destination is the link local of ctrl0, fe80::e0bc:7dff:fecb:f550, and there is no Proxy-URI option present).
The next 6 are 3 GET requests, the first two of them are proxied (with Proxy-URI option set to coap://[fe80::e0bc:7dff:fecb:f550]:5683/.well-known/core), while the last goes to actual server. Then are following their 3 responses.
For the last 4, the upstream most proxy got its proxy removed (resetting the behavior of the downstream most proxy to a proxy without an upstream). We see first 1 request going to the downstream most proxy, which then forwards it to the server. That request is followed by the 2 corresponding responses.
Issues/PRs references
None
Hi! :bird:
Could you rebase? There are not tests for this, do you intend to write some? Are there difficulties to it?