Commit 5de7796d authored by Jakub Kicinski's avatar Jakub Kicinski

Merge branch 'mptcp-more-selftest-coverage-and-code-cleanup-for-net-next'

Mat Martineau says:

====================
mptcp: More selftest coverage and code cleanup for net-next

Patches 1-5 and 7-8 add selftest coverage (and an associated subflow
counter in the kernel) to validate the recently-updated handling of
subflows with ID 0.

Patch 6 renames a label in the userspace path manager for clarity.

Patches 9-11 and 13-15 factor out common selftest code by moving certain
functions to mptcp_lib.sh

Patch 12 makes sure the random data file generated for selftest
payloads has the intended size.

v3: https://lore.kernel.org/r/20231115-send-net-next-2023107-v3-0-1ef58145a882@kernel.org
v2: https://lore.kernel.org/r/20231114-send-net-next-2023107-v2-0-b650a477362c@kernel.org
v1: https://lore.kernel.org/r/20231027-send-net-next-2023107-v1-0-03eff9452957@kernel.org
====================

Link: https://lore.kernel.org/r/20231128-send-net-next-2023107-v4-0-8d6b94150f6b@kernel.orgSigned-off-by: default avatarJakub Kicinski <kuba@kernel.org>
parents 3d6d7549 9369777c
...@@ -57,6 +57,7 @@ struct mptcp_info { ...@@ -57,6 +57,7 @@ struct mptcp_info {
__u64 mptcpi_bytes_sent; __u64 mptcpi_bytes_sent;
__u64 mptcpi_bytes_received; __u64 mptcpi_bytes_received;
__u64 mptcpi_bytes_acked; __u64 mptcpi_bytes_acked;
__u8 mptcpi_subflows_total;
}; };
/* MPTCP Reset reason codes, rfc8684 */ /* MPTCP Reset reason codes, rfc8684 */
......
...@@ -276,12 +276,12 @@ int mptcp_pm_nl_remove_doit(struct sk_buff *skb, struct genl_info *info) ...@@ -276,12 +276,12 @@ int mptcp_pm_nl_remove_doit(struct sk_buff *skb, struct genl_info *info)
if (!mptcp_pm_is_userspace(msk)) { if (!mptcp_pm_is_userspace(msk)) {
GENL_SET_ERR_MSG(info, "invalid request; userspace PM not selected"); GENL_SET_ERR_MSG(info, "invalid request; userspace PM not selected");
goto remove_err; goto out;
} }
if (id_val == 0) { if (id_val == 0) {
err = mptcp_userspace_pm_remove_id_zero_address(msk, info); err = mptcp_userspace_pm_remove_id_zero_address(msk, info);
goto remove_err; goto out;
} }
lock_sock(sk); lock_sock(sk);
...@@ -296,7 +296,7 @@ int mptcp_pm_nl_remove_doit(struct sk_buff *skb, struct genl_info *info) ...@@ -296,7 +296,7 @@ int mptcp_pm_nl_remove_doit(struct sk_buff *skb, struct genl_info *info)
if (!match) { if (!match) {
GENL_SET_ERR_MSG(info, "address with specified id not found"); GENL_SET_ERR_MSG(info, "address with specified id not found");
release_sock(sk); release_sock(sk);
goto remove_err; goto out;
} }
list_move(&match->list, &free_list); list_move(&match->list, &free_list);
...@@ -310,7 +310,7 @@ int mptcp_pm_nl_remove_doit(struct sk_buff *skb, struct genl_info *info) ...@@ -310,7 +310,7 @@ int mptcp_pm_nl_remove_doit(struct sk_buff *skb, struct genl_info *info)
} }
err = 0; err = 0;
remove_err: out:
sock_put(sk); sock_put(sk);
return err; return err;
} }
......
...@@ -1072,6 +1072,15 @@ static inline void __mptcp_do_fallback(struct mptcp_sock *msk) ...@@ -1072,6 +1072,15 @@ static inline void __mptcp_do_fallback(struct mptcp_sock *msk)
set_bit(MPTCP_FALLBACK_DONE, &msk->flags); set_bit(MPTCP_FALLBACK_DONE, &msk->flags);
} }
static inline bool __mptcp_has_initial_subflow(const struct mptcp_sock *msk)
{
struct sock *ssk = READ_ONCE(msk->first);
return ssk && ((1 << inet_sk_state_load(ssk)) &
(TCPF_ESTABLISHED | TCPF_SYN_SENT |
TCPF_SYN_RECV | TCPF_LISTEN));
}
static inline void mptcp_do_fallback(struct sock *ssk) static inline void mptcp_do_fallback(struct sock *ssk)
{ {
struct mptcp_subflow_context *subflow = mptcp_subflow_ctx(ssk); struct mptcp_subflow_context *subflow = mptcp_subflow_ctx(ssk);
......
...@@ -938,6 +938,8 @@ void mptcp_diag_fill_info(struct mptcp_sock *msk, struct mptcp_info *info) ...@@ -938,6 +938,8 @@ void mptcp_diag_fill_info(struct mptcp_sock *msk, struct mptcp_info *info)
info->mptcpi_bytes_sent = msk->bytes_sent; info->mptcpi_bytes_sent = msk->bytes_sent;
info->mptcpi_bytes_received = msk->bytes_received; info->mptcpi_bytes_received = msk->bytes_received;
info->mptcpi_bytes_retrans = msk->bytes_retrans; info->mptcpi_bytes_retrans = msk->bytes_retrans;
info->mptcpi_subflows_total = info->mptcpi_subflows +
__mptcp_has_initial_subflow(msk);
unlock_sock_fast(sk, slow); unlock_sock_fast(sk, slow);
} }
EXPORT_SYMBOL_GPL(mptcp_diag_fill_info); EXPORT_SYMBOL_GPL(mptcp_diag_fill_info);
......
...@@ -182,23 +182,6 @@ chk_msk_inuse() ...@@ -182,23 +182,6 @@ chk_msk_inuse()
__chk_nr get_msk_inuse $expected "$msg" 0 __chk_nr get_msk_inuse $expected "$msg" 0
} }
# $1: ns, $2: port
wait_local_port_listen()
{
local listener_ns="${1}"
local port="${2}"
local port_hex i
port_hex="$(printf "%04X" "${port}")"
for i in $(seq 10); do
ip netns exec "${listener_ns}" cat /proc/net/tcp | \
awk "BEGIN {rc=1} {if (\$2 ~ /:${port_hex}\$/ && \$4 ~ /0A/) {rc=0; exit}} END {exit rc}" &&
break
sleep 0.1
done
}
wait_connected() wait_connected()
{ {
local listener_ns="${1}" local listener_ns="${1}"
...@@ -222,7 +205,7 @@ echo "a" | \ ...@@ -222,7 +205,7 @@ echo "a" | \
ip netns exec $ns \ ip netns exec $ns \
./mptcp_connect -p 10000 -l -t ${timeout_poll} -w 20 \ ./mptcp_connect -p 10000 -l -t ${timeout_poll} -w 20 \
0.0.0.0 >/dev/null & 0.0.0.0 >/dev/null &
wait_local_port_listen $ns 10000 mptcp_lib_wait_local_port_listen $ns 10000
chk_msk_nr 0 "no msk on netns creation" chk_msk_nr 0 "no msk on netns creation"
chk_msk_listen 10000 chk_msk_listen 10000
...@@ -245,7 +228,7 @@ echo "a" | \ ...@@ -245,7 +228,7 @@ echo "a" | \
ip netns exec $ns \ ip netns exec $ns \
./mptcp_connect -p 10001 -l -s TCP -t ${timeout_poll} -w 20 \ ./mptcp_connect -p 10001 -l -s TCP -t ${timeout_poll} -w 20 \
0.0.0.0 >/dev/null & 0.0.0.0 >/dev/null &
wait_local_port_listen $ns 10001 mptcp_lib_wait_local_port_listen $ns 10001
echo "b" | \ echo "b" | \
timeout ${timeout_test} \ timeout ${timeout_test} \
ip netns exec $ns \ ip netns exec $ns \
...@@ -266,7 +249,7 @@ for I in `seq 1 $NR_CLIENTS`; do ...@@ -266,7 +249,7 @@ for I in `seq 1 $NR_CLIENTS`; do
./mptcp_connect -p $((I+10001)) -l -w 20 \ ./mptcp_connect -p $((I+10001)) -l -w 20 \
-t ${timeout_poll} 0.0.0.0 >/dev/null & -t ${timeout_poll} 0.0.0.0 >/dev/null &
done done
wait_local_port_listen $ns $((NR_CLIENTS + 10001)) mptcp_lib_wait_local_port_listen $ns $((NR_CLIENTS + 10001))
for I in `seq 1 $NR_CLIENTS`; do for I in `seq 1 $NR_CLIENTS`; do
echo "b" | \ echo "b" | \
......
...@@ -254,31 +254,6 @@ else ...@@ -254,31 +254,6 @@ else
set_ethtool_flags "$ns4" ns4eth3 "$ethtool_args" set_ethtool_flags "$ns4" ns4eth3 "$ethtool_args"
fi fi
print_file_err()
{
ls -l "$1" 1>&2
echo "Trailing bytes are: "
tail -c 27 "$1"
}
check_transfer()
{
local in=$1
local out=$2
local what=$3
cmp "$in" "$out" > /dev/null 2>&1
if [ $? -ne 0 ] ;then
echo "[ FAIL ] $what does not match (in, out):"
print_file_err "$in"
print_file_err "$out"
return 1
fi
return 0
}
check_mptcp_disabled() check_mptcp_disabled()
{ {
local disabled_ns="ns_disabled-$rndh" local disabled_ns="ns_disabled-$rndh"
...@@ -310,12 +285,6 @@ check_mptcp_disabled() ...@@ -310,12 +285,6 @@ check_mptcp_disabled()
return 0 return 0
} }
# $1: IP address
is_v6()
{
[ -z "${1##*:*}" ]
}
do_ping() do_ping()
{ {
local listener_ns="$1" local listener_ns="$1"
...@@ -324,7 +293,7 @@ do_ping() ...@@ -324,7 +293,7 @@ do_ping()
local ping_args="-q -c 1" local ping_args="-q -c 1"
local rc=0 local rc=0
if is_v6 "${connect_addr}"; then if mptcp_lib_is_v6 "${connect_addr}"; then
$ipv6 || return 0 $ipv6 || return 0
ping_args="${ping_args} -6" ping_args="${ping_args} -6"
fi fi
...@@ -341,38 +310,6 @@ do_ping() ...@@ -341,38 +310,6 @@ do_ping()
return 0 return 0
} }
# $1: ns, $2: MIB counter
get_mib_counter()
{
local listener_ns="${1}"
local mib="${2}"
# strip the header
ip netns exec "${listener_ns}" \
nstat -z -a "${mib}" | \
tail -n+2 | \
while read a count c rest; do
echo $count
done
}
# $1: ns, $2: port
wait_local_port_listen()
{
local listener_ns="${1}"
local port="${2}"
local port_hex i
port_hex="$(printf "%04X" "${port}")"
for i in $(seq 10); do
ip netns exec "${listener_ns}" cat /proc/net/tcp* | \
awk "BEGIN {rc=1} {if (\$2 ~ /:${port_hex}\$/ && \$4 ~ /0A/) {rc=0; exit}} END {exit rc}" &&
break
sleep 0.1
done
}
do_transfer() do_transfer()
{ {
local listener_ns="$1" local listener_ns="$1"
...@@ -441,12 +378,12 @@ do_transfer() ...@@ -441,12 +378,12 @@ do_transfer()
nstat -n nstat -n
fi fi
local stat_synrx_last_l=$(get_mib_counter "${listener_ns}" "MPTcpExtMPCapableSYNRX") local stat_synrx_last_l=$(mptcp_lib_get_counter "${listener_ns}" "MPTcpExtMPCapableSYNRX")
local stat_ackrx_last_l=$(get_mib_counter "${listener_ns}" "MPTcpExtMPCapableACKRX") local stat_ackrx_last_l=$(mptcp_lib_get_counter "${listener_ns}" "MPTcpExtMPCapableACKRX")
local stat_cookietx_last=$(get_mib_counter "${listener_ns}" "TcpExtSyncookiesSent") local stat_cookietx_last=$(mptcp_lib_get_counter "${listener_ns}" "TcpExtSyncookiesSent")
local stat_cookierx_last=$(get_mib_counter "${listener_ns}" "TcpExtSyncookiesRecv") local stat_cookierx_last=$(mptcp_lib_get_counter "${listener_ns}" "TcpExtSyncookiesRecv")
local stat_csum_err_s=$(get_mib_counter "${listener_ns}" "MPTcpExtDataCsumErr") local stat_csum_err_s=$(mptcp_lib_get_counter "${listener_ns}" "MPTcpExtDataCsumErr")
local stat_csum_err_c=$(get_mib_counter "${connector_ns}" "MPTcpExtDataCsumErr") local stat_csum_err_c=$(mptcp_lib_get_counter "${connector_ns}" "MPTcpExtDataCsumErr")
timeout ${timeout_test} \ timeout ${timeout_test} \
ip netns exec ${listener_ns} \ ip netns exec ${listener_ns} \
...@@ -454,7 +391,7 @@ do_transfer() ...@@ -454,7 +391,7 @@ do_transfer()
$extra_args $local_addr < "$sin" > "$sout" & $extra_args $local_addr < "$sin" > "$sout" &
local spid=$! local spid=$!
wait_local_port_listen "${listener_ns}" "${port}" mptcp_lib_wait_local_port_listen "${listener_ns}" "${port}"
local start local start
start=$(date +%s%3N) start=$(date +%s%3N)
...@@ -504,16 +441,16 @@ do_transfer() ...@@ -504,16 +441,16 @@ do_transfer()
return 1 return 1
fi fi
check_transfer $sin $cout "file received by client" mptcp_lib_check_transfer $sin $cout "file received by client"
retc=$? retc=$?
check_transfer $cin $sout "file received by server" mptcp_lib_check_transfer $cin $sout "file received by server"
rets=$? rets=$?
local stat_synrx_now_l=$(get_mib_counter "${listener_ns}" "MPTcpExtMPCapableSYNRX") local stat_synrx_now_l=$(mptcp_lib_get_counter "${listener_ns}" "MPTcpExtMPCapableSYNRX")
local stat_ackrx_now_l=$(get_mib_counter "${listener_ns}" "MPTcpExtMPCapableACKRX") local stat_ackrx_now_l=$(mptcp_lib_get_counter "${listener_ns}" "MPTcpExtMPCapableACKRX")
local stat_cookietx_now=$(get_mib_counter "${listener_ns}" "TcpExtSyncookiesSent") local stat_cookietx_now=$(mptcp_lib_get_counter "${listener_ns}" "TcpExtSyncookiesSent")
local stat_cookierx_now=$(get_mib_counter "${listener_ns}" "TcpExtSyncookiesRecv") local stat_cookierx_now=$(mptcp_lib_get_counter "${listener_ns}" "TcpExtSyncookiesRecv")
local stat_ooo_now=$(get_mib_counter "${listener_ns}" "TcpExtTCPOFOQueue") local stat_ooo_now=$(mptcp_lib_get_counter "${listener_ns}" "TcpExtTCPOFOQueue")
expect_synrx=$((stat_synrx_last_l)) expect_synrx=$((stat_synrx_last_l))
expect_ackrx=$((stat_ackrx_last_l)) expect_ackrx=$((stat_ackrx_last_l))
...@@ -542,8 +479,8 @@ do_transfer() ...@@ -542,8 +479,8 @@ do_transfer()
fi fi
if $checksum; then if $checksum; then
local csum_err_s=$(get_mib_counter "${listener_ns}" "MPTcpExtDataCsumErr") local csum_err_s=$(mptcp_lib_get_counter "${listener_ns}" "MPTcpExtDataCsumErr")
local csum_err_c=$(get_mib_counter "${connector_ns}" "MPTcpExtDataCsumErr") local csum_err_c=$(mptcp_lib_get_counter "${connector_ns}" "MPTcpExtDataCsumErr")
local csum_err_s_nr=$((csum_err_s - stat_csum_err_s)) local csum_err_s_nr=$((csum_err_s - stat_csum_err_s))
if [ $csum_err_s_nr -gt 0 ]; then if [ $csum_err_s_nr -gt 0 ]; then
...@@ -613,9 +550,8 @@ make_file() ...@@ -613,9 +550,8 @@ make_file()
ksize=$((SIZE / 1024)) ksize=$((SIZE / 1024))
rem=$((SIZE - (ksize * 1024))) rem=$((SIZE - (ksize * 1024)))
dd if=/dev/urandom of="$name" bs=1024 count=$ksize 2> /dev/null mptcp_lib_make_file $name 1024 $ksize
dd if=/dev/urandom conv=notrunc of="$name" bs=1 count=$rem 2> /dev/null dd if=/dev/urandom conv=notrunc of="$name" oflag=append bs=1 count=$rem 2> /dev/null
echo -e "\nMPTCP_TEST_FILE_END_MARKER" >> "$name"
echo "Created $name (size $(du -b "$name")) containing data sent by $who" echo "Created $name (size $(du -b "$name")) containing data sent by $who"
} }
...@@ -635,12 +571,12 @@ run_tests_lo() ...@@ -635,12 +571,12 @@ run_tests_lo()
fi fi
# skip if we don't want v6 # skip if we don't want v6
if ! $ipv6 && is_v6 "${connect_addr}"; then if ! $ipv6 && mptcp_lib_is_v6 "${connect_addr}"; then
return 0 return 0
fi fi
local local_addr local local_addr
if is_v6 "${connect_addr}"; then if mptcp_lib_is_v6 "${connect_addr}"; then
local_addr="::" local_addr="::"
else else
local_addr="0.0.0.0" local_addr="0.0.0.0"
...@@ -708,7 +644,7 @@ run_test_transparent() ...@@ -708,7 +644,7 @@ run_test_transparent()
TEST_GROUP="${msg}" TEST_GROUP="${msg}"
# skip if we don't want v6 # skip if we don't want v6
if ! $ipv6 && is_v6 "${connect_addr}"; then if ! $ipv6 && mptcp_lib_is_v6 "${connect_addr}"; then
return 0 return 0
fi fi
...@@ -741,7 +677,7 @@ EOF ...@@ -741,7 +677,7 @@ EOF
fi fi
local local_addr local local_addr
if is_v6 "${connect_addr}"; then if mptcp_lib_is_v6 "${connect_addr}"; then
local_addr="::" local_addr="::"
r6flag="-6" r6flag="-6"
else else
......
...@@ -207,3 +207,94 @@ mptcp_lib_result_print_all_tap() { ...@@ -207,3 +207,94 @@ mptcp_lib_result_print_all_tap() {
printf "%s\n" "${subtest}" printf "%s\n" "${subtest}"
done done
} }
# get the value of keyword $1 in the line marked by keyword $2
mptcp_lib_get_info_value() {
grep "${2}" | sed -n 's/.*\('"${1}"':\)\([0-9a-f:.]*\).*$/\2/p;q'
}
# $1: info name ; $2: evts_ns ; $3: event type
mptcp_lib_evts_get_info() {
mptcp_lib_get_info_value "${1}" "^type:${3:-1}," < "${2}"
}
# $1: PID
mptcp_lib_kill_wait() {
[ "${1}" -eq 0 ] && return 0
kill -SIGUSR1 "${1}" > /dev/null 2>&1
kill "${1}" > /dev/null 2>&1
wait "${1}" 2>/dev/null
}
# $1: IP address
mptcp_lib_is_v6() {
[ -z "${1##*:*}" ]
}
# $1: ns, $2: MIB counter
mptcp_lib_get_counter() {
local ns="${1}"
local counter="${2}"
local count
count=$(ip netns exec "${ns}" nstat -asz "${counter}" |
awk 'NR==1 {next} {print $2}')
if [ -z "${count}" ]; then
mptcp_lib_fail_if_expected_feature "${counter} counter"
return 1
fi
echo "${count}"
}
mptcp_lib_make_file() {
local name="${1}"
local bs="${2}"
local size="${3}"
dd if=/dev/urandom of="${name}" bs="${bs}" count="${size}" 2> /dev/null
echo -e "\nMPTCP_TEST_FILE_END_MARKER" >> "${name}"
}
# $1: file
mptcp_lib_print_file_err() {
ls -l "${1}" 1>&2
echo "Trailing bytes are: "
tail -c 27 "${1}"
}
# $1: input file ; $2: output file ; $3: what kind of file
mptcp_lib_check_transfer() {
local in="${1}"
local out="${2}"
local what="${3}"
if ! cmp "$in" "$out" > /dev/null 2>&1; then
echo "[ FAIL ] $what does not match (in, out):"
mptcp_lib_print_file_err "$in"
mptcp_lib_print_file_err "$out"
return 1
fi
return 0
}
# $1: ns, $2: port
mptcp_lib_wait_local_port_listen() {
local listener_ns="${1}"
local port="${2}"
local port_hex
port_hex="$(printf "%04X" "${port}")"
local _
for _ in $(seq 10); do
ip netns exec "${listener_ns}" cat /proc/net/tcp* | \
awk "BEGIN {rc=1} {if (\$2 ~ /:${port_hex}\$/ && \$4 ~ /0A/) \
{rc=0; exit}} END {exit rc}" &&
break
sleep 0.1
done
}
...@@ -135,38 +135,6 @@ check_mark() ...@@ -135,38 +135,6 @@ check_mark()
return 0 return 0
} }
print_file_err()
{
ls -l "$1" 1>&2
echo "Trailing bytes are: "
tail -c 27 "$1"
}
check_transfer()
{
local in=$1
local out=$2
local what=$3
cmp "$in" "$out" > /dev/null 2>&1
if [ $? -ne 0 ] ;then
echo "[ FAIL ] $what does not match (in, out):"
print_file_err "$in"
print_file_err "$out"
ret=1
return 1
fi
return 0
}
# $1: IP address
is_v6()
{
[ -z "${1##*:*}" ]
}
do_transfer() do_transfer()
{ {
local listener_ns="$1" local listener_ns="$1"
...@@ -183,7 +151,7 @@ do_transfer() ...@@ -183,7 +151,7 @@ do_transfer()
local mptcp_connect="./mptcp_connect -r 20" local mptcp_connect="./mptcp_connect -r 20"
local local_addr ip local local_addr ip
if is_v6 "${connect_addr}"; then if mptcp_lib_is_v6 "${connect_addr}"; then
local_addr="::" local_addr="::"
ip=ipv6 ip=ipv6
else else
...@@ -238,7 +206,7 @@ do_transfer() ...@@ -238,7 +206,7 @@ do_transfer()
check_mark $connector_ns 4 || retc=1 check_mark $connector_ns 4 || retc=1
fi fi
check_transfer $cin $sout "file received by server" mptcp_lib_check_transfer $cin $sout "file received by server"
rets=$? rets=$?
mptcp_lib_result_code "${retc}" "mark ${ip}" mptcp_lib_result_code "${retc}" "mark ${ip}"
...@@ -257,8 +225,7 @@ make_file() ...@@ -257,8 +225,7 @@ make_file()
local who=$2 local who=$2
local size=$3 local size=$3
dd if=/dev/urandom of="$name" bs=1024 count=$size 2> /dev/null mptcp_lib_make_file $name 1024 $size
echo -e "\nMPTCP_TEST_FILE_END_MARKER" >> "$name"
echo "Created $name (size $size KB) containing data sent by $who" echo "Created $name (size $size KB) containing data sent by $who"
} }
......
...@@ -123,23 +123,6 @@ setup() ...@@ -123,23 +123,6 @@ setup()
grep -q ' kmemleak_init$\| lockdep_init$\| kasan_init$\| prove_locking$' /proc/kallsyms && slack=$((slack+550)) grep -q ' kmemleak_init$\| lockdep_init$\| kasan_init$\| prove_locking$' /proc/kallsyms && slack=$((slack+550))
} }
# $1: ns, $2: port
wait_local_port_listen()
{
local listener_ns="${1}"
local port="${2}"
local port_hex i
port_hex="$(printf "%04X" "${port}")"
for i in $(seq 10); do
ip netns exec "${listener_ns}" cat /proc/net/tcp* | \
awk "BEGIN {rc=1} {if (\$2 ~ /:${port_hex}\$/ && \$4 ~ /0A/) {rc=0; exit}} END {exit rc}" &&
break
sleep 0.1
done
}
do_transfer() do_transfer()
{ {
local cin=$1 local cin=$1
...@@ -179,7 +162,7 @@ do_transfer() ...@@ -179,7 +162,7 @@ do_transfer()
0.0.0.0 < "$sin" > "$sout" & 0.0.0.0 < "$sin" > "$sout" &
local spid=$! local spid=$!
wait_local_port_listen "${ns3}" "${port}" mptcp_lib_wait_local_port_listen "${ns3}" "${port}"
timeout ${timeout_test} \ timeout ${timeout_test} \
ip netns exec ${ns1} \ ip netns exec ${ns1} \
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment