diff --git a/man/systemd.network.xml b/man/systemd.network.xml
index 818df99e5a..b9d74d6097 100644
--- a/man/systemd.network.xml
+++ b/man/systemd.network.xml
@@ -1622,6 +1622,15 @@ Table=1234
+
+ TCPCongestionControlAlgorithm=
+
+ Specifies the TCP congestion control algorithm for the route. Takes a name of the algorithm,
+ e.g. bbr, dctcp, or vegas. When unset,
+ the kernel's default will be used.
+
+
+
MultiPathRoute=address[@name] [weight]
diff --git a/src/libsystemd/sd-netlink/netlink-types-rtnl.c b/src/libsystemd/sd-netlink/netlink-types-rtnl.c
index be6f7cf791..919512d110 100644
--- a/src/libsystemd/sd-netlink/netlink-types-rtnl.c
+++ b/src/libsystemd/sd-netlink/netlink-types-rtnl.c
@@ -929,7 +929,7 @@ static const NLAPolicy rtnl_route_metrics_policies[] = {
[RTAX_RTO_MIN] = BUILD_POLICY(U32),
[RTAX_INITRWND] = BUILD_POLICY(U32),
[RTAX_QUICKACK] = BUILD_POLICY(U32),
- [RTAX_CC_ALGO] = BUILD_POLICY(U32),
+ [RTAX_CC_ALGO] = BUILD_POLICY(STRING),
[RTAX_FASTOPEN_NO_COOKIE] = BUILD_POLICY(U32),
};
diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf
index a4f038681a..dce7339d2f 100644
--- a/src/network/networkd-network-gperf.gperf
+++ b/src/network/networkd-network-gperf.gperf
@@ -196,6 +196,7 @@ Route.Type, config_parse_route_type,
Route.InitialCongestionWindow, config_parse_tcp_window, 0, 0
Route.InitialAdvertisedReceiveWindow, config_parse_tcp_window, 0, 0
Route.TCPAdvertisedMaximumSegmentSize, config_parse_tcp_advmss, 0, 0
+Route.TCPCongestionControlAlgorithm, config_parse_tcp_congestion, 0, 0
Route.QuickAck, config_parse_route_boolean, 0, 0
Route.FastOpenNoCookie, config_parse_route_boolean, 0, 0
Route.TTLPropagate, config_parse_route_boolean, 0, 0
diff --git a/src/network/networkd-route.c b/src/network/networkd-route.c
index 71e578d898..4f097364a1 100644
--- a/src/network/networkd-route.c
+++ b/src/network/networkd-route.c
@@ -107,6 +107,8 @@ Route *route_free(Route *route) {
sd_event_source_disable_unref(route->expire);
+ free(route->tcp_congestion_control_algo);
+
return mfree(route);
}
@@ -319,6 +321,7 @@ int route_get(Manager *manager, Link *link, const Route *in, Route **ret) {
int route_dup(const Route *src, Route **ret) {
_cleanup_(route_freep) Route *dest = NULL;
+ int r;
/* This does not copy mulipath routes. */
@@ -336,6 +339,11 @@ int route_dup(const Route *src, Route **ret) {
dest->manager = NULL;
dest->multipath_routes = NULL;
dest->expire = NULL;
+ dest->tcp_congestion_control_algo = NULL;
+
+ r = free_and_strdup(&dest->tcp_congestion_control_algo, src->tcp_congestion_control_algo);
+ if (r < 0)
+ return r;
*ret = TAKE_PTR(dest);
return 0;
@@ -1231,6 +1239,12 @@ static int route_configure(const Route *route, uint32_t lifetime_sec, Link *link
return r;
}
+ if (!isempty(route->tcp_congestion_control_algo)) {
+ r = sd_netlink_message_append_string(m, RTAX_CC_ALGO, route->tcp_congestion_control_algo);
+ if (r < 0)
+ return r;
+ }
+
r = sd_netlink_message_close_container(m);
if (r < 0)
return r;
@@ -2498,6 +2512,46 @@ int config_parse_route_type(
return 0;
}
+int config_parse_tcp_congestion(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ Network *network = userdata;
+ _cleanup_(route_free_or_set_invalidp) Route *n = NULL;
+ int r;
+
+ assert(filename);
+ assert(section);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ r = route_new_static(network, filename, section_line, &n);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to allocate route, ignoring assignment: %m");
+ return 0;
+ }
+
+ r = config_parse_string(unit, filename, line, section, section_line, lvalue, ltype,
+ rvalue, &n->tcp_congestion_control_algo, userdata);
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(n);
+ return 0;
+}
+
int config_parse_tcp_advmss(
const char *unit,
const char *filename,
diff --git a/src/network/networkd-route.h b/src/network/networkd-route.h
index b431e1a30f..fd4433eae4 100644
--- a/src/network/networkd-route.h
+++ b/src/network/networkd-route.h
@@ -50,6 +50,7 @@ struct Route {
uint32_t initcwnd;
uint32_t initrwnd;
uint32_t advmss;
+ char *tcp_congestion_control_algo;
unsigned char pref;
unsigned flags;
int gateway_onlink; /* Only used in conf parser and route_section_verify(). */
@@ -123,5 +124,6 @@ CONFIG_PARSER_PROTOTYPE(config_parse_route_type);
CONFIG_PARSER_PROTOTYPE(config_parse_tcp_window);
CONFIG_PARSER_PROTOTYPE(config_parse_route_mtu);
CONFIG_PARSER_PROTOTYPE(config_parse_multipath_route);
+CONFIG_PARSER_PROTOTYPE(config_parse_tcp_congestion);
CONFIG_PARSER_PROTOTYPE(config_parse_tcp_advmss);
CONFIG_PARSER_PROTOTYPE(config_parse_route_nexthop);
diff --git a/test/fuzz/fuzz-network-parser/directives b/test/fuzz/fuzz-network-parser/directives
index b7e8f16834..6f76f7ee18 100644
--- a/test/fuzz/fuzz-network-parser/directives
+++ b/test/fuzz/fuzz-network-parser/directives
@@ -189,6 +189,7 @@ Metric=
TTLPropagate=
MultiPathRoute=
TCPAdvertisedMaximumSegmentSize=
+TCPCongestionControlAlgorithm=
NextHop=
[Network]
KeepMaster=
diff --git a/test/test-network/conf/25-route-congctl.network b/test/test-network/conf/25-route-congctl.network
new file mode 100644
index 0000000000..f924d73cd9
--- /dev/null
+++ b/test/test-network/conf/25-route-congctl.network
@@ -0,0 +1,16 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+[Match]
+Name=dummy98
+
+[Network]
+IPv6AcceptRA=no
+Address=2001:1234:5:8f63::1/128
+Address=149.10.124.58/28
+
+[Route]
+Destination=2001:1234:5:8fff:ff:ff:ff:ff/128
+TCPCongestionControlAlgorithm=dctcp
+
+[Route]
+Destination=149.10.124.66
+TCPCongestionControlAlgorithm=dctcp
diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py
index 284bc3146f..1dddd86331 100755
--- a/test/test-network/systemd-networkd-tests.py
+++ b/test/test-network/systemd-networkd-tests.py
@@ -2800,6 +2800,24 @@ class NetworkdNetworkTests(unittest.TestCase, Utilities):
self.assertRegex(output, '149.10.124.48/28 proto kernel scope link src 149.10.124.58')
self.assertRegex(output, '149.10.124.66 via inet6 2001:1234:5:8fff:ff:ff:ff:ff proto static')
+ @expectedFailureIfModuleIsNotAvailable('tcp_dctcp')
+ def test_route_congctl(self):
+ copy_network_unit('25-route-congctl.network', '12-dummy.netdev')
+ start_networkd()
+ self.wait_online(['dummy98:routable'])
+
+ print('### ip -6 route show dev dummy98 2001:1234:5:8fff:ff:ff:ff:ff')
+ output = check_output('ip -6 route show dev dummy98 2001:1234:5:8fff:ff:ff:ff:ff')
+ print(output)
+ self.assertIn('2001:1234:5:8fff:ff:ff:ff:ff proto static', output)
+ self.assertIn('congctl dctcp', output)
+
+ print('### ip -4 route show dev dummy98 149.10.124.66')
+ output = check_output('ip -4 route show dev dummy98 149.10.124.66')
+ print(output)
+ self.assertIn('149.10.124.66 proto static', output)
+ self.assertIn('congctl dctcp', output)
+
@expectedFailureIfModuleIsNotAvailable('vrf')
def test_route_vrf(self):
copy_network_unit('25-route-vrf.network', '12-dummy.netdev',