use birdc::*;
mod server;
use server::*;
#[tokio::test(flavor = "multi_thread")]
async fn test_single_client_open() {
let _ = env_logger::try_init();
let server = MockServer::start_server("", 0)
.await
.expect("failed to start server");
let client = Client::for_unix_socket(&server.unix_socket);
client.connect().await.expect("failed to connect client");
client
.connect_sync()
.expect("failed to connect sync client");
server.wait_until(1, 3).await;
}
#[tokio::test]
async fn test_multiple_client_open() {
let _ = env_logger::try_init();
let server = MockServer::start_server("", 0)
.await
.expect("failed to start server");
let client = Client::for_unix_socket(&server.unix_socket);
let results = tokio::join!(client.connect(), client.connect(), client.connect());
assert!(results.0.is_ok());
assert!(results.1.is_ok());
assert!(results.2.is_ok());
server.wait_until(1, 3).await;
}
test_sync_async_request!(
test_raw_protocol(&get_test_text(), "show interfaces", response) {
validate_show_interfaces_response(&response);
}
);
test_sync_async_request!(
test_raw_protocol_with_delays(&get_test_text(), "show interfaces", response, 100) {
validate_show_interfaces_response(&response);
}
);
test_sync_async_request!(
test_show_interfaces(&get_test_text(), show_interfaces(), response, 0) {
assert_eq!(response.len(), 3);
}
);
test_sync_async_request!(
test_show_interfaces_summary(&get_interfaces_summary(), show_interfaces_summary(), response, 0) {
assert_eq!(response.len(), 3);
assert_eq!(response[0].name, "lo");
assert_eq!(response[0].state, "up");
assert!(matches!(&response[0].ipv4_address, Some(x) if x == "127.0.0.1/8"));
assert!(matches!(&response[0].ipv6_address, Some(x) if x == "::1/128"));
assert_eq!(response[1].name, "eth0");
assert_eq!(response[1].state, "up");
assert!(matches!(&response[1].ipv4_address, Some(x) if x == "172.30.0.12/16"));
assert!(matches!(&response[1].ipv6_address, Some(x) if x == "fe80::4495:80ff:fe71:a791/64"));
assert_eq!(response[2].name, "eth1");
assert_eq!(response[2].state, "up");
assert!(matches!(&response[2].ipv4_address, Some(x) if x == "169.254.199.2/30"));
assert!(matches!(&response[2].ipv6_address, Some(x) if x == "fe80::a06f:7ff:fea7:c662/64"));
}
);
test_sync_async_request!(
test_show_protocols(&get_protocols(), show_protocols(None), protocol) {
assert_eq!(protocol.len(), 9);
assert_eq!(protocol[0].name, "device1");
assert_eq!(protocol[0].proto, "Device");
assert!(protocol[0].table.is_none());
assert_eq!(protocol[0].state, "up");
assert_eq!(protocol[0].since, "2022-04-14");
assert!(protocol[0].info.is_none());
assert_eq!(protocol[1].name, "direct_eth0");
assert_eq!(protocol[1].proto, "Direct");
assert!(protocol[1].table.is_none());
assert_eq!(protocol[1].state, "up");
assert_eq!(protocol[1].since, "2022-04-14");
assert!(protocol[1].info.is_none());
assert_eq!(protocol[2].name, "kernel_v4");
assert_eq!(protocol[2].proto, "Kernel");
assert_eq!(protocol[2].table.as_ref().unwrap(), "master4");
assert_eq!(protocol[2].state, "up");
assert_eq!(protocol[2].since, "2022-04-14 11:22:33");
assert!(protocol[2].info.is_none());
assert_eq!(protocol[3].name, "kernel_v6");
assert_eq!(protocol[3].proto, "Kernel");
assert_eq!(protocol[3].table.as_ref().unwrap(), "master6");
assert_eq!(protocol[3].state, "up");
assert_eq!(protocol[3].since, "2022-04-14");
assert!(protocol[3].info.is_none());
assert_eq!(protocol[4].name, "bfd1");
assert_eq!(protocol[4].proto, "BFD");
assert!(protocol[4].table.is_none());
assert_eq!(protocol[4].state, "up");
assert_eq!(protocol[4].since, "2022-04-14");
assert!(protocol[4].info.is_none());
assert_eq!(protocol[5].name, "bgp_local4");
assert_eq!(protocol[5].proto, "BGP");
assert!(protocol[5].table.is_none());
assert_eq!(protocol[5].state, "up");
assert_eq!(protocol[5].since, "2022-04-16");
assert_eq!(protocol[5].info.as_ref().unwrap(), "Established");
assert_eq!(protocol[6].name, "bgp_local6");
assert_eq!(protocol[6].proto, "BGP");
assert!(protocol[6].table.is_none());
assert_eq!(protocol[6].state, "up");
assert_eq!(protocol[6].since, "2022-04-16");
assert_eq!(protocol[6].info.as_ref().unwrap(), "Established");
assert_eq!(protocol[7].name, "pipe6_kernel_main");
assert_eq!(protocol[7].proto, "Pipe");
assert!(protocol[7].table.is_none());
assert_eq!(protocol[7].state, "up");
assert_eq!(protocol[7].since, "2025-10-17");
assert_eq!(protocol[7].info.as_ref().unwrap(), "table6_kernel_main <=> table6_meadow");
assert_eq!(protocol[8].name, "pipe6_kernel_default");
assert_eq!(protocol[8].proto, "Pipe");
assert!(protocol[8].table.is_none());
assert_eq!(protocol[8].state, "up");
assert_eq!(protocol[8].since, "2025-10-17 08:18:58");
assert_eq!(protocol[8].info.as_ref().unwrap(), "table6_kernel_default <=> table6_meadow");
}
);
test_sync_async_request!(
test_show_protocols_pattern(&get_protocols_only_kernel(), show_protocols(Some("kernel*")), protocol) {
assert_eq!(protocol.len(), 2);
assert_eq!(protocol[0].name, "kernel_v4");
assert_eq!(protocol[0].proto, "Kernel");
assert_eq!(protocol[0].table.as_ref().unwrap(), "master4");
assert_eq!(protocol[0].state, "up");
assert_eq!(protocol[0].since, "2022-04-14 11:22:33");
assert!(protocol[0].info.is_none());
assert_eq!(protocol[1].name, "kernel_v6");
assert_eq!(protocol[1].proto, "Kernel");
assert_eq!(protocol[1].table.as_ref().unwrap(), "master6");
assert_eq!(protocol[1].state, "up");
assert_eq!(protocol[1].since, "2022-04-14");
assert!(protocol[1].info.is_none());
}
);
test_sync_async_request!(
test_show_protocols_all(&get_protocols_all(), show_protocols_details(None), protocols) {
assert_eq!(protocols.len(), 7);
assert_eq!(protocols[0].protocol.name, "device1");
assert_eq!(protocols[0].protocol.proto, "Device");
assert_eq!(protocols[0].protocol.state, "up");
assert_eq!(protocols[0].protocol.since, "2022-04-14");
assert!(protocols[0].detail.is_none());
assert_eq!(protocols[1].protocol.name, "direct_eth0");
assert_eq!(protocols[1].protocol.proto, "Direct");
assert_eq!(protocols[1].protocol.state, "up");
assert_eq!(protocols[1].protocol.since, "2022-04-14");
let details = protocols[1]
.detail
.as_ref()
.expect("detail should've been present");
assert!(details.proto_info.is_none());
assert_eq!(details.channels[0].name, "ipv4");
assert_eq!(details.channels[0].state, "UP");
assert_eq!(details.channels[0].table, "master4");
assert_eq!(details.channels[1].name, "ipv6");
assert_eq!(details.channels[1].state, "UP");
assert_eq!(details.channels[1].table, "master6");
assert_eq!(protocols[2].protocol.name, "kernel_v4");
assert_eq!(protocols[2].protocol.proto, "Kernel");
assert_eq!(protocols[2].protocol.state, "up");
assert_eq!(protocols[2].protocol.since, "2022-04-14 11:22:33");
let details = protocols[2]
.detail
.as_ref()
.expect("detail should've been present");
assert!(details.proto_info.is_none());
assert_eq!(details.channels[0].name, "ipv4");
assert_eq!(details.channels[0].state, "UP");
assert_eq!(details.channels[0].table, "master4");
assert_eq!(protocols[3].protocol.name, "kernel_v6");
assert_eq!(protocols[3].protocol.proto, "Kernel");
assert_eq!(protocols[3].protocol.state, "up");
assert_eq!(protocols[3].protocol.since, "2022-04-14");
let details = protocols[3]
.detail
.as_ref()
.expect("detail should've been present");
assert!(details.proto_info.is_none());
assert_eq!(details.channels[0].name, "ipv6");
assert_eq!(details.channels[0].state, "UP");
assert_eq!(details.channels[0].table, "master6");
assert_eq!(protocols[4].protocol.name, "bfd1");
assert_eq!(protocols[4].protocol.proto, "BFD");
assert_eq!(protocols[4].protocol.state, "up");
assert_eq!(protocols[4].protocol.since, "2022-04-14");
assert!(protocols[4].detail.is_none());
assert_eq!(protocols[5].protocol.name, "bgp_r1_v4");
assert_eq!(protocols[5].protocol.proto, "BGP");
assert_eq!(protocols[5].protocol.state, "up");
assert_eq!(protocols[5].protocol.since, "2022-04-14");
let details = protocols[5]
.detail
.as_ref()
.expect("detail should've been present");
assert!(matches!(&details.description, Some(x) if x == "IPv4 BGP with internal router"));
let ProtoSpecificInfo::Bgp(bgp_info) = details
.proto_info
.as_ref()
.expect("proto info should've been present");
assert_eq!(bgp_info.local_as, 64560);
assert_eq!(bgp_info.neighbor_as, 64561);
let bgp_session = bgp_info
.session
.as_ref()
.expect("expected bgp session to be present");
assert_eq!(bgp_session.neighbor_id, "172.29.0.1");
assert_eq!(bgp_session.hold_time, 240);
assert_eq!(bgp_session.keepalive_time, 80);
assert_eq!(details.channels[0].name, "ipv4");
assert_eq!(details.channels[0].state, "UP");
let route_stats = details.channels[0]
.route_stats
.as_ref()
.expect("route stats should've been present");
assert_eq!(route_stats.imported, 1);
assert_eq!(route_stats.exported, 0);
assert_eq!(protocols[6].protocol.name, "bgp_r1_v6");
assert_eq!(protocols[6].protocol.proto, "BGP");
assert_eq!(protocols[6].protocol.state, "up");
assert_eq!(protocols[6].protocol.since, "2022-04-14");
let details = protocols[6]
.detail
.as_ref()
.expect("detail should've been present");
assert!(matches!(&details.description, Some(x) if x == "IPv6 BGP with internal router"));
let ProtoSpecificInfo::Bgp(bgp_info) = details
.proto_info
.as_ref()
.expect("proto info should've been present");
assert_eq!(bgp_info.local_as, 64560);
assert_eq!(bgp_info.neighbor_as, 64561);
let bgp_session = bgp_info
.session
.as_ref()
.expect("expected bgp session to be present");
assert_eq!(bgp_session.neighbor_id, "172.29.0.1");
assert_eq!(bgp_session.hold_time, 240);
assert_eq!(bgp_session.keepalive_time, 80);
assert_eq!(details.channels[0].name, "ipv6");
assert_eq!(details.channels[0].state, "UP");
let route_stats = details.channels[0]
.route_stats
.as_ref()
.expect("route stats should've been present");
assert_eq!(route_stats.imported, 1);
assert_eq!(route_stats.exported, 0);
}
);
test_sync_async_request!(
test_show_status(&get_show_status(), show_status(), status) {
assert_eq!(status.version_line, "BIRD 2.0.7");
assert_eq!(status.router_id, "172.29.0.12");
assert_eq!(status.server_time.to_string(), "2022-05-08 10:14:23.381");
assert_eq!(status.last_reboot_on.to_string(), "2022-04-14 22:23:28.096");
assert_eq!(
status.last_reconfigured_on.to_string(),
"2022-04-15 00:00:46.707",
);
assert_eq!(status.status, "Daemon is up and running");
}
);
fn validate_show_interfaces_response(response: &[Message]) {
match &response[0] {
Message::InterfaceList(s) => assert_eq!(s, "lo up (index=1)"),
_ => panic!("was expecting Message::InterfaceList"),
}
match &response[1] {
Message::InterfaceFlags(s) => {
assert_eq!(s, "\tMultiAccess AdminUp LinkUp Loopback Ignored MTU=65536")
}
_ => panic!("was expecting Message::InterfaceFlags"),
}
match &response[2] {
Message::InterfaceAddress(s) => assert_eq!(
s,
"\t127.0.0.1/8 (Preferred, scope host)\n\t::1/128 (Preferred, scope host)"
),
_ => panic!("was expecting Message::InterfaceAddress"),
}
match &response[3] {
Message::InterfaceList(s) => assert_eq!(s, "eth0 up (index=2)"),
_ => panic!("was expecting Message::InterfaceList"),
}
match &response[4] {
Message::InterfaceFlags(s) => assert_eq!(
s,
"\tMultiAccess Broadcast Multicast AdminUp LinkUp MTU=9000"
),
_ => panic!("was expecting Message::InterfaceFlags"),
}
match &response[5] {
Message::InterfaceAddress(s) => assert_eq!(
s,
"\t172.30.0.12/16 (Preferred, scope site)\n\t172.29.1.15/32 (scope univ)\n\t172.29.1.16/32 (scope univ)\n\t172.29.1.17/32 (scope univ)\n\tfe80::4495:80ff:fe71:a791/64 (Preferred, scope link)\n\tfe80::4490::72/64 (scope univ)"
),
_ => panic!("was expecting Message::InterfaceAddress"),
}
match &response[6] {
Message::InterfaceList(s) => assert_eq!(s, "eth1 up (index=3)"),
_ => panic!("was expecting Message::InterfaceList"),
}
match &response[7] {
Message::InterfaceFlags(s) => assert_eq!(
s,
"\tMultiAccess Broadcast Multicast AdminUp LinkUp MTU=1500"
),
_ => panic!("was expecting Message::InterfaceFlags"),
}
match &response[8] {
Message::InterfaceAddress(s) => assert_eq!(
s,
"\t169.254.199.2/30 (Preferred, opposite 169.254.199.1, scope univ)\n\tfe80::a06f:7ff:fea7:c662/64 (Preferred, scope link)\n\tfe80:169:254:199::2/126 (scope link)"
),
_ => panic!("was expecting Message::InterfaceAddress"),
}
}
fn get_test_text() -> String {
heredoc(
"1001-lo up (index=1)
1004-\tMultiAccess AdminUp LinkUp Loopback Ignored MTU=65536
1003-\t127.0.0.1/8 (Preferred, scope host)
\t::1/128 (Preferred, scope host)
1001-eth0 up (index=2)
1004-\tMultiAccess Broadcast Multicast AdminUp LinkUp MTU=9000
1003-\t172.30.0.12/16 (Preferred, scope site)
\t172.29.1.15/32 (scope univ)
\t172.29.1.16/32 (scope univ)
\t172.29.1.17/32 (scope univ)
\tfe80::4495:80ff:fe71:a791/64 (Preferred, scope link)
\tfe80::4490::72/64 (scope univ)
1001-eth1 up (index=3)
1004-\tMultiAccess Broadcast Multicast AdminUp LinkUp MTU=1500
1003-\t169.254.199.2/30 (Preferred, opposite 169.254.199.1, scope univ)
\tfe80::a06f:7ff:fea7:c662/64 (Preferred, scope link)
\tfe80:169:254:199::2/126 (scope link)
0000
",
)
}
fn get_interfaces_summary() -> String {
heredoc(
"2005-Interface State IPv4 address IPv6 address
1005-lo up 127.0.0.1/8 ::1/128
eth0 up 172.30.0.12/16 fe80::4495:80ff:fe71:a791/64
eth1 up 169.254.199.2/30 fe80::a06f:7ff:fea7:c662/64
0000
",
)
}
fn get_show_status() -> String {
heredoc(
"1000-BIRD 2.0.7
1011-Router ID is 172.29.0.12
Current server time is 2022-05-08 10:14:23.381
Last reboot on 2022-04-14 22:23:28.096
Last reconfiguration on 2022-04-15 00:00:46.707
0013 Daemon is up and running
",
)
}
fn get_protocols() -> String {
heredoc(
"2002-Name Proto Table State Since Info
1002-device1 Device --- up 2022-04-14
direct_eth0 Direct --- up 2022-04-14
kernel_v4 Kernel master4 up 2022-04-14 11:22:33
kernel_v6 Kernel master6 up 2022-04-14
bfd1 BFD --- up 2022-04-14
bgp_local4 BGP --- up 2022-04-16 Established
bgp_local6 BGP --- up 2022-04-16 Established
pipe6_kernel_main Pipe --- up 2025-10-17 table6_kernel_main <=> table6_meadow
pipe6_kernel_default Pipe --- up 2025-10-17 08:18:58 table6_kernel_default <=> table6_meadow
0000
",
)
}
fn get_protocols_only_kernel() -> String {
heredoc(
"2002-Name Proto Table State Since Info
1002-kernel_v4 Kernel master4 up 2022-04-14 11:22:33
kernel_v6 Kernel master6 up 2022-04-14
0000
",
)
}
fn get_protocols_all() -> String {
heredoc(
"2002-Name Proto Table State Since Info
1002-device1 Device --- up 2022-04-14
1006-
1002-direct_eth0 Direct --- up 2022-04-14
1006- Channel ipv4
State: UP
Table: master4
Preference: 240
Input filter: ACCEPT
Output filter: REJECT
Routes: 7 imported, 0 exported, 7 preferred
Route change stats: received rejected filtered ignored accepted
Import updates: 7 0 0 0 7
Import withdraws: 0 0 --- 0 0
Export updates: 0 0 0 --- 0
Export withdraws: 0 --- --- --- 0
Channel ipv6
State: UP
Table: master6
Preference: 240
Input filter: ACCEPT
Output filter: REJECT
Routes: 0 imported, 0 exported, 0 preferred
Route change stats: received rejected filtered ignored accepted
Import updates: 3 0 0 1 2
Import withdraws: 2 0 --- 0 2
Export updates: 0 0 0 --- 0
Export withdraws: 0 --- --- --- 0
1002-kernel_v4 Kernel master4 up 2022-04-14 11:22:33
1006- Channel ipv4
State: UP
Table: master4
Preference: 10
Input filter: REJECT
Output filter: save_to_kernel
Routes: 0 imported, 2 exported, 0 preferred
Route change stats: received rejected filtered ignored accepted
Import updates: 0 0 0 0 0
Import withdraws: 0 0 --- 0 0
Export updates: 20 0 14 --- 6
Export withdraws: 4 --- --- --- 4
1002-kernel_v6 Kernel master6 up 2022-04-14
1006- Channel ipv6
State: UP
Table: master6
Preference: 10
Input filter: REJECT
Output filter: save_to_kernel
Routes: 0 imported, 2 exported, 0 preferred
Route change stats: received rejected filtered ignored accepted
Import updates: 0 0 0 0 0
Import withdraws: 0 0 --- 0 0
Export updates: 12 0 3 --- 9
Export withdraws: 9 --- --- --- 7
1002-bfd1 BFD --- up 2022-04-14
1006-
1002-bgp_r1_v4 BGP --- up 2022-04-14 Established
1006- Description: IPv4 BGP with internal router
BGP state: Established
Neighbor address: 172.29.0.1
Neighbor AS: 64561
Local AS: 64560
Neighbor ID: 172.29.0.1
Local capabilities
Multiprotocol
AF announced: ipv4
Route refresh
Graceful restart
4-octet AS numbers
Enhanced refresh
Long-lived graceful restart
Neighbor capabilities
Multiprotocol
AF announced: ipv4
Route refresh
Graceful restart
4-octet AS numbers
Enhanced refresh
Long-lived graceful restart
Session: external AS4
Source address: 172.29.0.12
Hold timer: 207.832/240
Keepalive timer: 48.076/80
Channel ipv4
State: UP
Table: master4
Preference: 100
Input filter: ACCEPT
Output filter: REJECT
Routes: 1 imported, 0 exported, 1 preferred
Route change stats: received rejected filtered ignored accepted
Import updates: 3 0 0 0 3
Import withdraws: 2 0 --- 0 2
Export updates: 13 3 10 --- 0
Export withdraws: 4 --- --- --- 0
BGP Next hop: 172.29.0.1
1002-bgp_r1_v6 BGP --- up 2022-04-14 Established
1006- Description: IPv6 BGP with internal router
BGP state: Established
Neighbor address: fe80:172:29::1%eth0
Neighbor AS: 64561
Local AS: 64560
Neighbor ID: 172.29.0.1
Local capabilities
Multiprotocol
AF announced: ipv6
Route refresh
Graceful restart
4-octet AS numbers
Enhanced refresh
Long-lived graceful restart
Neighbor capabilities
Multiprotocol
AF announced: ipv6
Route refresh
Graceful restart
4-octet AS numbers
Enhanced refresh
Long-lived graceful restart
Session: external AS4
Source address: fe80:172:29::12
Hold timer: 138.941/240
Keepalive timer: 71.918/80
Channel ipv6
State: UP
Table: master6
Preference: 100
Input filter: ACCEPT
Output filter: REJECT
Routes: 1 imported, 0 exported, 1 preferred
Route change stats: received rejected filtered ignored accepted
Import updates: 7 1 2 3 1
Import withdraws: 2 0 --- 0 2
Export updates: 12 1 1 --- 10
Export withdraws: 4 --- --- --- 4
BGP Next hop: :: fe80:172:29::1%eth0
0000
",
)
}