From b4301a8106f5392a3a3d565e777f1f1d7c6828ce Mon Sep 17 00:00:00 2001 From: lbl <1791778603@qq.com> Date: Mon, 9 Feb 2026 23:02:31 +0800 Subject: [PATCH] v2 --- .cargo/config.toml | 22 + .github/workflows/rust.yml | 250 +- .gitignore | 11 +- Cargo.lock | 3844 +++++++++++++++++ Cargo.toml | 48 +- README.md | 236 +- common/Cargo.toml | 46 - common/build.rs | 16 - common/src/args_parse.rs | 82 - common/src/callback.rs | 52 - common/src/cli.rs | 620 --- common/src/command/client.rs | 92 - common/src/command/entity.rs | 68 - common/src/command/mod.rs | 353 -- common/src/command/server.rs | 89 - common/src/config/file_config.rs | 190 - common/src/config/mod.rs | 38 - common/src/console_out/mod.rs | 366 -- common/src/console_out/table.rs | 23 - common/src/identifier.rs | 78 - common/src/lib.rs | 12 - dll/amd64/wintun.dll | Bin 0 -> 427552 bytes dll/arm/wintun.dll | Bin 0 -> 364552 bytes dll/arm64/wintun.dll | Bin 0 -> 222488 bytes dll/x86/wintun.dll | Bin 0 -> 550928 bytes documents/img/ssh.jpg | Bin 63721 -> 0 bytes src/args_config.rs | 468 ++ src/extract_wintun_dll.rs | 33 + src/lib.rs | 1 + src/log.rs | 61 + src/main_cli.rs | 142 + src/main_ctrl.rs | 42 + src/main_web.rs | 36 + vn-link-cli/Cargo.toml | 37 - vn-link-cli/README.md | 61 - vn-link-cli/build.rs | 5 - vn-link-cli/src/main.rs | 89 - vn-link/Cargo.toml | 32 - vn-link/src/config.rs | 85 - vn-link/src/in_mapping/mod.rs | 2 - vn-link/src/in_mapping/tcp.rs | 46 - vn-link/src/in_mapping/udp.rs | 63 - vn-link/src/lib.rs | 8 - vn-link/src/out_mapping/mod.rs | 2 - vn-link/src/out_mapping/tcp.rs | 64 - vn-link/src/out_mapping/udp.rs | 137 - vn-link/src/vnt_link/mod.rs | 217 - vnt-cli/Cargo.toml | 51 - vnt-cli/README.md | 256 -- vnt-cli/build.rs | 5 - vnt-cli/src/main.rs | 104 - vnt-cli/src/root_check/mod.rs | 11 - vnt-cli/src/root_check/unix.rs | 3 - vnt-cli/src/root_check/windows.rs | 76 - vnt-core/Cargo.toml | 58 + vnt-core/build.rs | 15 + vnt-core/proto/client.proto | 62 + vnt-core/proto/control_message.proto | 72 + vnt-core/proto/fec.proto | 18 + vnt-core/proto/rpc.proto | 37 + vnt-core/src/api/mod.rs | 94 + vnt-core/src/compression/lz4_compression.rs | 132 + vnt-core/src/compression/mod.rs | 45 + vnt-core/src/context/config.rs | 101 + vnt-core/src/context/mod.rs | 632 +++ vnt-core/src/context/nat.rs | 147 + vnt-core/src/core/mod.rs | 353 ++ vnt-core/src/crypto/chacha20_poly1305.rs | 169 + vnt-core/src/crypto/mod.rs | 47 + vnt-core/src/enhanced_tunnel/inbound.rs | 72 + vnt-core/src/enhanced_tunnel/mod.rs | 75 + vnt-core/src/enhanced_tunnel/outbound.rs | 68 + .../src/enhanced_tunnel/quic_over/boot.rs | 200 + .../quic_over/enhanced_io/enhanced_inbound.rs | 93 + .../enhanced_io/enhanced_outbound.rs | 89 + .../quic_over/enhanced_io/mod.rs | 3 + .../quic_over/enhanced_io/socket.rs | 55 + vnt-core/src/enhanced_tunnel/quic_over/mod.rs | 7 + .../enhanced_tunnel/quic_over/quic_client.rs | 309 ++ .../enhanced_tunnel/quic_over/quic_inbound.rs | 18 + .../quic_over/quic_outbound.rs | 94 + .../enhanced_tunnel/quic_over/quic_server.rs | 245 ++ vnt-core/src/fec/decoder.rs | 302 ++ vnt-core/src/fec/encoder.rs | 247 ++ vnt-core/src/fec/mod.rs | 5 + vnt-core/src/lib.rs | 15 + vnt-core/src/nat/internal_nat/icmp_nat.rs | 139 + vnt-core/src/nat/internal_nat/mod.rs | 225 + vnt-core/src/nat/internal_nat/tcp_nat.rs | 81 + vnt-core/src/nat/internal_nat/udp_nat.rs | 188 + vnt-core/src/nat/mod.rs | 114 + vnt-core/src/port_mapping/mod.rs | 100 + vnt-core/src/port_mapping/tcp_port_mapping.rs | 85 + vnt-core/src/port_mapping/udp_port_mapping.rs | 145 + vnt-core/src/protocol/client_message.rs | 98 + vnt-core/src/protocol/control_message.rs | 275 ++ vnt-core/src/protocol/ip_packet_protocol.rs | 302 ++ vnt-core/src/protocol/mod.rs | 21 + vnt-core/src/protocol/rpc_message.rs | 4 + vnt-core/src/protocol/transmission.rs | 266 ++ vnt-core/src/tls/cert.rs | 114 + vnt-core/src/tls/mod.rs | 2 + vnt-core/src/tls/verifier.rs | 228 + vnt-core/src/tun/enhanced_tun.rs | 18 + vnt-core/src/tun/general.rs | 236 + vnt-core/src/tun/mod.rs | 6 + vnt-core/src/tun/sender.rs | 40 + vnt-core/src/tunnel_core/mod.rs | 4 + vnt-core/src/tunnel_core/outbound.rs | 286 ++ vnt-core/src/tunnel_core/p2p/inbound.rs | 268 ++ vnt-core/src/tunnel_core/p2p/mod.rs | 5 + vnt-core/src/tunnel_core/p2p/outbound.rs | 133 + vnt-core/src/tunnel_core/p2p/route_table.rs | 265 ++ vnt-core/src/tunnel_core/p2p/transport/mod.rs | 4 + .../src/tunnel_core/p2p/transport/nat_test.rs | 387 ++ .../src/tunnel_core/p2p/transport/punch.rs | 148 + .../src/tunnel_core/p2p/transport/task.rs | 337 ++ .../tunnel_core/server/connection_manager.rs | 346 ++ vnt-core/src/tunnel_core/server/inbound.rs | 332 ++ vnt-core/src/tunnel_core/server/mod.rs | 5 + vnt-core/src/tunnel_core/server/outbound.rs | 329 ++ vnt-core/src/tunnel_core/server/rpc.rs | 150 + .../tunnel_core/server/transport/config.rs | 173 + .../src/tunnel_core/server/transport/mod.rs | 105 + .../src/tunnel_core/server/transport/quic.rs | 90 + .../src/tunnel_core/server/transport/tcp.rs | 79 + .../src/tunnel_core/server/transport/wss.rs | 96 + vnt-core/src/utils/device_id.rs | 29 + vnt-core/src/utils/dns_query.rs | 251 ++ vnt-core/src/utils/mod.rs | 11 + vnt-core/src/utils/task_control.rs | 255 ++ vnt-ipc/Cargo.toml | 20 + vnt-ipc/build.rs | 7 + vnt-ipc/proto/local_ipc.proto | 103 + vnt-ipc/src/client.rs | 285 ++ vnt-ipc/src/lib.rs | 12 + vnt-ipc/src/message.rs | 4 + vnt-ipc/src/server.rs | 194 + vnt-web/Cargo.toml | 23 + vnt-web/src/lib.rs | 17 + vnt-web/src/service_http.rs | 1105 +++++ vnt-web/static/index.html | 2884 +++++++++++++ vnt-web/static/tailwindcss3.4.17.js.gz | Bin 0 -> 123080 bytes vnt-web/static/vue-router.global.prod.js.gz | Bin 0 -> 9597 bytes vnt-web/static/vue.global.prod.js.gz | Bin 0 -> 58885 bytes vnt/Cargo.toml | 89 - vnt/LICENSE | 201 - vnt/build.rs | 27 - vnt/packet/Cargo.toml | 9 - vnt/packet/src/arp/arp.rs | 122 - vnt/packet/src/arp/mod.rs | 1 - vnt/packet/src/ethernet/mod.rs | 2 - vnt/packet/src/ethernet/packet.rs | 77 - vnt/packet/src/ethernet/protocol.rs | 141 - vnt/packet/src/icmp/icmp.rs | 167 - vnt/packet/src/icmp/mod.rs | 414 -- vnt/packet/src/igmp/igmp_v1.rs | 117 - vnt/packet/src/igmp/igmp_v2.rs | 118 - vnt/packet/src/igmp/igmp_v3.rs | 493 --- vnt/packet/src/igmp/mod.rs | 46 - vnt/packet/src/ip/ipv4/mod.rs | 2 - vnt/packet/src/ip/ipv4/packet.rs | 235 - vnt/packet/src/ip/ipv4/protocol.rs | 742 ---- vnt/packet/src/ip/mod.rs | 17 - vnt/packet/src/lib.rs | 143 - vnt/packet/src/tcp/mod.rs | 42 - vnt/packet/src/tcp/tcp.rs | 178 - vnt/packet/src/udp/mod.rs | 1 - vnt/packet/src/udp/udp.rs | 150 - vnt/proto/message.proto | 94 - vnt/src/channel/context.rs | 559 --- vnt/src/channel/handler.rs | 12 - vnt/src/channel/idle.rs | 45 - vnt/src/channel/mod.rs | 365 -- vnt/src/channel/notify.rs | 126 - vnt/src/channel/punch.rs | 508 --- vnt/src/channel/sender.rs | 272 -- vnt/src/channel/socket/mod.rs | 138 - vnt/src/channel/socket/unix.rs | 46 - vnt/src/channel/socket/windows.rs | 56 - vnt/src/channel/tcp_channel.rs | 212 - vnt/src/channel/udp_channel.rs | 370 -- vnt/src/channel/ws_channel.rs | 173 - vnt/src/cipher/aes_cbc/mod.rs | 2 - vnt/src/cipher/aes_cbc/rs_aes_cbc.rs | 153 - vnt/src/cipher/aes_ecb/mod.rs | 9 - vnt/src/cipher/aes_ecb/openssl_aes_ecb.rs | 219 - vnt/src/cipher/aes_ecb/rs_aes_ecb.rs | 154 - vnt/src/cipher/aes_gcm/aes_gcm_cipher.rs | 132 - vnt/src/cipher/aes_gcm/mod.rs | 9 - vnt/src/cipher/aes_gcm/ring_aes_gcm_cipher.rs | 148 - vnt/src/cipher/chacha20/mod.rs | 2 - vnt/src/cipher/chacha20/rs_chacha20.rs | 107 - vnt/src/cipher/chacha20_poly1305/mod.rs | 9 - .../ring_chacha20_poly1305.rs | 127 - .../chacha20_poly1305/rs_chacha20_poly1305.rs | 126 - vnt/src/cipher/cipher.rs | 335 -- vnt/src/cipher/finger.rs | 76 - vnt/src/cipher/mod.rs | 32 - vnt/src/cipher/rsa_cipher.rs | 112 - vnt/src/cipher/sm4_cbc/mod.rs | 2 - vnt/src/cipher/sm4_cbc/rs_sm4_cbc.rs | 153 - vnt/src/cipher/xor/mod.rs | 2 - vnt/src/cipher/xor/xor.rs | 84 - vnt/src/compression/lz4_compress.rs | 33 - vnt/src/compression/mod.rs | 220 - vnt/src/compression/zstd_compress.rs | 38 - vnt/src/core/conn.rs | 538 --- vnt/src/core/mod.rs | 247 -- vnt/src/external_route/mod.rs | 66 - vnt/src/handle/callback.rs | 322 -- vnt/src/handle/extension/mod.rs | 24 - vnt/src/handle/handshaker.rs | 112 - vnt/src/handle/maintain/addr_request.rs | 71 - vnt/src/handle/maintain/heartbeat.rs | 260 -- vnt/src/handle/maintain/idle.rs | 210 - vnt/src/handle/maintain/mod.rs | 19 - vnt/src/handle/maintain/punch.rs | 354 -- vnt/src/handle/maintain/re_nat_type.rs | 55 - vnt/src/handle/maintain/up_status.rs | 81 - vnt/src/handle/mod.rs | 276 -- vnt/src/handle/recv_data/client.rs | 401 -- vnt/src/handle/recv_data/mod.rs | 187 - vnt/src/handle/recv_data/server.rs | 599 --- vnt/src/handle/recv_data/turn.rs | 52 - vnt/src/handle/registrar.rs | 54 - vnt/src/handle/tun_tap/mod.rs | 35 - vnt/src/handle/tun_tap/platform.rs | 115 - vnt/src/handle/tun_tap/tun_handler.rs | 288 -- vnt/src/ip_proxy/icmp_proxy.rs | 225 - vnt/src/ip_proxy/mod.rs | 126 - vnt/src/ip_proxy/tcp_proxy.rs | 160 - vnt/src/ip_proxy/udp_proxy.rs | 192 - vnt/src/lib.rs | 31 - vnt/src/nat/mod.rs | 354 -- vnt/src/nat/stun.rs | 224 - vnt/src/port_mapping/mod.rs | 101 - vnt/src/port_mapping/tcp_mapping.rs | 54 - vnt/src/port_mapping/udp_mapping.rs | 107 - vnt/src/protocol/body.rs | 593 --- vnt/src/protocol/control_packet.rs | 165 - vnt/src/protocol/error_packet.rs | 89 - vnt/src/protocol/extension.rs | 141 - vnt/src/protocol/ip_turn_packet.rs | 120 - vnt/src/protocol/mod.rs | 309 -- vnt/src/protocol/other_turn_packet.rs | 23 - vnt/src/protocol/service_packet.rs | 53 - vnt/src/tun_tap_device/create_device.rs | 241 -- vnt/src/tun_tap_device/mod.rs | 11 - vnt/src/tun_tap_device/tun_create_helper.rs | 141 - vnt/src/tun_tap_device/vnt_device.rs | 7 - vnt/src/util/counter/adder.rs | 33 - vnt/src/util/counter/mod.rs | 2 - vnt/src/util/dns_query.rs | 436 -- vnt/src/util/limit/mod.rs | 5 - vnt/src/util/limit/rate_limiter.rs | 62 - vnt/src/util/limit/traffic_meter.rs | 132 - vnt/src/util/mod.rs | 17 - vnt/src/util/notify.rs | 174 - vnt/src/util/scheduler.rs | 176 - vnt/src/util/upnp.rs | 81 - 261 files changed, 20179 insertions(+), 21547 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 Cargo.lock delete mode 100644 common/Cargo.toml delete mode 100644 common/build.rs delete mode 100644 common/src/args_parse.rs delete mode 100644 common/src/callback.rs delete mode 100644 common/src/cli.rs delete mode 100644 common/src/command/client.rs delete mode 100644 common/src/command/entity.rs delete mode 100644 common/src/command/mod.rs delete mode 100644 common/src/command/server.rs delete mode 100644 common/src/config/file_config.rs delete mode 100644 common/src/config/mod.rs delete mode 100644 common/src/console_out/mod.rs delete mode 100644 common/src/console_out/table.rs delete mode 100644 common/src/identifier.rs delete mode 100644 common/src/lib.rs create mode 100644 dll/amd64/wintun.dll create mode 100644 dll/arm/wintun.dll create mode 100644 dll/arm64/wintun.dll create mode 100644 dll/x86/wintun.dll delete mode 100644 documents/img/ssh.jpg create mode 100644 src/args_config.rs create mode 100644 src/extract_wintun_dll.rs create mode 100644 src/lib.rs create mode 100644 src/log.rs create mode 100644 src/main_cli.rs create mode 100644 src/main_ctrl.rs create mode 100644 src/main_web.rs delete mode 100644 vn-link-cli/Cargo.toml delete mode 100644 vn-link-cli/README.md delete mode 100644 vn-link-cli/build.rs delete mode 100644 vn-link-cli/src/main.rs delete mode 100644 vn-link/Cargo.toml delete mode 100644 vn-link/src/config.rs delete mode 100644 vn-link/src/in_mapping/mod.rs delete mode 100644 vn-link/src/in_mapping/tcp.rs delete mode 100644 vn-link/src/in_mapping/udp.rs delete mode 100644 vn-link/src/lib.rs delete mode 100644 vn-link/src/out_mapping/mod.rs delete mode 100644 vn-link/src/out_mapping/tcp.rs delete mode 100644 vn-link/src/out_mapping/udp.rs delete mode 100644 vn-link/src/vnt_link/mod.rs delete mode 100644 vnt-cli/Cargo.toml delete mode 100644 vnt-cli/README.md delete mode 100644 vnt-cli/build.rs delete mode 100644 vnt-cli/src/main.rs delete mode 100644 vnt-cli/src/root_check/mod.rs delete mode 100644 vnt-cli/src/root_check/unix.rs delete mode 100644 vnt-cli/src/root_check/windows.rs create mode 100644 vnt-core/Cargo.toml create mode 100644 vnt-core/build.rs create mode 100644 vnt-core/proto/client.proto create mode 100644 vnt-core/proto/control_message.proto create mode 100644 vnt-core/proto/fec.proto create mode 100644 vnt-core/proto/rpc.proto create mode 100644 vnt-core/src/api/mod.rs create mode 100644 vnt-core/src/compression/lz4_compression.rs create mode 100644 vnt-core/src/compression/mod.rs create mode 100644 vnt-core/src/context/config.rs create mode 100644 vnt-core/src/context/mod.rs create mode 100644 vnt-core/src/context/nat.rs create mode 100644 vnt-core/src/core/mod.rs create mode 100644 vnt-core/src/crypto/chacha20_poly1305.rs create mode 100644 vnt-core/src/crypto/mod.rs create mode 100644 vnt-core/src/enhanced_tunnel/inbound.rs create mode 100644 vnt-core/src/enhanced_tunnel/mod.rs create mode 100644 vnt-core/src/enhanced_tunnel/outbound.rs create mode 100644 vnt-core/src/enhanced_tunnel/quic_over/boot.rs create mode 100644 vnt-core/src/enhanced_tunnel/quic_over/enhanced_io/enhanced_inbound.rs create mode 100644 vnt-core/src/enhanced_tunnel/quic_over/enhanced_io/enhanced_outbound.rs create mode 100644 vnt-core/src/enhanced_tunnel/quic_over/enhanced_io/mod.rs create mode 100644 vnt-core/src/enhanced_tunnel/quic_over/enhanced_io/socket.rs create mode 100644 vnt-core/src/enhanced_tunnel/quic_over/mod.rs create mode 100644 vnt-core/src/enhanced_tunnel/quic_over/quic_client.rs create mode 100644 vnt-core/src/enhanced_tunnel/quic_over/quic_inbound.rs create mode 100644 vnt-core/src/enhanced_tunnel/quic_over/quic_outbound.rs create mode 100644 vnt-core/src/enhanced_tunnel/quic_over/quic_server.rs create mode 100644 vnt-core/src/fec/decoder.rs create mode 100644 vnt-core/src/fec/encoder.rs create mode 100644 vnt-core/src/fec/mod.rs create mode 100644 vnt-core/src/lib.rs create mode 100644 vnt-core/src/nat/internal_nat/icmp_nat.rs create mode 100644 vnt-core/src/nat/internal_nat/mod.rs create mode 100644 vnt-core/src/nat/internal_nat/tcp_nat.rs create mode 100644 vnt-core/src/nat/internal_nat/udp_nat.rs create mode 100644 vnt-core/src/nat/mod.rs create mode 100644 vnt-core/src/port_mapping/mod.rs create mode 100644 vnt-core/src/port_mapping/tcp_port_mapping.rs create mode 100644 vnt-core/src/port_mapping/udp_port_mapping.rs create mode 100644 vnt-core/src/protocol/client_message.rs create mode 100644 vnt-core/src/protocol/control_message.rs create mode 100644 vnt-core/src/protocol/ip_packet_protocol.rs create mode 100644 vnt-core/src/protocol/mod.rs create mode 100644 vnt-core/src/protocol/rpc_message.rs create mode 100644 vnt-core/src/protocol/transmission.rs create mode 100644 vnt-core/src/tls/cert.rs create mode 100644 vnt-core/src/tls/mod.rs create mode 100644 vnt-core/src/tls/verifier.rs create mode 100644 vnt-core/src/tun/enhanced_tun.rs create mode 100644 vnt-core/src/tun/general.rs create mode 100644 vnt-core/src/tun/mod.rs create mode 100644 vnt-core/src/tun/sender.rs create mode 100644 vnt-core/src/tunnel_core/mod.rs create mode 100644 vnt-core/src/tunnel_core/outbound.rs create mode 100644 vnt-core/src/tunnel_core/p2p/inbound.rs create mode 100644 vnt-core/src/tunnel_core/p2p/mod.rs create mode 100644 vnt-core/src/tunnel_core/p2p/outbound.rs create mode 100644 vnt-core/src/tunnel_core/p2p/route_table.rs create mode 100644 vnt-core/src/tunnel_core/p2p/transport/mod.rs create mode 100644 vnt-core/src/tunnel_core/p2p/transport/nat_test.rs create mode 100644 vnt-core/src/tunnel_core/p2p/transport/punch.rs create mode 100644 vnt-core/src/tunnel_core/p2p/transport/task.rs create mode 100644 vnt-core/src/tunnel_core/server/connection_manager.rs create mode 100644 vnt-core/src/tunnel_core/server/inbound.rs create mode 100644 vnt-core/src/tunnel_core/server/mod.rs create mode 100644 vnt-core/src/tunnel_core/server/outbound.rs create mode 100644 vnt-core/src/tunnel_core/server/rpc.rs create mode 100644 vnt-core/src/tunnel_core/server/transport/config.rs create mode 100644 vnt-core/src/tunnel_core/server/transport/mod.rs create mode 100644 vnt-core/src/tunnel_core/server/transport/quic.rs create mode 100644 vnt-core/src/tunnel_core/server/transport/tcp.rs create mode 100644 vnt-core/src/tunnel_core/server/transport/wss.rs create mode 100644 vnt-core/src/utils/device_id.rs create mode 100644 vnt-core/src/utils/dns_query.rs create mode 100644 vnt-core/src/utils/mod.rs create mode 100644 vnt-core/src/utils/task_control.rs create mode 100644 vnt-ipc/Cargo.toml create mode 100644 vnt-ipc/build.rs create mode 100644 vnt-ipc/proto/local_ipc.proto create mode 100644 vnt-ipc/src/client.rs create mode 100644 vnt-ipc/src/lib.rs create mode 100644 vnt-ipc/src/message.rs create mode 100644 vnt-ipc/src/server.rs create mode 100644 vnt-web/Cargo.toml create mode 100644 vnt-web/src/lib.rs create mode 100644 vnt-web/src/service_http.rs create mode 100644 vnt-web/static/index.html create mode 100644 vnt-web/static/tailwindcss3.4.17.js.gz create mode 100644 vnt-web/static/vue-router.global.prod.js.gz create mode 100644 vnt-web/static/vue.global.prod.js.gz delete mode 100644 vnt/Cargo.toml delete mode 100644 vnt/LICENSE delete mode 100644 vnt/build.rs delete mode 100644 vnt/packet/Cargo.toml delete mode 100644 vnt/packet/src/arp/arp.rs delete mode 100644 vnt/packet/src/arp/mod.rs delete mode 100644 vnt/packet/src/ethernet/mod.rs delete mode 100644 vnt/packet/src/ethernet/packet.rs delete mode 100644 vnt/packet/src/ethernet/protocol.rs delete mode 100644 vnt/packet/src/icmp/icmp.rs delete mode 100644 vnt/packet/src/icmp/mod.rs delete mode 100644 vnt/packet/src/igmp/igmp_v1.rs delete mode 100644 vnt/packet/src/igmp/igmp_v2.rs delete mode 100644 vnt/packet/src/igmp/igmp_v3.rs delete mode 100644 vnt/packet/src/igmp/mod.rs delete mode 100644 vnt/packet/src/ip/ipv4/mod.rs delete mode 100644 vnt/packet/src/ip/ipv4/packet.rs delete mode 100644 vnt/packet/src/ip/ipv4/protocol.rs delete mode 100644 vnt/packet/src/ip/mod.rs delete mode 100644 vnt/packet/src/lib.rs delete mode 100644 vnt/packet/src/tcp/mod.rs delete mode 100644 vnt/packet/src/tcp/tcp.rs delete mode 100644 vnt/packet/src/udp/mod.rs delete mode 100644 vnt/packet/src/udp/udp.rs delete mode 100644 vnt/proto/message.proto delete mode 100644 vnt/src/channel/context.rs delete mode 100644 vnt/src/channel/handler.rs delete mode 100644 vnt/src/channel/idle.rs delete mode 100644 vnt/src/channel/mod.rs delete mode 100644 vnt/src/channel/notify.rs delete mode 100644 vnt/src/channel/punch.rs delete mode 100644 vnt/src/channel/sender.rs delete mode 100644 vnt/src/channel/socket/mod.rs delete mode 100644 vnt/src/channel/socket/unix.rs delete mode 100644 vnt/src/channel/socket/windows.rs delete mode 100644 vnt/src/channel/tcp_channel.rs delete mode 100644 vnt/src/channel/udp_channel.rs delete mode 100644 vnt/src/channel/ws_channel.rs delete mode 100644 vnt/src/cipher/aes_cbc/mod.rs delete mode 100644 vnt/src/cipher/aes_cbc/rs_aes_cbc.rs delete mode 100644 vnt/src/cipher/aes_ecb/mod.rs delete mode 100644 vnt/src/cipher/aes_ecb/openssl_aes_ecb.rs delete mode 100644 vnt/src/cipher/aes_ecb/rs_aes_ecb.rs delete mode 100644 vnt/src/cipher/aes_gcm/aes_gcm_cipher.rs delete mode 100644 vnt/src/cipher/aes_gcm/mod.rs delete mode 100644 vnt/src/cipher/aes_gcm/ring_aes_gcm_cipher.rs delete mode 100644 vnt/src/cipher/chacha20/mod.rs delete mode 100644 vnt/src/cipher/chacha20/rs_chacha20.rs delete mode 100644 vnt/src/cipher/chacha20_poly1305/mod.rs delete mode 100644 vnt/src/cipher/chacha20_poly1305/ring_chacha20_poly1305.rs delete mode 100644 vnt/src/cipher/chacha20_poly1305/rs_chacha20_poly1305.rs delete mode 100644 vnt/src/cipher/cipher.rs delete mode 100644 vnt/src/cipher/finger.rs delete mode 100644 vnt/src/cipher/mod.rs delete mode 100644 vnt/src/cipher/rsa_cipher.rs delete mode 100644 vnt/src/cipher/sm4_cbc/mod.rs delete mode 100644 vnt/src/cipher/sm4_cbc/rs_sm4_cbc.rs delete mode 100644 vnt/src/cipher/xor/mod.rs delete mode 100644 vnt/src/cipher/xor/xor.rs delete mode 100644 vnt/src/compression/lz4_compress.rs delete mode 100644 vnt/src/compression/mod.rs delete mode 100644 vnt/src/compression/zstd_compress.rs delete mode 100644 vnt/src/core/conn.rs delete mode 100644 vnt/src/core/mod.rs delete mode 100644 vnt/src/external_route/mod.rs delete mode 100644 vnt/src/handle/callback.rs delete mode 100644 vnt/src/handle/extension/mod.rs delete mode 100644 vnt/src/handle/handshaker.rs delete mode 100644 vnt/src/handle/maintain/addr_request.rs delete mode 100644 vnt/src/handle/maintain/heartbeat.rs delete mode 100644 vnt/src/handle/maintain/idle.rs delete mode 100644 vnt/src/handle/maintain/mod.rs delete mode 100644 vnt/src/handle/maintain/punch.rs delete mode 100644 vnt/src/handle/maintain/re_nat_type.rs delete mode 100644 vnt/src/handle/maintain/up_status.rs delete mode 100644 vnt/src/handle/mod.rs delete mode 100644 vnt/src/handle/recv_data/client.rs delete mode 100644 vnt/src/handle/recv_data/mod.rs delete mode 100644 vnt/src/handle/recv_data/server.rs delete mode 100644 vnt/src/handle/recv_data/turn.rs delete mode 100644 vnt/src/handle/registrar.rs delete mode 100644 vnt/src/handle/tun_tap/mod.rs delete mode 100644 vnt/src/handle/tun_tap/platform.rs delete mode 100644 vnt/src/handle/tun_tap/tun_handler.rs delete mode 100644 vnt/src/ip_proxy/icmp_proxy.rs delete mode 100644 vnt/src/ip_proxy/mod.rs delete mode 100644 vnt/src/ip_proxy/tcp_proxy.rs delete mode 100644 vnt/src/ip_proxy/udp_proxy.rs delete mode 100644 vnt/src/lib.rs delete mode 100644 vnt/src/nat/mod.rs delete mode 100644 vnt/src/nat/stun.rs delete mode 100644 vnt/src/port_mapping/mod.rs delete mode 100644 vnt/src/port_mapping/tcp_mapping.rs delete mode 100644 vnt/src/port_mapping/udp_mapping.rs delete mode 100644 vnt/src/protocol/body.rs delete mode 100644 vnt/src/protocol/control_packet.rs delete mode 100644 vnt/src/protocol/error_packet.rs delete mode 100644 vnt/src/protocol/extension.rs delete mode 100644 vnt/src/protocol/ip_turn_packet.rs delete mode 100644 vnt/src/protocol/mod.rs delete mode 100644 vnt/src/protocol/other_turn_packet.rs delete mode 100644 vnt/src/protocol/service_packet.rs delete mode 100644 vnt/src/tun_tap_device/create_device.rs delete mode 100644 vnt/src/tun_tap_device/mod.rs delete mode 100644 vnt/src/tun_tap_device/tun_create_helper.rs delete mode 100644 vnt/src/tun_tap_device/vnt_device.rs delete mode 100644 vnt/src/util/counter/adder.rs delete mode 100644 vnt/src/util/counter/mod.rs delete mode 100644 vnt/src/util/dns_query.rs delete mode 100644 vnt/src/util/limit/mod.rs delete mode 100644 vnt/src/util/limit/rate_limiter.rs delete mode 100644 vnt/src/util/limit/traffic_meter.rs delete mode 100644 vnt/src/util/mod.rs delete mode 100644 vnt/src/util/notify.rs delete mode 100644 vnt/src/util/scheduler.rs delete mode 100644 vnt/src/util/upnp.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..16823e5 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,22 @@ +[build] +# target = ["x86_64-unknown-linux-musl"] +# target = ["x86_64-unknown-linux-gnu"] +# target = ["aarch64-linux-android"] +# target = ["x86_64-linux-android"] +# target = ["aarch64-apple-ios"] +# target = ["x86_64-apple-ios"] +# target = ["x86_64-pc-windows-msvc"] +# target = ["x86_64-apple-darwin"] +# target = ["x86_64-unknown-freebsd"] + +[target.aarch64-unknown-linux-musl] +linker = "aarch64-linux-musl-gcc" +rustflags = ["-C", "target-feature=+crt-static", "-C", "strip=symbols"] + +[target.mipsel-unknown-linux-musl] +linker = "mipsel-linux-muslsf-gcc" +rustflags = ["-C", "target-feature=+crt-static","-C", "strip=symbols","-C", "link-arg=-static", + "-C", "relocation-model=static","-C", "link-arg=-no-pie","--cfg", "compiler_builtins_no_debug", + "-L", "/opt/musl_gcc/mipsel-linux-muslsf-cross/mipsel-linux-muslsf/lib", + "-L", "/opt/musl_gcc/mipsel-linux-muslsf-cross/lib/gcc/mipsel-linux-muslsf/11.2.1"] + diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0ee4919..51c3a56 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -7,71 +7,62 @@ on: env: CARGO_TERM_COLOR: always -permissions: - contents: write + defaults: run: + # necessary for windows shell: bash +permissions: + contents: write + packages: write jobs: build: + name: Build ${{ matrix.TARGET }} strategy: fail-fast: false matrix: include: - - TARGET: i686-unknown-linux-musl - OS: ubuntu-latest - FEATURES: ring-cipher,openssl-vendored,wss - TARGET: x86_64-unknown-linux-musl OS: ubuntu-latest - FEATURES: ring-cipher,wss - TARGET: aarch64-unknown-linux-musl OS: ubuntu-latest - FEATURES: ring-cipher,wss - TARGET: armv7-unknown-linux-musleabihf OS: ubuntu-latest - FEATURES: ring-cipher,wss - TARGET: armv7-unknown-linux-musleabi OS: ubuntu-latest - FEATURES: ring-cipher,wss - TARGET: arm-unknown-linux-musleabihf OS: ubuntu-latest - FEATURES: ring-cipher,wss - TARGET: arm-unknown-linux-musleabi OS: ubuntu-latest - FEATURES: ring-cipher,wss - TARGET: x86_64-apple-darwin OS: macos-latest - FEATURES: ring-cipher,wss - TARGET: aarch64-apple-darwin OS: macos-latest - FEATURES: ring-cipher,wss - TARGET: i686-pc-windows-msvc OS: windows-latest - FEATURES: ring-cipher,wss - TARGET: x86_64-pc-windows-msvc OS: windows-latest - FEATURES: ring-cipher,wss + - TARGET: aarch64-pc-windows-msvc + OS: windows-latest - TARGET: mipsel-unknown-linux-musl OS: ubuntu-latest - FEATURES: ring-cipher,wss - TARGET: mips-unknown-linux-musl OS: ubuntu-latest - FEATURES: ring-cipher,wss - + - TARGET: x86_64-unknown-freebsd + OS: ubuntu-latest + ARTIFACT_NAME: freebsd-13.2-x86_64 + BSD_VERSION: 13.2 runs-on: ${{ matrix.OS }} env: - NAME: vnt-cli + NAME: Vnt TARGET: ${{ matrix.TARGET }} OS: ${{ matrix.OS }} FEATURES: ${{ matrix.FEATURES }} - steps: - uses: actions/checkout@v4 - - name: Init submodules run: git submodule update --init --recursive --remote && git submodule status - - name: Cargo cache uses: actions/cache@v4 with: @@ -79,69 +70,89 @@ jobs: ~/.cargo/registry ./target key: build-cargo-registry-${{matrix.TARGET}} - + - name: Install Protobuf (MacOS) + if: runner.os == 'macOS' + run: brew install protobuf + - name: Install Protobuf (Windows) + if: runner.os == 'Windows' + run: choco install protoc - name: Use strawberry perl - if: startsWith(matrix.OS, 'windows') + if: startsWith(matrix.os, 'windows') run: echo OPENSSL_SRC_PERL=C:/Strawberry/perl/bin/perl >> $GITHUB_ENV - - name: List run: find ./ - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 + - name: Build Vnt X86_64-FreeBSD + uses: cross-platform-actions/action@v0.23.0 + if: ${{ endsWith(matrix.TARGET, 'freebsd') }} + env: + TARGET: ${{ matrix.TARGET }} with: - toolchain: nightly - profile: minimal - override: true - - - name: Verify Rust version + operating_system: freebsd + environment_variables: TARGET + architecture: x86-64 + version: ${{ matrix.BSD_VERSION }} + shell: bash + memory: 5G + cpu_count: 4 + run: | + uname -a + echo $SHELL + pwd + ls -lah + whoami + env | sort + sudo pkg install -y git protobuf llvm15 + curl --proto 'https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + source $HOME/.cargo/env + export CC=clang + export CXX=clang++ + export CARGO_TERM_COLOR=always + cargo build --release --verbose --bin vnt2_web --features vnt-web --target $TARGET + cargo build --release --verbose --bin vnt2_cli --features vnt-ipc --target $TARGET + cargo build --release --verbose --bin vnt2_ctrl --features vnt-ipc --target $TARGET + - name: Install and configure dependencies + if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }} run: | - rustc --version - cargo --version - - - name: Install dependencies on Ubuntu - if: startsWith(matrix.OS, 'ubuntu') - run: | - sudo apt-get update && sudo apt-get install -y clang llvm libc6-dev-i386 \ - crossbuild-essential-arm64 crossbuild-essential-armhf musl-tools libboost-all-dev libc6-dev wget tar - - # Install musl cross toolchains if needed - case $TARGET in - mipsel-unknown-linux-musl) - MUSL_URI=mipsel-linux-muslsf-cross - URL=mipsel-linux-muslsf - ;; - aarch64-unknown-linux-musl) - MUSL_URI=aarch64-linux-musl-cross - ;; - armv7-unknown-linux-musleabihf) - MUSL_URI=armv7l-linux-musleabihf-cross - ;; - armv7-unknown-linux-musleabi) - MUSL_URI=armv7m-linux-musleabi-cross - ;; - arm-unknown-linux-musleabihf) - MUSL_URI=arm-linux-musleabihf-cross - ;; - arm-unknown-linux-musleabi) - MUSL_URI=arm-linux-musleabi-cross - ;; - mips-unknown-linux-musl) - MUSL_URI=mips-linux-muslsf-cross - URL=mips-linux-muslsf - ;; - i686-unknown-linux-musl) - MUSL_URI=i686-linux-musl-cross - ;; - esac - - if [ -n "$MUSL_URI" ]; then - mkdir -p /opt/musl_gcc - wget -c https://github.com/rustp2p/musl-cross/releases/download/0.0.1/$MUSL_URI.tgz -P /opt/musl_gcc/ - tar zxf /opt/musl_gcc/$MUSL_URI.tgz -C /opt/musl_gcc/ - sudo ln -s /opt/musl_gcc/$MUSL_URI/bin/*gcc /usr/bin/ + # dependencies are only needed on ubuntu as that's the only place where + # we make cross-compilation + + if [[ $OS =~ ^ubuntu.*$ ]]; then + sudo apt-get update && sudo apt-get install protobuf-compiler clang llvm libc6-dev-i386 crossbuild-essential-arm64 crossbuild-essential-armhf musl-tools libboost-all-dev libc6-dev -y + # curl -s musl.cc | grep mipsel + case $TARGET in + mipsel-unknown-linux-musl) + MUSL_URI=mipsel-linux-muslsf-cross + URL=mipsel-linux-muslsf + ;; + aarch64-unknown-linux-musl) + MUSL_URI=aarch64-linux-musl-cross + ;; + armv7-unknown-linux-musleabihf) + MUSL_URI=armv7l-linux-musleabihf-cross + ;; + armv7-unknown-linux-musleabi) + MUSL_URI=armv7m-linux-musleabi-cross + ;; + arm-unknown-linux-musleabihf) + MUSL_URI=arm-linux-musleabihf-cross + ;; + arm-unknown-linux-musleabi) + MUSL_URI=arm-linux-musleabi-cross + ;; + mips-unknown-linux-musl) + MUSL_URI=mips-linux-muslsf-cross + URL=mips-linux-muslsf + ;; + esac + + + if [ -n "$MUSL_URI" ]; then + mkdir -p ./musl_gcc + wget -c https://github.com/rustp2p/musl-cross/releases/download/0.0.1/$MUSL_URI.tgz -P /opt/musl_gcc/ + tar zxf /opt/musl_gcc/$MUSL_URI.tgz -C /opt/musl_gcc/ + sudo ln -s /opt/musl_gcc/$MUSL_URI/bin/*gcc /usr/bin/ + fi fi - if [[ $TARGET =~ ^mips.*$ ]]; then cd /opt/musl_gcc/${URL}-cross/lib/gcc/${URL}/11.2.1 cp libgcc_eh.a libunwind.a @@ -152,13 +163,10 @@ jobs: cp -f $RUST_LIB_SRC/library/Cargo.lock $RUST_LIB_SRC/Cargo.lock fi fi - - rustup -V - - - - name: Configure cargo for cross-compilation - run: | - cat >>~/.cargo/config <>~/.cargo/config.toml <vnt-cli.exe -k 123456 - ``` -2. 可以执行info命令查看当前设备的虚拟ip - ```shell - root@DESKTOP-0BCHNIO:/opt# ./vnt-cli --info - Name: Ubuntu 18.04 (bionic) [64-bit] - Virtual ip: 10.26.0.2 - Virtual gateway: 10.26.0.1 - Virtual netmask: 255.255.255.0 - Connection status: Connected - NAT type: Cone - Relay server: 43.139.56.10:29871 - Public ips: 120.228.76.75 - Local ip: 172.25.165.58 - ``` -3. 也可以执行list命令查看其他设备的虚拟ip - ```shell - root@DESKTOP-0BCHNIO:/opt# ./vnt-cli --list - Name Virtual Ip P2P/Relay Rt Status - Windows 10.0.22621 (Windows 11 Professional) [64-bit] 10.26.0.3 p2p 2 Online - CentOS 7.9.2009 (Core) [64-bit] 10.26.0.4 p2p 35 Online - ``` -4. 最后可以用虚拟ip实现设备间相互访问 - - ssh -5. 帮助,使用-h命令查看 - -### 使用须知 - -- token的作用是标识一个虚拟局域网,当使用公共服务器时,建议使用一个唯一值当token(比如uuid),否则有可能连接到其他人创建的虚拟局域网中 -- 默认使用公共服务器做注册和中继,目前的配置是2核4G 4Mbps,有需要再扩展~ -- vnt-cli需要使用命令行运行 -- Mac和Linux下需要加可执行权限(例如:chmod +x ./vnt-cli) -- 可以自己搭中继服务器([server](https://github.com/vnt-dev/vnts)) - -### 直接使用 - -[**下载release文件**](https://github.com/vnt-dev/vnt/releases) - -[**帮助文档**](https://rustvnt.com) - -### 自行编译 - -
点击展开 - -前提条件:安装rust编译环境([install rust](https://www.rust-lang.org/zh-CN/tools/install)) +## 使用vnt2_cli组网 +使用方式和vnt1.0一样,只是增减了一些功能,具体参数请查看 -h ``` -到项目根目录下执行 cargo build -p vnt-cli - -也可按需编译,将得到更小的二进制文件,使用--no-default-features排除默认features - -cargo build -p vnt-cli --no-default-features +# 启动程序 服务端可以使用101.35.230.139:6660 +./vnt2_cli -k 123456 -s 101.35.230.139:6660 ``` -features说明 +``` +# 查看组网信息 +./vnt2_ctrl info +``` + +## 使用vnt2_web组网 -| feature | 说明 | 是否默认 | -|-------------------|--------------------------------|------| -| openssl | 使用openssl中的加密算法 | 否 | -| openssl-vendored | 从源码编译openssl | 否 | -| ring-cipher | 使用ring中的加密算法 | 否 | -| aes_cbc | 支持aes_cbc加密 | 是 | -| aes_ecb | 支持aes_ecb加密 | 是 | -| aes_gcm | 支持aes_gcm加密 | 是 | -| sm4_cbc | 支持sm4_cbc加密 | 是 | -| chacha20_poly1305 | 支持chacha20和chacha20_poly1305加密 | 是 | -| server_encrypt | 支持服务端加密 | 是 | -| ip_proxy | 内置ip代理 | 是 | -| port_mapping | 端口映射 | 是 | -| log | 日志 | 是 | -| command | list、route等命令 | 是 | -| file_config | yaml配置文件 | 是 | -| lz4 | lz4压缩 | 是 | -| zstd | zstd压缩 | 否 | -| upnp | upnp协议 | 否 | -| ws | ws协议 | 是 | -| wss | wss协议 | 是 | +1. 启动程序 -
+ ``` + # 启动程序 + ./vnt2_web + ``` +2. 浏览器打开 http://127.0.0.1:19099 +3. 在页面上添加组网配置,再启动组网 -### 支持平台 -- Mac -- Linux -- Windows - - 默认使用tun网卡 依赖wintun.dll([win-tun](https://www.wintun.net/))(将dll放到同目录下,建议使用版本0.14.1) - - 可选择使用tap网卡 依赖tap-windows([win-tap](https://build.openvpn.net/downloads/releases/))(建议使用版本9.24.7) -- Android +# VNT2.0新特性 -### GUI +1. 提升安全性,支持tcp-tls、quic、wss协议连接服务器,和服务端强制使用tls加密,并支持证书绑定,防止伪造服务端攻击 +2. 提升流量稳定性,支持使用quic代理流量,支持FEC冗余传输 +3. 简化操作,去除了大量vnt1.0的重复和无用的配置参数 +4. vnt-link、vnt合二为一 +5. 支持有tun模式、无tun模式、端口映射 +6. 全功能的情况下,减少程序体积 +7. 性能提升,支持linux-offload +8. 更规范的api接入,可以轻松自定义客户端 +9. 支持同时连接多个服务端,可以容灾和负载均衡 -支持安卓和Windows [下载](https://github.com/vnt-dev/VntApp/releases/) +# 说明 -### 特性 +vnt2.0整体重构了一遍,和1.0不兼容,同时也可能引入新的bug,欢迎反馈 -- IP层数据转发 -- NAT穿透 - - 点对点穿透 - - 服务端中继转发 - - 客户端中继转发 -- IP代理(点对点、点对网) -- p2p组播/广播 -- 客户端数据加密(`aes-gcm`、`chacha20-poly1305`等多种加密算法) -- 服务端数据加密(`rsa` + `aes-gcm`) -- 多通道UDP应对QOS -- 支持TCP、UDP、WebSocket等多种协议 -- 支持数据压缩 +其他平台后续再推出 -### 更多玩法 +### 相关库 +1. tun虚拟网卡(https://github.com/tun-rs/tun-rs) +2. 路由设置(https://github.com/tun-rs/route_manager) +3. 用户态协议栈(用于quic代理和无tun模式出口)(https://github.com/rustp2p/tcp_ip) +4. 打洞通道处理(https://github.com/rustp2p/rustp2p/tree/master/rustp2p-core) -1. 和远程桌面(如mstsc)搭配,超低延迟的体验 -2. 安装samba服务,共享磁盘 -3. 点对网,访问内网其他机器、IP代理(结合启动参数'-i'和'-o') -### Todo -- ~~桌面UI(已支持)~~ -- 使用FEC、ARQ等方式提升弱网环境的稳定性 -### 常见问题 - -
展开 - -#### 问题1: 设置网络地址失败 - -##### 可能原因: - -vnt默认使用10.26.0.0/24网段,和本地网络适配器的ip冲突 - -##### 解决方法: - -1. 方法一:找到冲突的IP,将其改成别的 -2. 方法二:自建服务器,指定其他不会冲突的网段 -3. 方法三:增加参数-d ,设置不同的id会让服务端分配不同的IP,从而绕开有冲突的IP - -#### 问题2: windows系统上wintun.dll加载失败 - -##### 可能原因: - -没有下载wintun.dll 或者使用的wintun.dll有问题 - -##### 解决方法: - -1. 下载最新版的wintun.dll [下载链接](https://www.wintun.net/builds/wintun-0.14.1.zip) -2. 解压后找到对应架构的目录,通常是amd64 -3. 将对应的wintun.dll放到和vnt-cli同目录下(或者放到C盘Windows目录下) -4. 再次启动vnt-cli - -#### 问题3: 丢包严重,或是不能正常组网通信 - -##### 可能原因: - -某些宽带下(比如广电宽带)UDP丢包严重 - -##### 解决方法: - -1. 使用TCP模式中继转发(vnt-cli增加--tcp参数) -2. 如果p2p后效果很差,可以选择禁用p2p(vnt-cli增加--use-channel relay 参数) - -#### 问题4:重启后虚拟IP发生变化,或指定了IP不能启动 - -##### 可能原因: - -设备重启后程序自动获取的id值改变,导致注册时重新分配了新的IP,或是IP冲突 - -##### 解决方法: - -1. 命令行启动增加-d参数(使用配置文件启动则在配置文件中增加device_id参数),要保证每个设备的值都不一样,取值可以任意64位以内字符串 - -
- -### 交流群 - -对VNT有任何问题均可以加群联系作者 - -QQ群1: 1034868233(满人) - -QQ群2: 950473757 - -QQ群3: 1060550456 - -### 赞助 - -如果VNT对你有帮助,欢迎打赏作者 - - - -### 其他 - -可使用社区小伙伴搭建的中继服务器 - -1. -s vnt.8443.eu.org:29871 -2. -s vnt.wherewego.top:29872 - -### 参与贡献 - - - - diff --git a/common/Cargo.toml b/common/Cargo.toml deleted file mode 100644 index 5aec21f..0000000 --- a/common/Cargo.toml +++ /dev/null @@ -1,46 +0,0 @@ -[package] -name = "common" -version = "1.2.16" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -vnt = { path = "../vnt", package = "vnt", default-features = false } -anyhow = "1.0.82" -console = "0.15.2" -log = "0.4.17" -log4rs = { version = "1.3.0", optional = true } -serde = { version = "1.0", features = ["derive"] } -serde_yaml = "0.9.32" -getopts = "0.2.21" -gethostname = "0.4.3" -uuid = { version = "1.8.0", features = ["v4"] } -sys-locale = "0.3.1" - -[features] -default = [] -openssl = ["vnt/openssl"] -openssl-vendored = ["vnt/openssl-vendored"] -ring-cipher = ["vnt/ring-cipher"] -aes_cbc = ["vnt/aes_cbc"] -aes_ecb = ["vnt/aes_ecb"] -sm4_cbc = ["vnt/sm4_cbc"] -aes_gcm = ["vnt/aes_gcm"] -chacha20_poly1305 = ["vnt/chacha20_poly1305"] -server_encrypt = ["vnt/server_encrypt"] -ip_proxy = ["vnt/ip_proxy"] -port_mapping = ["vnt/port_mapping"] -lz4 = ["vnt/lz4_compress"] -zstd = ["vnt/zstd_compress"] -upnp = ["vnt/upnp"] -ws = ["vnt/ws"] -wss = ["vnt/wss"] -command = [] -file_config = [] -log = ["log4rs"] -integrated_tun = ["vnt/integrated_tun"] - -[build-dependencies] -rand = "0.8.5" -chrono = "0.4.23" \ No newline at end of file diff --git a/common/build.rs b/common/build.rs deleted file mode 100644 index 8970ed9..0000000 --- a/common/build.rs +++ /dev/null @@ -1,16 +0,0 @@ -use rand::Rng; -use std::fs::File; -use std::io::Write; - -fn main() { - let now_time = chrono::Local::now(); - let serial_number = format!( - "{}-{}", - &now_time.format("%y%m%d%H%M").to_string(), - rand::thread_rng().gen_range(100..1000) - ); - let generated_code = format!(r#"pub const SERIAL_NUMBER: &str = "{}";"#, serial_number); - let dest_path = "src/generated_serial_number.rs"; - let mut file = File::create(&dest_path).unwrap(); - file.write_all(generated_code.as_bytes()).unwrap(); -} diff --git a/common/src/args_parse.rs b/common/src/args_parse.rs deleted file mode 100644 index 9f3ff01..0000000 --- a/common/src/args_parse.rs +++ /dev/null @@ -1,82 +0,0 @@ -use std::net::Ipv4Addr; - -pub fn ips_parse(ips: &Vec) -> Result, String> { - let mut in_ips_c = vec![]; - for x in ips { - let mut split = x.split(","); - let net = if let Some(net) = split.next() { - net - } else { - return Err(format!("ipv4/mask,ipv4 {:?}", x)); - }; - let ip = if let Some(ip) = split.next() { - ip - } else { - return Err(format!("ipv4/mask,ipv4 {:?}", x)); - }; - let ip = if let Ok(ip) = ip.parse::() { - ip - } else { - return Err(format!("not ipv4 {:?}", ip)); - }; - let mut split = net.split("/"); - let dest = if let Some(dest) = split.next() { - dest - } else { - return Err(format!("no ipv4/mask {:?}", net)); - }; - let mask = if let Some(mask) = split.next() { - mask - } else { - return Err(format!("no netmask {:?}", net)); - }; - let dest = if let Ok(dest) = dest.parse::() { - dest - } else { - return Err(format!("not ipv4 {:?}", dest)); - }; - let mask = to_ip(mask)?; - in_ips_c.push((u32::from_be_bytes(dest.octets()), mask, ip)); - } - Ok(in_ips_c) -} - -pub fn out_ips_parse(ips: &Vec) -> Result, String> { - let mut in_ips_c = vec![]; - for x in ips { - let mut split = x.split("/"); - let dest = if let Some(dest) = split.next() { - dest - } else { - return Err(format!("no ipv4/mask {:?}", x)); - }; - let mask = if let Some(mask) = split.next() { - mask - } else { - return Err(format!("no netmask {:?}", x)); - }; - let dest = if let Ok(dest) = dest.parse::() { - dest - } else { - return Err(format!("not ipv4 {:?}", dest)); - }; - let mask = to_ip(mask)?; - in_ips_c.push((u32::from_be_bytes(dest.octets()), mask)); - } - Ok(in_ips_c) -} - -pub fn to_ip(mask: &str) -> Result { - if let Ok(m) = mask.parse::() { - if m > 32 { - return Err("not netmask".to_string()); - } - let mut mask = 0u32; - for i in 0..m { - mask = mask | (1 << (31 - i)); - } - Ok(mask) - } else { - Err("not netmask".to_string()) - } -} diff --git a/common/src/callback.rs b/common/src/callback.rs deleted file mode 100644 index dff2f1e..0000000 --- a/common/src/callback.rs +++ /dev/null @@ -1,52 +0,0 @@ -use std::process; - -use console::style; -use vnt::{ConnectInfo, ErrorInfo, ErrorType, HandshakeInfo, RegisterInfo, VntCallback}; - -#[derive(Clone)] -pub struct VntHandler {} - -impl VntCallback for VntHandler { - fn success(&self) { - println!(" {} ", style("====== Connect Successfully ======").green()) - } - #[cfg(feature = "integrated_tun")] - fn create_tun(&self, info: vnt::DeviceInfo) { - println!("create_tun {}", info) - } - - fn connect(&self, info: ConnectInfo) { - println!("connect {}", info) - } - - fn handshake(&self, info: HandshakeInfo) -> bool { - println!("handshake {}", info); - true - } - - fn register(&self, info: RegisterInfo) -> bool { - println!("register {}", style(info).green()); - true - } - - fn error(&self, info: ErrorInfo) { - log::error!("error {:?}", info); - println!("{}", style(format!("error {}", info)).red()); - match info.code { - ErrorType::TokenError - | ErrorType::AddressExhausted - | ErrorType::IpAlreadyExists - | ErrorType::InvalidIp - | ErrorType::LocalIpExists - | ErrorType::FailedToCrateDevice => { - self.stop(); - } - _ => {} - } - } - - fn stop(&self) { - println!("stopped"); - process::exit(0) - } -} diff --git a/common/src/cli.rs b/common/src/cli.rs deleted file mode 100644 index c645003..0000000 --- a/common/src/cli.rs +++ /dev/null @@ -1,620 +0,0 @@ -use crate::args_parse::{ips_parse, out_ips_parse}; -#[cfg(feature = "command")] -use crate::command; -use crate::{config, generated_serial_number}; -use anyhow::anyhow; -use console::style; -use getopts::Options; -use std::collections::HashMap; -use std::io; -use std::net::Ipv4Addr; -use std::path::PathBuf; -use std::str::FromStr; -use sys_locale::get_locale; -use vnt::channel::punch::PunchModel; -use vnt::channel::UseChannelType; -use vnt::cipher::CipherModel; -use vnt::compression::Compressor; -use vnt::core::Config; - -pub fn app_home() -> io::Result { - let root_path = match std::env::current_exe() { - Ok(path) => { - if let Some(v) = path.as_path().parent() { - v.to_path_buf() - } else { - log::warn!("current_exe parent none:{:?}", path); - PathBuf::new() - } - } - Err(e) => { - log::warn!("current_exe err:{:?}", e); - PathBuf::new() - } - }; - let path = root_path.join("env"); - if !path.exists() { - std::fs::create_dir_all(&path)?; - } - Ok(path) -} - -pub fn parse_args_config() -> anyhow::Result, bool)>> { - #[cfg(feature = "log")] - let _ = log4rs::init_file("log4rs.yaml", Default::default()); - let args: Vec = std::env::args().collect(); - let program = args[0].clone(); - let mut opts = Options::new(); - opts.optopt("k", "", "组网标识", ""); - opts.optopt("n", "", "设备名称", ""); - opts.optopt("d", "", "设备标识", ""); - opts.optflag("c", "", "关闭交互式命令"); - opts.optopt("s", "", "注册和中继服务器地址", ""); - opts.optmulti("e", "", "stun服务器", ""); - opts.optflag("a", "", "使用tap模式"); - opts.optopt("", "nic", "虚拟网卡名称,windows下使用tap则必填", ""); - opts.optmulti("i", "", "配置点对网(IP代理)入站时使用", ""); - opts.optmulti("o", "", "配置点对网出站时使用", ""); - opts.optopt("w", "", "客户端加密", ""); - opts.optflag("W", "", "服务端加密"); - opts.optopt("u", "", "自定义mtu(默认为1430)", ""); - opts.optopt("", "ip", "指定虚拟ip", ""); - opts.optflag("", "relay", "仅使用服务器转发"); - opts.optopt("", "par", "任务并行度(必须为正整数)", ""); - opts.optopt("", "model", "加密模式", ""); - opts.optflag("", "finger", "指纹校验"); - opts.optopt("", "punch", "取值ipv4/ipv6", ""); - opts.optopt("", "ports", "监听的端口", ""); - opts.optflag("", "cmd", "开启窗口输入"); - opts.optflag("", "no-proxy", "关闭内置代理"); - opts.optflag("", "first-latency", "优先延迟"); - opts.optopt("", "use-channel", "使用通道 relay/p2p", ""); - opts.optopt("", "packet-loss", "丢包率", ""); - opts.optopt("", "packet-delay", "延迟", ""); - opts.optmulti("", "dns", "dns", ""); - opts.optmulti("", "mapping", "mapping", ""); - opts.optmulti("", "vnt-mapping", "vnt-mapping", ""); - opts.optopt("f", "", "配置文件", ""); - opts.optopt("", "compressor", "压缩算法", ""); - opts.optopt("", "local-dev", "指定本地ipv4网卡名称", ""); - opts.optflag("", "disable-stats", "关闭流量统计"); - opts.optflag("", "allow-wg", "允许接入WireGuard"); - //"后台运行时,查看其他设备列表" - opts.optflag("", "add", "后台运行时,添加地址"); - opts.optflag("", "list", "后台运行时,查看其他设备列表"); - opts.optflag("", "all", "后台运行时,查看其他设备完整信息"); - opts.optflag("", "info", "后台运行时,查看当前设备信息"); - opts.optflag("", "route", "后台运行时,查看数据转发路径"); - opts.optflag("", "chart_a", "后台运行时,查看流量统计"); - opts.optopt("", "chart_b", "后台运行时,查看流量统计", ""); - opts.optflag("", "stop", "停止后台运行"); - opts.optflag("h", "help", "帮助"); - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => { - print_usage(&program, opts); - return Err(anyhow::anyhow!("{}", f.to_string())); - } - }; - if matches.opt_present("h") || args.len() == 1 { - print_usage(&program, opts); - return Ok(None); - } - - #[cfg(feature = "command")] - if matches.opt_present("list") { - command::command(command::CommandEnum::List); - return Ok(None); - } else if matches.opt_present("info") { - command::command(command::CommandEnum::Info); - return Ok(None); - } else if matches.opt_present("stop") { - command::command(command::CommandEnum::Stop); - return Ok(None); - } else if matches.opt_present("route") { - command::command(command::CommandEnum::Route); - return Ok(None); - } else if matches.opt_present("all") { - command::command(command::CommandEnum::All); - return Ok(None); - } - #[cfg(feature = "command")] - if matches.opt_present("chart_a") { - command::command(command::CommandEnum::ChartA); - return Ok(None); - } - #[cfg(feature = "command")] - if let Some(v) = matches.opt_str("chart_b") { - command::command(command::CommandEnum::ChartB(v)); - return Ok(None); - } - let conf = matches.opt_str("f"); - let (config, vnt_link_config, cmd) = if conf.is_some() { - match config::read_config(&conf.unwrap()) { - Ok(c) => c, - Err(e) => { - return Err(anyhow::anyhow!("conf err {}", e)); - } - } - } else { - if !matches.opt_present("k") { - print_usage(&program, opts); - return Err(anyhow::anyhow!("parameter -k not found .")); - } - #[cfg(feature = "integrated_tun")] - let device_name = matches.opt_str("nic"); - let token: String = matches.opt_get("k").unwrap().unwrap(); - let device_id = matches.opt_get_default("d", String::new()).unwrap(); - let device_id = if device_id.is_empty() { - config::get_device_id() - } else { - device_id - }; - if device_id.is_empty() { - print_usage(&program, opts); - return Err(anyhow::anyhow!("parameter -d not found .")); - } - let name = matches - .opt_get_default( - "n", - gethostname::gethostname() - .to_str() - .unwrap_or("UnknownName") - .to_string(), - ) - .unwrap(); - let server_address_str = matches - .opt_get_default("s", "vnt.wherewego.top:29872".to_string()) - .unwrap(); - - let mut stun_server = matches.opt_strs("e"); - if stun_server.is_empty() { - for x in config::PUB_STUN { - stun_server.push(x.to_string()); - } - } - let dns = matches.opt_strs("dns"); - let in_ip = matches.opt_strs("i"); - let in_ip = match ips_parse(&in_ip) { - Ok(in_ip) => in_ip, - Err(e) => { - print_usage(&program, opts); - println!(); - println!("-i: {:?} {}", in_ip, e); - return Err(anyhow::anyhow!("example: -i 192.168.0.0/24,10.26.0.3")); - } - }; - let out_ip = matches.opt_strs("o"); - let out_ip = match out_ips_parse(&out_ip) { - Ok(out_ip) => out_ip, - Err(e) => { - print_usage(&program, opts); - println!(); - println!("-o: {:?} {}", out_ip, e); - return Err(anyhow::anyhow!("example: -o 0.0.0.0/0")); - } - }; - let password: Option = matches.opt_get("w").unwrap(); - let server_encrypt = matches.opt_present("W"); - #[cfg(not(feature = "server_encrypt"))] - { - if server_encrypt { - println!("Server encryption not supported"); - return Err(anyhow::anyhow!("Server encryption not supported")); - } - } - let mtu: Option = matches.opt_get("u").unwrap(); - let mtu = if let Some(mtu) = mtu { - match u32::from_str(&mtu) { - Ok(mtu) => Some(mtu), - Err(e) => { - print_usage(&program, opts); - println!(); - println!("'-u {}' {}", mtu, e); - return Err(anyhow::anyhow!("'-u {}' {}", mtu, e)); - } - } - } else { - None - }; - let virtual_ip: Option = matches.opt_get("ip").unwrap(); - let virtual_ip = - virtual_ip.map(|v| Ipv4Addr::from_str(&v).expect(&format!("'--ip {}' error", v))); - if let Some(virtual_ip) = virtual_ip { - if virtual_ip.is_unspecified() || virtual_ip.is_broadcast() || virtual_ip.is_multicast() - { - return Err(anyhow::anyhow!("'--ip {}' invalid", virtual_ip)); - } - } - let relay = matches.opt_present("relay"); - - let cipher_model = match matches.opt_get::("model") { - Ok(model) => { - #[cfg(not(any(feature = "aes_gcm", feature = "server_encrypt")))] - { - if password.is_some() && model.is_none() { - return Err(anyhow::anyhow!("'--model ' undefined")); - } - model.unwrap_or(CipherModel::None) - } - #[cfg(any(feature = "aes_gcm", feature = "server_encrypt"))] - model.unwrap_or(CipherModel::AesGcm) - } - Err(e) => { - return Err(anyhow::anyhow!("'--model ' invalid,{}", e)); - } - }; - - let finger = matches.opt_present("finger"); - let punch_model = matches - .opt_get::("punch") - .unwrap() - .unwrap_or(PunchModel::All); - let use_channel_type = matches - .opt_get::("use-channel") - .unwrap() - .unwrap_or_else(|| { - if relay { - UseChannelType::Relay - } else { - UseChannelType::All - } - }); - - let ports = matches - .opt_get::("ports") - .unwrap_or(None) - .map(|v| v.split(",").map(|x| x.parse().unwrap_or(0)).collect()); - - let cmd = matches.opt_present("cmd"); - #[cfg(feature = "ip_proxy")] - #[cfg(feature = "integrated_tun")] - let no_proxy = matches.opt_present("no-proxy"); - let first_latency = matches.opt_present("first-latency"); - let packet_loss = matches - .opt_get::("packet-loss") - .expect("--packet-loss"); - let packet_delay = matches - .opt_get::("packet-delay") - .expect("--packet-delay") - .unwrap_or(0); - #[cfg(feature = "port_mapping")] - let port_mapping_list = matches.opt_strs("mapping"); - let vnt_mapping_list = matches.opt_strs("vnt-mapping"); - let local_dev: Option = matches.opt_get("local-dev").unwrap(); - - let disable_stats = matches.opt_present("disable-stats"); - let allow_wire_guard = matches.opt_present("allow-wg"); - let compressor = if let Some(compressor) = matches.opt_str("compressor").as_ref() { - Compressor::from_str(compressor) - .map_err(|e| anyhow!("{}", e)) - .unwrap() - } else { - Compressor::None - }; - let config = Config::new( - #[cfg(feature = "integrated_tun")] - #[cfg(target_os = "windows")] - false, - token, - device_id, - name, - server_address_str, - dns, - stun_server, - in_ip, - out_ip, - password, - mtu, - virtual_ip, - #[cfg(feature = "integrated_tun")] - #[cfg(feature = "ip_proxy")] - no_proxy, - server_encrypt, - cipher_model, - finger, - punch_model, - ports, - first_latency, - #[cfg(feature = "integrated_tun")] - device_name, - use_channel_type, - packet_loss, - packet_delay, - #[cfg(feature = "port_mapping")] - port_mapping_list, - compressor, - !disable_stats, - allow_wire_guard, - local_dev, - )?; - (config, vnt_mapping_list, cmd) - }; - println!("version {}", vnt::VNT_VERSION); - println!("Serial:{}", generated_serial_number::SERIAL_NUMBER); - log::info!( - "version:{},Serial:{}", - vnt::VNT_VERSION, - generated_serial_number::SERIAL_NUMBER - ); - Ok(Some((config, vnt_link_config, cmd))) -} - -fn get_description(key: &str, language: &str) -> String { - // 设置一个全局的映射来存储中英文对照 - let descriptions: HashMap<&str, (&str, &str)> = [ - ("-k ", ("使用相同的token,就能组建一个局域网络", "Use the same token to form a local network")), - ("-n ", ("给设备一个名字,便于区分不同设备,默认使用系统版本", "Give the device a name to distinguish it, defaults to system version")), - ("-d ", ("设备唯一标识符,不使用--ip参数时,服务端凭此参数分配虚拟ip,注意不能重复", "Device unique identifier, used by the server to allocate virtual IP when --ip parameter is not used, must be unique")), - ("-s ", ("注册和中继服务器地址,协议支持使用tcp://和ws://和wss://,默认为udp://", "Registration and relay server address, protocols support using tcp://, ws://, and wss://, default is udp://")), - ("-e ", ("stun服务器,用于探测NAT类型,可使用多个地址,如-e stun.miwifi.com -e turn.cloudflare.com", "STUN server for detecting NAT type, can specify multiple addresses, e.g., -e stun.miwifi.com -e turn.cloudflare.com")), - ("-i ", ("配置点对网(IP代理)时使用,-i 192.168.0.0/24,10.26.0.3表示允许接收网段192.168.0.0/24的数据并转发到10.26.0.3,可指定多个网段", "Used when configuring point-to-point network (IP proxy), -i 192.168.0.0/24,10.26.0.3 allows receiving data from subnet 192.168.0.0/24 and forwarding to 10.26.0.3, specify multiple subnets")), - ("-o ", ("配置点对网时使用,-o 192.168.0.0/24表示允许将数据转发到192.168.0.0/24,可指定多个网段", "Used when configuring point-to-point network, -o 192.168.0.0/24 allows forwarding data to 192.168.0.0/24, specify multiple subnets")), - ("-w ", ("使用该密码生成的密钥对客户端数据进行加密,并且服务端无法解密,使用相同密码的客户端才能通信", "Encrypt client data with keys generated by this password, server cannot decrypt, clients must use the same password to communicate")), - ("-W", ("加密当前客户端和服务端通信的数据,请留意服务端指纹是否正确", "Encrypt the data currently being communicated between the client and server, please pay attention to whether the server fingerprint is correct")), - ("-u ", ("自定义mtu(默认为1420)", "Customize MTU (default is 1420)")), - ("-f ", ("读取配置文件中的配置", "Read configuration from file")), - ("--ip ", ("指定虚拟ip,指定的ip不能和其他设备重复,必须有效并且在服务端所属网段下,默认情况由服务端分配", "Specify virtual IP, must be unique and valid within server subnet, by default allocated by server")), - ("--model ", ("加密模式(默认aes_gcm),可选值", "Encryption mode (default aes_gcm), options ")), - ("--finger", ("增加数据指纹校验,可增加安全性,如果服务端开启指纹校验,则客户端也必须开启", "Add data fingerprint verification for increased security, client must enable if server does")), - ("--punch ", ("取值ipv4/ipv6/ipv4-tcp/ipv4-udp/ipv6-tcp/ipv6-udp/all,ipv4表示仅使用ipv4打洞", "Values ipv4/ipv6/ipv4-tcp/ipv4-udp/ipv6-tcp/ipv6-udp/all, ipv4 for IPv4 hole punching only")), - ("--ports ", ("取值0~65535,指定本地监听的一组端口,默认监听两个随机端口,使用过多端口会增加网络负担", "Values 0~65535, specify a group of local listening ports, defaults to two random ports, using many ports increases network load")), - ("--cmd", ("开启交互式命令,使用此参数开启控制台输入", "Enable interactive command mode, use this parameter to enable console input")), - ("--no-proxy", ("关闭内置代理,如需点对网则需要配置网卡NAT转发", "Disable built-in proxy, configure network card NAT forwarding for point-to-point networking")), - ("--first-latency", ("优先低延迟的通道,默认情况优先使用p2p通道", "Prioritize low-latency channels, defaults to prioritizing p2p channel")), - ("--use-channel ", ("使用通道 relay/p2p/all,默认两者都使用", "Use channel relay/p2p/all, defaults to using both")), - ("--nic ", ("指定虚拟网卡名称", "Specify virtual network card name")), - ("--packet-loss <0>", ("模拟丢包,取值0~1之间的小数,程序会按设定的概率主动丢包,可用于模拟弱网", "Simulate packet loss, value between 0 and 1, program actively drops packets based on set probability, useful for simulating weak networks")), - ("--packet-delay <0>", ("模拟延迟,正整数,单位毫秒,程序将根据设定值延迟发送数据包,可用于模拟弱网", "Simulate latency, integer, in milliseconds (ms). The program will delay sending packets according to the set value and can be used to simulate weak networks")), - ("--dns ", ("DNS服务器地址,可使用多个dns,不指定时使用系统解析", "DNS server address, can specify multiple DNS servers, defaults to system resolution if not specified")), - ("--mapping ", ("端口映射,例如 --mapping udp:0.0.0.0:80-domain:80 映射目标是本地路由能访问的设备", "Port mapping, e.g., --mapping udp:0.0.0.0:80-domain:80 maps to a device accessible by local routing")), - ("--compressor-all ", ("启用压缩,可选值lz4/zstd<,level>,level为压缩级别,例如 --compressor lz4 或--compressor zstd,10", "Enable compression, options lz4/zstd<,level>, level is compression level, e.g., --compressor lz4 or --compressor zstd,10")), - ("--compressor-lz4 ", ("启用压缩,可选值lz4,例如 --compressor lz4", "Enable compression, option lz4, e.g., --compressor lz4")), - ("--compressor-zstd ", ("启用压缩,可选值zstd<,level>,level为压缩级别,例如 --compressor zstd,10", "Enable compression, options zstd<,level>, level is compression level, e.g., --compressor zstd,10")), - ("--vnt-mapping ", ("vnt地址映射,例如 --vnt-mapping tcp:80-10.26.0.10:80 映射目标是vnt网络或其子网中的设备", "VNT address mapping, e.g., --vnt-mapping tcp:80-10.26.0.10:80 maps to a device in VNT network or its subnet")), - ("--local-dev", ("本地出口网卡的名称", "name of local export network card")), - ("--disable-stats", ("关闭流量统计", "Disable traffic statistics")), - ("--allow-wg", ("允许接入WireGuard客户端", "Allow access to WireGuard client")), - ("--list", ("后台运行时,查看其他设备列表", "View list of other devices when running in background")), - ("--all", ("后台运行时,查看其他设备完整信息", "View complete information of other devices when running in background")), - ("--info", ("后台运行时,查看当前设备信息", "View information of current device when running in background")), - ("--route", ("后台运行时,查看数据转发路径", "View data forwarding path when running in background")), - ("--chart_a", ("后台运行时,查看所有IP的流量统计", "View traffic statistics of all IPs when running in background")), - ("--chart_b ", ("后台运行时,查看单个IP的历史流量", "View historical traffic of a single IP when running in background")), - ("--stop", ("停止后台运行", "Stop running in background")) - // ... 其他选项 - ] - .iter() - .cloned() - .collect(); - - if let Some(&(zh, en)) = descriptions.get(key) { - if language.starts_with("zh") { - return zh.to_string(); // 返回 String 类型 - } - // 默认返回英文 - return en.to_string(); // 返回 String 类型 - } - // 如果没有找到对应的键,则返回空字符串 - String::new() -} - -fn print_usage(program: &str, _opts: Options) { - // 获取系统语言 Locale::user_default().unwrap_or_else(|_| Locale::default()); - let language = get_locale().unwrap_or_else(|| String::from("en-US")); - println!("Usage: {} [options]", program); - println!("version:{}", vnt::VNT_VERSION); - println!("Serial:{}", generated_serial_number::SERIAL_NUMBER); - println!("Options:"); - println!( - " -k {}", - green(get_description("-k ", &language).to_string()) - ); - println!( - " -n {}", - get_description("-n ", &language) - ); - println!( - " -d {}", - get_description("-d ", &language) - ); - println!( - " -s {}", - get_description("-s ", &language) - ); - println!( - " -e {}", - get_description("-e ", &language) - ); - - println!( - " -i {}", - get_description("-i ", &language) - ); - println!( - " -o {}", - get_description("-o ", &language) - ); - println!( - " -w {}", - get_description("-w ", &language) - ); - #[cfg(feature = "server_encrypt")] - println!(" -W {}", get_description("-W", &language)); - println!( - " -u {}", - get_description("-u ", &language) - ); - #[cfg(feature = "file_config")] - println!( - " -f {}", - get_description("-f ", &language) - ); - - println!( - " --ip {}", - get_description("--ip ", &language) - ); - let mut enums = String::new(); - #[cfg(any(feature = "aes_gcm", feature = "server_encrypt"))] - enums.push_str("/aes_gcm"); - #[cfg(feature = "chacha20_poly1305")] - enums.push_str("/chacha20_poly1305/chacha20"); - #[cfg(feature = "aes_cbc")] - enums.push_str("/aes_cbc"); - #[cfg(feature = "aes_ecb")] - enums.push_str("/aes_ecb"); - #[cfg(feature = "sm4_cbc")] - enums.push_str("/sm4_cbc"); - enums.push_str("/xor"); - println!( - " --model {}{}", - get_description("--model ", &language), - &enums[1..] - ); - #[cfg(any( - feature = "aes_gcm", - feature = "chacha20_poly1305", - feature = "server_encrypt", - feature = "aes_cbc", - feature = "aes_ecb", - feature = "sm4_cbc" - ))] - println!( - " --finger {}", - get_description("--finger", &language) - ); - println!( - " --punch {}", - get_description("--punch ", &language) - ); - println!( - " --ports {}", - get_description("--ports ", &language) - ); - #[cfg(feature = "command")] - println!( - " --cmd {}", - get_description("--cmd", &language) - ); - #[cfg(feature = "ip_proxy")] - #[cfg(feature = "integrated_tun")] - println!( - " --no-proxy {}", - get_description("--no-proxy", &language) - ); - println!( - " --first-latency {}", - get_description("--first-latency", &language) - ); - println!( - " --use-channel {}", - get_description("--use-channel ", &language) - ); - #[cfg(feature = "integrated_tun")] - println!( - " --nic {}", - get_description("--nic ", &language) - ); - println!( - " --packet-loss <0> {}", - get_description("--packet-loss <0>", &language) - ); - println!( - " --packet-delay <0> {}", - get_description("--packet-delay <0>", &language) - ); - println!( - " --dns {}", - get_description("--dns ", &language) - ); - - #[cfg(feature = "port_mapping")] - println!( - " --mapping {}", - get_description("--mapping ", &language) - ); - - #[cfg(all(feature = "lz4", feature = "zstd"))] - println!( - " --compressor {}", - get_description("--compressor-all ", &language) - ); - #[cfg(feature = "lz4")] - #[cfg(not(feature = "zstd"))] - println!( - " --compressor {}", - get_description("--compressor-lz4 ", &language) - ); - #[cfg(feature = "zstd")] - #[cfg(not(feature = "lz4"))] - println!( - " --compressor {}", - get_description("--compressor-zstd ", &language) - ); - - #[cfg(not(feature = "integrated_tun"))] - println!( - " --vnt-mapping {}", - green(get_description("--vnt-mapping ", &language).to_string()) - ); - println!( - " --local-dev {}", - get_description("--local-dev", &language) - ); - println!( - " --disable-stats {}", - get_description("--disable-stats", &language) - ); - println!( - " --allow-wg {}", - get_description("--allow-wg", &language) - ); - println!(); - #[cfg(feature = "command")] - { - // #[cfg(not(feature = "integrated_tun"))] - // println!( - // " --add {}", - // yellow("后台运行时,添加VNT地址映射 用法同'--vnt-mapping'".to_string()) - // ); - println!( - " --list {}", - yellow(get_description("--list", &language).to_string()) - ); - println!( - " --all {}", - yellow(get_description("--all", &language).to_string()) - ); - println!( - " --info {}", - yellow(get_description("--info", &language).to_string()) - ); - println!( - " --route {}", - yellow(get_description("--route", &language).to_string()) - ); - println!( - " --chart_a {}", - yellow(get_description("--chart_a", &language).to_string()) - ); - println!( - " --chart_b {}", - yellow(get_description("--chart_b ", &language).to_string()) - ); - println!( - " --stop {}", - yellow(get_description("--stop", &language).to_string()) - ); - } - println!(" -h, --help display help information(显示帮助信息)"); -} - -fn green(str: String) -> impl std::fmt::Display { - style(str).green() -} - -#[cfg(feature = "command")] -fn yellow(str: String) -> impl std::fmt::Display { - style(str).yellow() -} diff --git a/common/src/command/client.rs b/common/src/command/client.rs deleted file mode 100644 index e6caab9..0000000 --- a/common/src/command/client.rs +++ /dev/null @@ -1,92 +0,0 @@ -use serde::Deserialize; -use std::io; -use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket}; -use std::str::FromStr; -use std::time::Duration; - -use crate::command::entity::{ChartA, ChartB, DeviceItem, Info, RouteItem}; - -pub struct CommandClient { - buf: Vec, - udp: UdpSocket, -} - -impl CommandClient { - pub fn new() -> io::Result { - let port = read_command_port().unwrap_or_else(|e| { - log::warn!("read_command_port:{:?}", e); - 39271 - }); - let udp = UdpSocket::bind("127.0.0.1:0")?; - udp.set_read_timeout(Some(Duration::from_secs(5)))?; - udp.connect(SocketAddr::V4(SocketAddrV4::new( - Ipv4Addr::new(127, 0, 0, 1), - port, - )))?; - Ok(Self { - udp, - buf: vec![0; 65536 * 8], - }) - } -} -fn read_command_port() -> io::Result { - let path_buf = crate::cli::app_home()?.join("command-port"); - let port = std::fs::read_to_string(path_buf)?; - match u16::from_str(&port) { - Ok(port) => Ok(port), - Err(_) => { - return Err(io::Error::new( - io::ErrorKind::Other, - "'command-port' file error", - )); - } - } -} - -impl CommandClient { - pub fn list(&mut self) -> io::Result> { - self.send_cmd(b"list") - } - pub fn route(&mut self) -> io::Result> { - self.send_cmd(b"route") - } - pub fn info(&mut self) -> io::Result { - self.send_cmd(b"info") - } - pub fn chart_a(&mut self) -> io::Result { - self.send_cmd(b"chart_a") - } - pub fn chart_b(&mut self, input: &str) -> io::Result { - let cmd = if input.is_empty() { - "chart_b".to_string() - } else { - format!("chart_b:{}", input) - }; - self.send_cmd(cmd.as_bytes()) - } - fn send_cmd<'a, V: Deserialize<'a>>(&'a mut self, cmd: &[u8]) -> io::Result { - self.udp.send(cmd)?; - let len = self.udp.recv(&mut self.buf)?; - match serde_yaml::from_slice::(&self.buf[..len]) { - Ok(val) => Ok(val), - Err(e) => { - log::error!( - "send_cmd {:?} {:?},{:?}", - std::str::from_utf8(cmd), - std::str::from_utf8(&self.buf[..len]), - e - ); - Err(io::Error::new( - io::ErrorKind::Other, - format!("data error {:?} buf_len={}", e, len), - )) - } - } - } - pub fn stop(&self) -> io::Result { - self.udp.send(b"stop")?; - let mut buf = [0; 10240]; - let len = self.udp.recv(&mut buf)?; - Ok(String::from_utf8(buf[..len].to_vec()).unwrap()) - } -} diff --git a/common/src/command/entity.rs b/common/src/command/entity.rs deleted file mode 100644 index 9770897..0000000 --- a/common/src/command/entity.rs +++ /dev/null @@ -1,68 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::net::{Ipv4Addr, SocketAddr}; - -#[derive(Serialize, Deserialize, Debug)] -pub struct Info { - pub name: String, - pub virtual_ip: String, - pub virtual_gateway: String, - pub virtual_netmask: String, - pub connect_status: String, - pub relay_server: String, - pub nat_type: String, - pub public_ips: String, - pub local_addr: String, - pub ipv6_addr: String, - pub port_mapping_list: Vec<(bool, SocketAddr, String)>, - pub in_ips: Vec<(u32, u32, Ipv4Addr)>, - pub out_ips: Vec<(u32, u32)>, - pub udp_listen_addr: Vec, - pub tcp_listen_addr: String, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct RouteItem { - pub destination: String, - pub next_hop: String, - pub metric: String, - pub rt: String, - pub interface: String, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct DeviceItem { - pub name: String, - pub virtual_ip: String, - pub nat_type: String, - pub public_ips: String, - pub local_ip: String, - pub ipv6: String, - pub nat_traversal_type: String, - pub rt: String, - pub status: String, - pub client_secret: bool, - pub client_secret_hash: Vec, - pub current_client_secret: bool, - pub current_client_secret_hash: Vec, - pub wire_guard: bool, -} - -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct ChartA { - pub disable_stats: bool, - pub up_total: u64, - pub down_total: u64, - pub up_map: HashMap, - pub down_map: HashMap, -} - -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct ChartB { - pub disable_stats: bool, - pub ip: Option, - pub up_total: u64, - pub up_list: Vec, - pub down_total: u64, - pub down_list: Vec, -} diff --git a/common/src/command/mod.rs b/common/src/command/mod.rs deleted file mode 100644 index 2ffa8bc..0000000 --- a/common/src/command/mod.rs +++ /dev/null @@ -1,353 +0,0 @@ -use std::collections::HashSet; -use std::io; -use std::net::Ipv4Addr; -use vnt::channel::ConnectProtocol; -use vnt::core::Vnt; - -use crate::command::entity::{ChartA, ChartB, DeviceItem, Info, RouteItem}; -use crate::console_out; - -pub mod client; -pub mod entity; -pub mod server; - -pub enum CommandEnum { - Route, - List, - All, - Info, - ChartA, - ChartB(String), - Stop, -} - -pub fn command_str(cmd: &str, vnt: &Vnt) -> bool { - if cmd.is_empty() { - return false; - } - let cmd = cmd.to_lowercase(); - let cmd = cmd.trim(); - match cmd { - "list" => { - let list = command_list(&vnt); - console_out::console_device_list(list); - } - "info" => { - let info = command_info(&vnt); - console_out::console_info(info); - } - "route" => { - let route = command_route(&vnt); - console_out::console_route_table(route); - } - "all" => { - let list = command_list(&vnt); - console_out::console_device_list_all(list); - } - "chart_a" => { - let chart = command_chart_a(&vnt); - console_out::console_chart_a(chart); - } - "stop" => { - let _ = vnt.stop(); - return false; - } - _ => {} - } - if let Some(ip) = cmd.strip_prefix("chart_b") { - let chart = if ip.is_empty() { - command_chart_b(&vnt, &vnt.current_device().virtual_gateway.to_string()) - } else { - command_chart_b(&vnt, &ip[1..]) - }; - console_out::console_chart_b(chart); - } - println!(); - return true; -} - -pub fn command(cmd: CommandEnum) { - if let Err(e) = command_(cmd) { - println!("cmd: {:?}", e); - } -} - -fn command_(cmd: CommandEnum) -> io::Result<()> { - let mut command_client = client::CommandClient::new()?; - match cmd { - CommandEnum::Route => { - let list = command_client.route()?; - console_out::console_route_table(list); - } - CommandEnum::List => { - let list = command_client.list()?; - console_out::console_device_list(list); - } - CommandEnum::All => { - let list = command_client.list()?; - console_out::console_device_list_all(list); - } - CommandEnum::Info => { - let info = command_client.info()?; - console_out::console_info(info); - } - CommandEnum::ChartA => { - let chart = command_client.chart_a()?; - console_out::console_chart_a(chart); - } - CommandEnum::ChartB(input) => { - let chart = command_client.chart_b(&input)?; - console_out::console_chart_b(chart); - } - CommandEnum::Stop => { - command_client.stop()?; - } - } - Ok(()) -} - -pub fn command_route(vnt: &Vnt) -> Vec { - let route_table = vnt.route_table(); - let server_addr = vnt.config().server_address_str.clone(); - let mut route_list = Vec::with_capacity(route_table.len()); - for (destination, routes) in route_table { - for route in routes { - let next_hop = vnt - .route_key(&route.route_key()) - .map_or(String::new(), |v| v.to_string()); - let metric = route.metric.to_string(); - let rt = if route.rt < 0 { - "".to_string() - } else { - route.rt.to_string() - }; - let interface = match route.protocol { - ConnectProtocol::UDP => route.addr.to_string(), - ConnectProtocol::TCP => { - format!("tcp@{}", route.addr) - } - ConnectProtocol::WS | ConnectProtocol::WSS => server_addr.clone(), - }; - - let item = RouteItem { - destination: destination.to_string(), - next_hop, - metric, - rt, - interface, - }; - route_list.push(item); - } - } - route_list -} - -pub fn command_list(vnt: &Vnt) -> Vec { - let info = vnt.current_device(); - let device_list = vnt.device_list(); - let mut list = Vec::new(); - let current_client_secret = vnt.client_encrypt(); - let client_encrypt_hash = vnt.client_encrypt_hash().unwrap_or(&[]); - for peer in device_list { - let name = peer.name; - let virtual_ip = peer.virtual_ip.to_string(); - let (nat_type, public_ips, local_ip, ipv6) = - if let Some(nat_info) = vnt.peer_nat_info(&peer.virtual_ip) { - let nat_type = format!("{:?}", nat_info.nat_type); - let public_ips: Vec = - nat_info.public_ips.iter().map(|v| v.to_string()).collect(); - let public_ips = public_ips.join(","); - let local_ip = nat_info - .local_ipv4() - .map(|v| v.to_string()) - .unwrap_or("None".to_string()); - let ipv6 = nat_info - .ipv6() - .map(|v| v.to_string()) - .unwrap_or("None".to_string()); - (nat_type, public_ips, local_ip, ipv6) - } else { - ( - "".to_string(), - "".to_string(), - "".to_string(), - "".to_string(), - ) - }; - let (nat_traversal_type, rt) = if let Some(route) = vnt.route(&peer.virtual_ip) { - let nat_traversal_type = if route.metric == 1 { - if route.protocol.is_base_tcp() { - "tcp-p2p" - } else { - "p2p" - } - } else { - let next_hop = vnt.route_key(&route.route_key()); - if let Some(next_hop) = next_hop { - if info.is_gateway(&next_hop) { - "server-relay" - } else { - "client-relay" - } - } else { - "server-relay" - } - } - .to_string(); - let rt = if route.rt < 0 { - "".to_string() - } else { - route.rt.to_string() - }; - (nat_traversal_type, rt) - } else { - ("relay".to_string(), "".to_string()) - }; - let status = format!("{:?}", peer.status); - let client_secret = peer.client_secret; - let item = DeviceItem { - name, - virtual_ip, - nat_type, - public_ips, - local_ip, - ipv6, - nat_traversal_type, - rt, - status, - client_secret, - client_secret_hash: peer.client_secret_hash, - current_client_secret, - current_client_secret_hash: client_encrypt_hash.to_vec(), - wire_guard: peer.wireguard, - }; - list.push(item); - } - list -} - -pub fn command_info(vnt: &Vnt) -> Info { - let config = vnt.config(); - let current_device = vnt.current_device(); - let nat_info = vnt.nat_info(); - let name = vnt.name().to_string(); - let virtual_ip = current_device.virtual_ip().to_string(); - let virtual_gateway = current_device.virtual_gateway().to_string(); - let virtual_netmask = current_device.virtual_netmask.to_string(); - let connect_status = format!("{:?}", vnt.connection_status()); - let relay_server = if current_device.connect_server.port() == 0 { - config.server_address_str.clone() - } else { - current_device.connect_server.to_string() - }; - let nat_type = format!("{:?}", nat_info.nat_type); - let public_ips: Vec = nat_info.public_ips.iter().map(|v| v.to_string()).collect(); - let public_ips = public_ips.join(","); - let local_addr = nat_info - .local_ipv4() - .map(|v| v.to_string()) - .unwrap_or("None".to_string()); - let ipv6_addr = nat_info - .ipv6() - .map(|v| v.to_string()) - .unwrap_or("None".to_string()); - #[cfg(feature = "port_mapping")] - let port_mapping_list = vnt.config().port_mapping_list.clone(); - #[cfg(not(feature = "port_mapping"))] - let port_mapping_list = vec![]; - let in_ips = vnt.config().in_ips.clone(); - let out_ips = vnt.config().out_ips.clone(); - let udp_listen_addr = nat_info - .udp_ports - .iter() - .map(|port| format!("0.0.0.0:{}", port)) - .collect(); - let tcp_listen_addr = format!("0.0.0.0:{}", nat_info.tcp_port); - Info { - name, - virtual_ip, - virtual_gateway, - virtual_netmask, - connect_status, - relay_server, - nat_type, - public_ips, - local_addr, - ipv6_addr, - port_mapping_list, - in_ips, - out_ips, - udp_listen_addr, - tcp_listen_addr, - } -} - -pub fn command_chart_a(vnt: &Vnt) -> ChartA { - let disable_stats = !vnt.config().enable_traffic; - if disable_stats { - let mut chart = ChartA::default(); - chart.disable_stats = true; - return chart; - } - let (up_total, up_map) = vnt.up_stream_all().unwrap_or_default(); - let (down_total, down_map) = vnt.down_stream_all().unwrap_or_default(); - ChartA { - disable_stats, - up_total, - down_total, - up_map, - down_map, - } -} - -pub fn command_chart_b(vnt: &Vnt, input_str: &str) -> ChartB { - let disable_stats = !vnt.config().enable_traffic; - if disable_stats { - let mut chart = ChartB::default(); - chart.disable_stats = true; - return chart; - } - let (_, up_map) = vnt.up_stream_history().unwrap_or_default(); - let (_, down_map) = vnt.down_stream_history().unwrap_or_default(); - let up_keys: HashSet<_> = up_map.keys().cloned().collect(); - let down_keys: HashSet<_> = down_map.keys().cloned().collect(); - let mut keys: Vec = up_keys.union(&down_keys).cloned().collect(); - keys.sort(); - if let Some(ip) = find_matching_ipv4_address(input_str, &keys) { - let (up_total, up_list) = up_map.get(&ip).cloned().unwrap_or_default(); - let (down_total, down_list) = down_map.get(&ip).cloned().unwrap_or_default(); - ChartB { - disable_stats, - ip: Some(ip), - up_total, - up_list, - down_total, - down_list, - } - } else { - ChartB::default() - } -} - -fn match_from_end(input_str: &str, ip: &str) -> bool { - let mut input_chars = input_str.chars().rev(); - let mut ip_chars = ip.chars().rev(); - - while let (Some(ic), Some(pc)) = (input_chars.next(), ip_chars.next()) { - if ic != pc { - return false; - } - } - - input_chars.next().is_none() // Ensure all input characters matched -} - -fn find_matching_ipv4_address(input_str: &str, ip_addresses: &[Ipv4Addr]) -> Option { - for &ip in ip_addresses { - let ip_str = ip.to_string(); - if match_from_end(input_str, &ip_str) { - return Some(ip); - } - } - None -} diff --git a/common/src/command/server.rs b/common/src/command/server.rs deleted file mode 100644 index 38a7c14..0000000 --- a/common/src/command/server.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::command::command_chart_b; -use std::io; -use std::io::Write; -use std::net::UdpSocket; -use vnt::core::Vnt; - -pub struct CommandServer {} - -impl CommandServer { - pub fn new() -> Self { - Self {} - } -} - -impl CommandServer { - pub fn start(self, vnt: Vnt) -> io::Result<()> { - let udp = if let Ok(udp) = UdpSocket::bind("127.0.0.1:39271") { - udp - } else { - UdpSocket::bind("127.0.0.1:0")? - }; - let addr = udp.local_addr()?; - log::info!("启动后台cmd:{:?}", addr); - if let Err(e) = save_port(addr.port()) { - log::warn!("保存后台命令端口失败:{:?}", e); - } - - let mut buf = [0u8; 64]; - loop { - let (len, addr) = udp.recv_from(&mut buf)?; - match std::str::from_utf8(&buf[..len]) { - Ok(cmd) => { - if let Ok(out) = command(cmd, &vnt) { - if let Err(e) = udp.send_to(out.as_bytes(), addr) { - log::warn!("cmd={},err={:?}", cmd, e); - } - if "stopped" == &out { - break; - } - } - } - Err(e) => { - log::warn!("{:?}", e); - } - } - } - Ok(()) - } -} -fn save_port(port: u16) -> io::Result<()> { - let path_buf = crate::cli::app_home()?.join("command-port"); - let mut file = std::fs::File::create(path_buf)?; - file.write_all(port.to_string().as_bytes())?; - file.sync_all() -} - -fn command(cmd: &str, vnt: &Vnt) -> io::Result { - let cmd = cmd.trim(); - let out_str = match cmd { - "route" => serde_yaml::to_string(&crate::command::command_route(vnt)) - .unwrap_or_else(|e| format!("error {:?}", e)), - "list" => serde_yaml::to_string(&crate::command::command_list(vnt)) - .unwrap_or_else(|e| format!("error {:?}", e)), - "info" => serde_yaml::to_string(&crate::command::command_info(vnt)) - .unwrap_or_else(|e| format!("error {:?}", e)), - "chart_a" => serde_yaml::to_string(&crate::command::command_chart_a(vnt)) - .unwrap_or_else(|e| format!("error {:?}", e)), - "stop" => { - vnt.stop(); - "stopped".to_string() - } - _ => { - if let Some(ip) = cmd.strip_prefix("chart_b") { - let chart = if ip.is_empty() { - command_chart_b(&vnt, &vnt.current_device().virtual_gateway.to_string()) - } else { - command_chart_b(&vnt, &ip[1..]) - }; - serde_yaml::to_string(&chart).unwrap_or_else(|e| format!("error {:?}", e)) - } else { - format!( - "command '{}' not found. Try to enter: 'route'/'list'/'stop' \n", - cmd - ) - } - } - }; - Ok(out_str) -} diff --git a/common/src/config/file_config.rs b/common/src/config/file_config.rs deleted file mode 100644 index a86163e..0000000 --- a/common/src/config/file_config.rs +++ /dev/null @@ -1,190 +0,0 @@ -use anyhow::anyhow; -use std::net::Ipv4Addr; -use std::str::FromStr; - -use crate::config::get_device_id; -use crate::{args_parse, config}; -use serde::{Deserialize, Serialize}; -use vnt::channel::punch::PunchModel; -use vnt::channel::UseChannelType; -use vnt::cipher::CipherModel; -use vnt::compression::Compressor; -use vnt::core::Config; - -#[derive(Serialize, Deserialize, Debug)] -#[serde(default)] -pub struct FileConfig { - #[cfg(target_os = "windows")] - pub tap: bool, - pub token: String, - pub device_id: String, - pub name: String, - pub server_address: String, - pub stun_server: Vec, - pub dns: Vec, - pub in_ips: Vec, - pub out_ips: Vec, - pub password: Option, - pub mtu: Option, - pub tcp: bool, - pub ip: Option, - pub use_channel: String, - #[cfg(feature = "ip_proxy")] - pub no_proxy: bool, - pub server_encrypt: bool, - pub cipher_model: Option, - pub finger: bool, - pub punch_model: String, - pub ports: Option>, - pub cmd: bool, - pub first_latency: bool, - pub device_name: Option, - pub packet_loss: Option, - pub packet_delay: u32, - #[cfg(feature = "port_mapping")] - pub mapping: Vec, - pub compressor: Option, - pub vnt_mapping: Vec, - pub disable_stats: bool, - // 允许传递wg流量 - pub allow_wire_guard: bool, - pub local_dev: Option, -} - -impl Default for FileConfig { - fn default() -> Self { - let mut stun_server = Vec::new(); - for x in config::PUB_STUN { - stun_server.push(x.to_string()); - } - Self { - #[cfg(target_os = "windows")] - tap: false, - token: "".to_string(), - device_id: get_device_id(), - name: gethostname::gethostname() - .to_str() - .unwrap_or("UnknownName") - .to_string(), - server_address: "nat1.wherewego.top:29872".to_string(), - stun_server, - dns: vec![], - in_ips: vec![], - out_ips: vec![], - password: None, - mtu: None, - tcp: false, - ip: None, - use_channel: "all".to_string(), - #[cfg(feature = "ip_proxy")] - no_proxy: false, - server_encrypt: false, - cipher_model: None, - finger: false, - punch_model: "all".to_string(), - ports: None, - cmd: false, - first_latency: false, - device_name: None, - packet_loss: None, - packet_delay: 0, - #[cfg(feature = "port_mapping")] - mapping: vec![], - compressor: None, - vnt_mapping: vec![], - disable_stats: false, - allow_wire_guard: false, - local_dev: None, - } - } -} - -pub fn read_config(file_path: &str) -> anyhow::Result<(Config, Vec, bool)> { - let conf = std::fs::read_to_string(file_path)?; - let file_conf = match serde_yaml::from_str::(&conf) { - Ok(val) => val, - Err(e) => { - log::error!("serde_yaml::from_str {:?}", e); - return Err(anyhow!("serde_yaml::from_str {:?}", e)); - } - }; - if file_conf.token.is_empty() { - return Err(anyhow!("token is_empty")); - } - - let in_ips = match args_parse::ips_parse(&file_conf.in_ips) { - Ok(in_ips) => in_ips, - Err(e) => { - return Err(anyhow!("in_ips {:?} error:{}", &file_conf.in_ips, e)); - } - }; - let out_ips = match args_parse::out_ips_parse(&file_conf.out_ips) { - Ok(out_ips) => out_ips, - Err(e) => { - return Err(anyhow!("out_ips {:?} error:{}", &file_conf.out_ips, e)); - } - }; - let virtual_ip = match file_conf.ip.clone().map(|v| Ipv4Addr::from_str(&v)) { - None => None, - Some(r) => Some(r.map_err(|e| anyhow!("ip {:?} error:{}", &file_conf.ip, e))?), - }; - let cipher_model = if let Some(v) = file_conf.cipher_model { - CipherModel::from_str(&v).map_err(|e| anyhow!("{}", e))? - } else { - #[cfg(not(any(feature = "aes_gcm", feature = "server_encrypt")))] - if file_conf.password.is_some() { - Err(anyhow!("cipher_model undefined"))? - } else { - CipherModel::None - } - #[cfg(any(feature = "aes_gcm", feature = "server_encrypt"))] - CipherModel::AesGcm - }; - - let punch_model = PunchModel::from_str(&file_conf.punch_model).map_err(|e| anyhow!("{}", e))?; - let use_channel_type = - UseChannelType::from_str(&file_conf.use_channel).map_err(|e| anyhow!("{}", e))?; - let compressor = if let Some(compressor) = file_conf.compressor.as_ref() { - Compressor::from_str(compressor).map_err(|e| anyhow!("{}", e))? - } else { - Compressor::None - }; - let config = Config::new( - #[cfg(target_os = "windows")] - #[cfg(feature = "integrated_tun")] - file_conf.tap, - file_conf.token, - file_conf.device_id, - file_conf.name, - file_conf.server_address, - file_conf.dns, - file_conf.stun_server, - in_ips, - out_ips, - file_conf.password, - file_conf.mtu, - virtual_ip, - #[cfg(feature = "integrated_tun")] - #[cfg(feature = "ip_proxy")] - file_conf.no_proxy, - file_conf.server_encrypt, - cipher_model, - file_conf.finger, - punch_model, - file_conf.ports, - file_conf.first_latency, - #[cfg(feature = "integrated_tun")] - file_conf.device_name, - use_channel_type, - file_conf.packet_loss, - file_conf.packet_delay, - #[cfg(feature = "port_mapping")] - file_conf.mapping, - compressor, - !file_conf.disable_stats, - file_conf.allow_wire_guard, - file_conf.local_dev, - )?; - - Ok((config, file_conf.vnt_mapping, file_conf.cmd)) -} diff --git a/common/src/config/mod.rs b/common/src/config/mod.rs deleted file mode 100644 index 8810c92..0000000 --- a/common/src/config/mod.rs +++ /dev/null @@ -1,38 +0,0 @@ -pub const PUB_STUN: [&'static str; 4] = [ - "stun.miwifi.com", - "stun.chat.bilibili.com", - "stun.hitv.com", - "stun.cdnbye.com", -]; -#[cfg(feature = "file_config")] -mod file_config; - -use crate::identifier; -#[cfg(feature = "file_config")] -pub use file_config::read_config; - -#[cfg(not(feature = "file_config"))] -pub fn read_config(_file_path: &str) -> anyhow::Result<(vnt::core::Config, Vec, bool)> { - unimplemented!() -} - -pub fn get_device_id() -> String { - if let Some(id) = identifier::get_unique_identifier() { - id - } else { - let path_buf = match crate::cli::app_home() { - Ok(path_buf) => path_buf.join("device-id"), - Err(e) => { - log::warn!("{:?}", e); - return String::new(); - } - }; - if let Ok(id) = std::fs::read_to_string(path_buf.as_path()) { - id - } else { - let id = uuid::Uuid::new_v4().to_string(); - let _ = std::fs::write(path_buf, &id); - id - } - } -} diff --git a/common/src/console_out/mod.rs b/common/src/console_out/mod.rs deleted file mode 100644 index 3a3bc7c..0000000 --- a/common/src/console_out/mod.rs +++ /dev/null @@ -1,366 +0,0 @@ -use console::{style, Style}; -use std::collections::HashSet; -use std::net::Ipv4Addr; - -use crate::command::entity::{ChartA, ChartB, DeviceItem, Info, RouteItem}; - -pub mod table; - -pub fn console_info(status: Info) { - println!("Name: {}", style(status.name).green()); - println!("Virtual ip: {}", style(status.virtual_ip).green()); - println!("Virtual gateway: {}", style(status.virtual_gateway).green()); - println!("Virtual netmask: {}", style(status.virtual_netmask).green()); - if status.connect_status.eq_ignore_ascii_case("Connected") { - println!( - "Connection status: {}", - style(status.connect_status).green() - ); - } else { - println!("Connection status: {}", style(status.connect_status).red()); - } - - println!("NAT type: {}", style(status.nat_type).green()); - println!("Relay server: {}", style(status.relay_server).green()); - println!( - "Udp listen: {}", - style(status.udp_listen_addr.join(", ")).green() - ); - println!("Tcp listen: {}", style(status.tcp_listen_addr).green()); - println!("Public ips: {}", style(status.public_ips).green()); - println!("Local addr: {}", style(status.local_addr).green()); - println!("IPv6: {}", style(status.ipv6_addr).green()); - - if !status.port_mapping_list.is_empty() { - println!("------------------------------------------"); - println!("Port mapping {}", status.port_mapping_list.len()); - for (is_tcp, addr, dest) in status.port_mapping_list { - if is_tcp { - println!(" TCP: {} -> {}", addr, dest) - } else { - println!(" UDP: {} -> {}", addr, dest) - } - } - } - if !status.in_ips.is_empty() || !status.out_ips.is_empty() { - println!("------------------------------------------"); - } - if !status.in_ips.is_empty() { - println!("IP forwarding {}", status.in_ips.len()); - for (dest, mask, ip) in status.in_ips { - println!( - " -- {} --> {}/{}", - ip, - Ipv4Addr::from(dest), - mask.count_ones() - ) - } - } - if !status.out_ips.is_empty() { - println!("Allows network {}", status.out_ips.len()); - for (dest, mask) in status.out_ips { - println!(" {}/{}", Ipv4Addr::from(dest), mask.count_ones()) - } - } -} - -fn convert(num: u64) -> String { - let gigabytes = num / (1024 * 1024 * 1024); - let remaining_bytes = num % (1024 * 1024 * 1024); - let megabytes = remaining_bytes / (1024 * 1024); - let remaining_bytes = remaining_bytes % (1024 * 1024); - let kilobytes = remaining_bytes / 1024; - let remaining_bytes = remaining_bytes % 1024; - let mut s = String::new(); - if gigabytes > 0 { - s.push_str(&format!("{} GB ", gigabytes)); - } - if megabytes > 0 { - s.push_str(&format!("{} MB ", megabytes)); - } - if kilobytes > 0 { - s.push_str(&format!("{} KB ", kilobytes)); - } - if remaining_bytes > 0 { - s.push_str(&format!("{} bytes", remaining_bytes)); - } - s -} - -pub fn console_route_table(mut list: Vec) { - if list.is_empty() { - println!("No route found"); - return; - } - list.sort_by(|t1, t2| t1.destination.cmp(&t2.destination)); - let mut out_list = Vec::with_capacity(list.len()); - - out_list.push(vec![ - ("Destination".to_string(), Style::new()), - ("Next Hop".to_string(), Style::new()), - ("Metric".to_string(), Style::new()), - ("Rt".to_string(), Style::new()), - ("Interface".to_string(), Style::new()), - ]); - for item in list { - out_list.push(vec![ - (item.destination, Style::new().green()), - (item.next_hop, Style::new().green()), - (item.metric, Style::new().green()), - (item.rt, Style::new().green()), - (item.interface, Style::new().green()), - ]); - } - - table::println_table(out_list) -} - -pub fn console_device_list(mut list: Vec) { - if list.is_empty() { - println!("No other devices found"); - return; - } - list.sort_by(|t1, t2| t1.virtual_ip.cmp(&t2.virtual_ip)); - list.sort_by(|t1, t2| t1.status.cmp(&t2.status)); - let mut out_list = Vec::with_capacity(list.len()); - //表头 - out_list.push(vec![ - ("Name".to_string(), Style::new()), - ("Virtual Ip".to_string(), Style::new()), - ("Status".to_string(), Style::new()), - ("P2P/Relay".to_string(), Style::new()), - ("Rt".to_string(), Style::new()), - ]); - for item in list { - let name = if item.wire_guard { - format!("{}(wg)", item.name) - } else { - item.name - }; - if &item.status == "Online" { - if !item.wire_guard - && (item.client_secret != item.current_client_secret - || (!item.current_client_secret_hash.is_empty() - && !item.client_secret_hash.is_empty() - && item.current_client_secret_hash != item.client_secret_hash)) - { - //加密状态不一致,无法通信的 - out_list.push(vec![ - (name, Style::new().red()), - (item.virtual_ip, Style::new().red()), - (item.status, Style::new().red()), - ("Mismatch".to_string(), Style::new().red()), - ("".to_string(), Style::new().red()), - ]); - } else { - if item.nat_traversal_type.contains("p2p") { - out_list.push(vec![ - (name, Style::new().green()), - (item.virtual_ip, Style::new().green()), - (item.status, Style::new().green()), - (item.nat_traversal_type, Style::new().green()), - (item.rt, Style::new().green()), - ]); - } else { - out_list.push(vec![ - (name, Style::new().yellow()), - (item.virtual_ip, Style::new().yellow()), - (item.status, Style::new().yellow()), - (item.nat_traversal_type, Style::new().yellow()), - (item.rt, Style::new().yellow()), - ]); - } - } - } else { - out_list.push(vec![ - (name, Style::new().color256(102)), - (item.virtual_ip, Style::new().color256(102)), - (item.status, Style::new().color256(102)), - ("".to_string(), Style::new().color256(102)), - ("".to_string(), Style::new().color256(102)), - ]); - } - } - table::println_table(out_list) -} - -pub fn console_device_list_all(mut list: Vec) { - if list.is_empty() { - println!("No other devices found"); - return; - } - list.sort_by(|t1, t2| t1.virtual_ip.cmp(&t2.virtual_ip)); - list.sort_by(|t1, t2| t1.status.cmp(&t2.status)); - let mut out_list = Vec::with_capacity(list.len()); - //表头 - out_list.push(vec![ - ("Name".to_string(), Style::new()), - ("Virtual Ip".to_string(), Style::new()), - ("Status".to_string(), Style::new()), - ("P2P/Relay".to_string(), Style::new()), - ("Rt".to_string(), Style::new()), - ("NAT Type".to_string(), Style::new()), - ("Public Ips".to_string(), Style::new()), - ("Local Ip".to_string(), Style::new()), - ("IPv6".to_string(), Style::new()), - ]); - for item in list { - if &item.status == "Online" { - if &item.nat_traversal_type == "p2p" { - out_list.push(vec![ - (item.name, Style::new().green()), - (item.virtual_ip, Style::new().green()), - (item.status, Style::new().green()), - (item.nat_traversal_type, Style::new().green()), - (item.rt, Style::new().green()), - (item.nat_type, Style::new().green()), - (item.public_ips, Style::new().green()), - (item.local_ip, Style::new().green()), - (item.ipv6, Style::new().green()), - ]); - } else { - out_list.push(vec![ - (item.name, Style::new().yellow()), - (item.virtual_ip, Style::new().yellow()), - (item.status, Style::new().yellow()), - (item.nat_traversal_type, Style::new().yellow()), - (item.rt, Style::new().yellow()), - (item.nat_type, Style::new().yellow()), - (item.public_ips, Style::new().yellow()), - (item.local_ip, Style::new().yellow()), - (item.ipv6, Style::new().yellow()), - ]); - } - } else { - out_list.push(vec![ - (item.name, Style::new().color256(102)), - (item.virtual_ip, Style::new().color256(102)), - (item.status, Style::new().color256(102)), - ("".to_string(), Style::new().color256(102)), - ("".to_string(), Style::new().color256(102)), - ("".to_string(), Style::new().color256(102)), - ("".to_string(), Style::new().color256(102)), - ("".to_string(), Style::new().color256(102)), - ("".to_string(), Style::new().color256(102)), - ]); - } - } - table::println_table(out_list) -} - -pub fn console_chart_a(chart_a: ChartA) { - if chart_a.disable_stats { - println!("Traffic statistics not enabled"); - return; - } - println!(); - println!("-----------------------------------------------------------------"); - println!( - "Upload total = {}", - style(convert(chart_a.up_total)).green() - ); - println!( - "Download total = {}", - style(convert(chart_a.down_total)).green() - ); - println!("-----------------------------------------------------------------"); - let up_keys: HashSet<_> = chart_a.up_map.keys().cloned().collect(); - let down_keys: HashSet<_> = chart_a.down_map.keys().cloned().collect(); - let mut keys: Vec = up_keys.union(&down_keys).cloned().collect(); - // 排序 - keys.sort(); - - // 找到最大的值,用于缩放条形图长度 - let up_max_value = *chart_a.up_map.values().max().unwrap_or(&0); - let down_max_value = *chart_a.down_map.values().max().unwrap_or(&0); - let max_value = up_max_value.max(down_max_value); - let max_value = max_value.max(1); - let max_height = 50; - // 打印条形图 - for key in &keys { - if let Some(&value) = chart_a.up_map.get(key) { - let bar = "█".repeat(((value as f64 / max_value as f64) * max_height as f64) as usize); - println!( - "{:<10} | {} upload {}", - key, - bar, - style(convert(value)).green() - ); - } - if let Some(&value) = chart_a.down_map.get(key) { - let bar = "█".repeat(((value as f64 / max_value as f64) * max_height as f64) as usize); - println!( - "{:<10} | {} download {}", - key, - bar, - style(convert(value)).green() - ); - } - println!("-"); - } -} - -pub fn console_chart_b(chart_b: ChartB) { - if chart_b.disable_stats { - println!("Traffic statistics not enabled"); - return; - } - let ip = if let Some(ip) = chart_b.ip { - ip - } else { - println!("Ip: None"); - return; - }; - println!("---------------------------- upload ----------------------------"); - println!("IP: {}", ip); - println!("Upload total: {}", style(convert(chart_b.up_total)).green()); - println!( - "Max: {}", - style(convert( - chart_b - .up_list - .iter() - .max() - .cloned() - .map_or(0, |v| v as u64) - )) - .green() - ); - console_chart_b_list(chart_b.up_list); - println!("---------------------------- download ----------------------------"); - println!("IP: {}", ip); - println!( - "Download total: {}", - style(convert(chart_b.down_total)).green() - ); - println!( - "Max: {}", - style(convert( - chart_b - .down_list - .iter() - .max() - .cloned() - .map_or(0, |v| v as u64) - )) - .green() - ); - console_chart_b_list(chart_b.down_list); -} -fn console_chart_b_list(list: Vec) { - let max_value = *list.iter().max().unwrap_or(&0); - let max_value = max_value.max(1); - let max_height = max_value.min(20); - // 遍历从最大高度到0 - for i in (0..=max_height).rev() { - for &value in &list { - let scaled_value = (value as f64 / max_value as f64 * max_height as f64) as usize; - if scaled_value >= i { - print!("█"); - } else { - print!(" "); - } - } - println!(); - } -} diff --git a/common/src/console_out/table.rs b/common/src/console_out/table.rs deleted file mode 100644 index aacc006..0000000 --- a/common/src/console_out/table.rs +++ /dev/null @@ -1,23 +0,0 @@ -use console::Style; - -pub fn println_table(table: Vec>) { - if table.is_empty() { - return; - } - let mut width_list = vec![0; table[0].len()]; - for in_list in table.iter() { - for (index, (item, _)) in in_list.iter().enumerate() { - let width = console::measure_text_width(item) + 4; - if width_list[index] < width { - width_list[index] = width; - } - } - } - for in_list in table { - for (col, (item, style)) in in_list.iter().enumerate() { - let str = format!("{:1$}", item, width_list[col]); - print!("{}", style.apply_to(str)); - } - println!() - } -} diff --git a/common/src/identifier.rs b/common/src/identifier.rs deleted file mode 100644 index 329acd3..0000000 --- a/common/src/identifier.rs +++ /dev/null @@ -1,78 +0,0 @@ -#[cfg(target_os = "windows")] -pub fn get_unique_identifier() -> Option { - use std::os::windows::process::CommandExt; - use std::process::Command; - let output = match Command::new("wmic") - .creation_flags(0x08000000) - .args(&["csproduct", "get", "UUID"]) - .output() - { - Ok(output) => output, - Err(_) => { - return None; - } - }; - - let result = String::from_utf8_lossy(&output.stdout); - let identifier = result.lines().nth(1).unwrap_or("").trim(); - if identifier.is_empty() { - None - } else { - Some(identifier.to_string()) - } -} - -#[cfg(target_os = "macos")] -pub fn get_unique_identifier() -> Option { - use std::process::Command; - let output = match Command::new("ioreg") - .args(&["-rd1", "-c", "IOPlatformExpertDevice"]) - .output() - { - Ok(output) => output, - Err(_) => { - return None; - } - }; - - let result = String::from_utf8_lossy(&output.stdout); - let identifier = result - .lines() - .find(|line| line.contains("IOPlatformUUID")) - .unwrap_or("") - .trim(); - if identifier.is_empty() { - None - } else { - Some(identifier.to_string()) - } -} - -#[cfg(target_os = "linux")] -pub fn get_unique_identifier() -> Option { - use std::process::Command; - - // Try to execute 'dmidecode' command to get the system identifier first. - if let Ok(output) = Command::new("dmidecode") - .arg("-s") - .arg("system-uuid") - .output() - { - let identifier = String::from_utf8_lossy(&output.stdout).trim().to_owned(); - if !identifier.is_empty() { - return Some(identifier.to_string()); - } - } - - // Try to read file /etc/machine-id if 'dmidecode' command cannot be executed or get nothing. - // 对 linux 或 wsl 来说,读取 /etc/machine-id 即可获取当前操作系统的 - // 唯一标识,而且某些环境没有预装`dmidecode`命令 - if let Ok(identifier) = std::fs::read_to_string("/etc/machine-id") { - let identifier = identifier.trim(); - if !identifier.is_empty() { - return Some(identifier.to_string()); - } - } - - None -} diff --git a/common/src/lib.rs b/common/src/lib.rs deleted file mode 100644 index d218387..0000000 --- a/common/src/lib.rs +++ /dev/null @@ -1,12 +0,0 @@ -pub mod args_parse; -#[cfg(feature = "command")] -pub mod command; -pub mod config; -#[cfg(feature = "command")] -mod console_out; -pub mod identifier; - -pub mod cli; -mod generated_serial_number; - -pub mod callback; diff --git a/dll/amd64/wintun.dll b/dll/amd64/wintun.dll new file mode 100644 index 0000000000000000000000000000000000000000..aee04e77bfbd6601adbcd5a46435f059e31cf6e4 GIT binary patch literal 427552 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

l6@4tc8N5VA~I#-O;aK8jwH}g?6 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 S2U$OlRhm)2P)^SQ)BgZ@5?ieR literal 0 HcmV?d00001 diff --git a/dll/arm/wintun.dll b/dll/arm/wintun.dll new file mode 100644 index 0000000000000000000000000000000000000000..0017794f4638f1a1b4e469619a9824accac6ccea GIT binary patch literal 364552 zcmeFa4Rlo1xj($mIWv>V_do)MFu)`eFcQE(P!UikljMXXU?Nb9qIDu@Pxz21*3#0} z2~uxRZw&-75Y)8V{zH&9Bia(tiXXk#w~BX$*ou7USZyIhdrwBlOulB`-?Pu0Ndnm3 z-nHJf-u13GYfa|tefEC#v!DI!=WBn=lG_`Y#27Q+6NxZ(2;cPQ;Q#;eKeNDC%9K}A z*vs)pvknQ1k7g}bYBt(xAK38S2ddZGR#o43--ZWmci&@s;CuJkYVNZYS1z-y->~|g zS;@%>IZ@E1$6tF^>Ns{(|9Ic~=>303`j+?q{C*1m_PuZB-ygki$G7*rKfj;MzdPR- z@SX5x;ZcTfsiX7gVSFEc|HS(V{QKDZBLALwl)j~o68?SoeJ{^nTC+-_HuWA>l`!^> z5XYo_YpeAw(qP4^}gl z^deqR-+ztopW%DNp98&_r4?fzj=_s6^dkY^3pLv4MN#g^tXi!g?<4xk((zWlq#WSY zpde#|RJj4~smSe@lshu(fsGHW!YfWY(2xc2x#N;@j#&@fvu*t*7 zm~%w(|I?43hLVn)3b7-u63JIvimq0P7V|4x+CL3hdHℑmD_<^yo7?Vp5Oyhd%*} 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�%=kFg*8R~rLze$&^!&l1)w&A&xQW2O3pL5OR6RN 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]-=0K9~}KskUtLYTa@i){1?H|ud5m| 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

&DF}ai|c=L{TtWV`5x&Jr|H{TTlWib?7-EXtDsA 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

Aw#mWS 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 q3n}#+{^hVY+(I7J+?#iw5RZy5 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 literal 0 HcmV?d00001 diff --git a/dll/arm64/wintun.dll b/dll/arm64/wintun.dll new file mode 100644 index 0000000000000000000000000000000000000000..dc4e4aeeb1400f75fa40bfd4d2503a029c03c52c GIT binary patch literal 222488 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%0D)mq_#ZFXgZHZPKCT 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 literal 0 HcmV?d00001 diff --git a/dll/x86/wintun.dll b/dll/x86/wintun.dll new file mode 100644 index 0000000000000000000000000000000000000000..2ab97dba062208bcb6ffc32bef8347d586751038 GIT binary patch literal 550928 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

@B2e3Nq ziqPiOB+NVTJb&<5H%m5pw*!G!UeelwDmb|QQNDs}ALBPLtZD5NJn81tjz)Rom@zKY zDA_=@W?3}}g`VM?o(!pq&136;m1{vkBIcq@R;mgD=d5I<>+jpsw)rP=q7w{%gAtOl zJUjf(vya}}PBn)#7jBc$rK%|D)|%%oq@Xs8ONMy45!Dp5yZlCn`!dm?fH z`K7>Rjf@`%L-)~l0t+U!Cl*K2l|k%T#(HTr?m+l87mQ?VJC<<5RqpnFjIwKdQc9Os z1SW9%T-s5PZSO2l-K(wyOH0j%;0{=W7Fo)O2ov@ieR-y>W*XgNEYB8_k$sLmU6_*Q z*I5zjz<7U(RAPk8ic33Yk?kxsGb$q57Zh~PKtYSy3iM4Pxn&Cs*(hS6?Zzq_pS#Uu zIAb1%bF`1IFTo0+*j3abwz6gvSSj8lxL-D;$Zwh-dQviW9@RazfP2*m$+pJh*`S_z z<>sG31CY`pgr0_&G+&EI2DhRgl{@ny2AgZrpo(rTm(X=~L}}zIT4A`dH&59=kqNkL|tc(N(25y|3S+9xYXj0>_+t)MMj^>hWT)daSD& zE@3a+qaM$d!k6y!K6A5rJhe+bo_I$+9vjE_cX}VaNIg~;FdsX;kIYq%rf!4|I=u}a zt4Cc1Vk(`D57N%F)Z2aXMq_=sH`C)-8qd%gKTv!5r&@iLdRZ^MvJ5W=9XV}bq3WwV zy_!CS7h^%R3IVrzF_2&72^^_DU@YiWkjWOLr+SOAz*La4EXcv?4RC=J#BD*|t$x;6 zuunmb()Hx?X7vw^1-lgFpat1py~~)V&3-cC=0`tvX6=pg_lsBTj+=Vit^*1mczN;2hi4j9F z#HSX48te1;VrN&eWK*S$N3?q3J44SXs0iT$?a}J{>0Qo43VPh?>4RGRhYH%Qpz$$% zSF7Kpp!*bbhZXltt^Q>NW&Ej=kTc{9XZINe?NZR6SWxD`_Z757K_9W8uT(#1csBx3 zWBOwCcMUJ&-dlYS`Mgklo8f&HaX!R9_ceQ*? zf|dsKDLq6#IUkVcI03fH5RytH~pB5=`k78LK)M-Cxxya z9w)B5_0}yBCmKw~f=tiWWm6!bv|qP{GTokCXK(4-?^)gt&31N6-@df2*7qIy7I)fy zX2HNgaqFOp(Y}CaI0HB-|XWktGLz$VAA;JCnVX? zcGUlbWVjR@dbHi)2a%#4DM(bhz7CpV;F~O0M#e}aN8g{g$KYZ-MOZJxEahzW4Y*$w z#;dZEn)8sqWv;oL9TA5Uo5u-C7y9i`MXY9$dEIC>DWi)sm(R9@i*l`jo!-Wa-#<9nRSlG`QFS76RQHE%*74%13+6H2hQRLjyMR`?4c}Zm^ zh6X6ze}?Dnm7^YXdEQ%=-gjX1+kNloTMB|D+4_o!Hu70Mw6jQwcSXk182$aeSPi0P z*`bo@<}R);g`3cx#tBQW%6?B@9+i9TfV-n;nmsUd-hA^7`Zh9TzBz>>gi(~;YNk@) zIUw)+6;o(bQEpX!hJQHbZBG~_xmCfDv7#yK#D4ge#6eF)?cyWQ2JQFX7y+Fl#{X#} zP;&H3M_@ZWFC%as1n7Tv1Z1QBZ;XJQ3-tffMqs<#SO5JraEzXp5fB>Ve{BTR5@5v{ z>No+Kau~Q4*1!80w?4s=DNBMyxt18JBBW1S)(uitr8_izmfm?HxFXlsX>=Op6C3wX z+5tFdb8+z;ESZ?M$2V+I-yyti&B)a@u&dBsYHMl`o3>eHsZv`cCe%!v042zE0^%bc#zOvmnHpYI#^cBk#0 zi^YQ3g7nbrSSbBQsZ&WV)#A3!pa$=8-HSPbp+;v~oBu)}v-Llx%E+|mQn>cQ+fGKR zDwq|o10%y86L}+tc6pxKvQlgPSY>XV2zSNk~0tEL{7_(7&~RW-iC|B1d-{H z*6E)=AMx6mG2bxhA#XqO5{IUua97!X%WAzfmvq{OEynTq@MR>r>x+hOH(5k}f`CD2 zhC{}2U=W3GYn>-8B!22B9#wb#N!><=?WgaWIeHaM9OyGR-4kag#!?3Ig}2$|_O*WO z(my+CJlnOG_Nno-`f_n@WFyz7MtjaSn)U@fDZ=}^xf+_GSQ#@eq3eRff^%KLx#_{V z8Nuf1Y+%9W0{NYr*)%sRI5#^ucU*Ap#HP8EgL9`e&7B6r5Z-eAY@!Gq;(&`4oRKb4 z@G~-uHnOU0%d##|tb^)q;hfIMv~_|OY_^UWZvM0RpJ;=08KW4flD>^NcB-Im3*&6n zF-dIIc(95pabWU7(7-jQTbbzy3`Km+;@j0BIp9w-xElHu_y+~Ex|I`ci00LfGj^K) zqClC7++mxIy`*TN?>q*c{+Z%jEQ%9C5_p938_krhEOVzy)gTp3`if zp6fq$ReQibr`a*1uu0%Y(a*7Zt{8%}8w0dA*J`}z-7A}2-c{T+h)FN)B`D38TE5dV z)Y)o<)Dbd(Mx<_BlUEq-=Ciby+Ko-Z;;oxL?G8UI*vn=p;89nRD{y2n!35j~nc3!O zKFe4YZ;P$TF{9bhyshc18O{3|wge8tjXwFh*5Zwq$rxBtighd9!i5QFowJx6!tU&t z<+ewh#w-^y2AI?aaWX#Iuam0AN!qP-3?l=2H zE&`z`B>Tl)SdqI9NfG}LuDQlT(0ThkMd{02mmi3*`LGD0!CCNFj|69BH@6Rvb#aQ$ z9s0g38Z!-k0Tbe4aS5O`?oqpBg60=?GGynVR)F~m;{g#N>{oQmjG}bCY6@2?$mwl> z^mb5cRj}leD&emvqu<9uMVUqxLRL-RCTHy>c)mMIE6iV_Q_xhDHQlZ0-&61wJ(^nk zi`c}*A#Jt{p?FwYo-OpV|Nv1yqCNyWDF7X8}$3;GaMVYa}cw<<;810^B z*mavj@xJw{aW+U}n_e<8rdLg(rjz-fW(#j7F>V)ibDaMw-E{gd)lIkme8s~W#dq8l zcuBT9tklCxvR&b3lISJbb|vw^%ect*_;o}AzKa%Cd7}Q|p`vl6IC>h#IV-Y|;{_fP zGB`K{i^jo+9ZFnR-#$Ljl)6fbRd;dPEb7T)0iU4FJl^3z@ zF6M<+2)m$&qg}kqjbqa3^tAMQV~Eh^qAbSLJv~)xyoAr#L!}J0{;K@q+D+_N8%3Gn ze0i3*g()8Ahv#i!xXa`(Jd!{p;U8$RWxUS+`}|+nYRkBl|8MgD5dS~mAFQWsxc3r} z(TO7N{q1_U8*ZQ0C_dFY3bRrdhfyuEaT{==BrsJ4r?qK~h&D*5oAEvQz;>^Dp8T0N zJ*4KDSV74zn-qB4=YyTnrak<78Mg-XUSWjH;nWwsf$@vpoFy@)SaMue4A^rLBl3VAz1@GN9{si-&z1jxym~Z%5g|A)m_1(@ zxRtzM`Pm}eKTLw@Xp7AC-6i$*U!+I zp4Qr#{wwHM^`%E=`mfWY*VUY(M`vjdH4s6M&JSozy?ZEI1Qmrx#wq&e9Ba@t+F>q) zG>v?yn>Qn~Z>IWhk`)yA(|kg@XvfZhct$fZ8 zi*dqfizjQ1e`Cc!^ylrV;IKV|%N1i=RS*rZ;vV0)B@Jz;#LB672yZUm#gWtoE~rNM zauym}t3>9kxF<4-2)j+fhgJzSw>7w+N83<-iSNE8Ri3x~Zr?(d{w`F$G_osH@8=L7W4)j>rkmPbLqB*g8>(dk~%1#d~MJ(ZVqde2tW;~?GoFA*-0=H+T3m#5;-F;kjI%j|ux8kZL zGjd^Qh9E^NvTxkYu>yBIA-FNadIdj{qi98@6nb_(_%qBGKJ38Sg^ zq*{*TGGqp#5GMAskq4%NPC?umhEpmwQtE}2cn@+wJ)X`7UqDmVNm+F(rh!=2(UnHg z6r>5i|E#>0a0Yp|?*2?}V$3bV$S;}#Gb31R1}l3-S65`#Xqnu4A+3RBlLFM^T*w-7 z^;!cHiCb63+G;U^9VG@lIm8pa`{H}gNZEV*DY2r=$k}>HW-Ky>S9To3AtFlDnm4I+ zvFQ{skKO_)8X4ZADS@WQ`6|WPf>dKQQ>+~~R+DRXQcoxN20~@1Nv>6sq3{#^DKkCq zJ@_)(wTa8RL;p5#StkdSXj(0o?{(*Np<^|%aVQV)BqBhLh5Wc8$9(Px<_#)E8R~ea zEu)wJzw&>Ae+Q8KUV{nAoR%D^y~2M1nnTT?t+5dg%FAK z_hOEN8f#&c?DV_C_#_?DS(LkaBs$BoJ1^nv2R4JgkTWP2#Pasc5E;uuzB_O6%@X9m znPdrj}O0ayQ(Z)!mRI>Sm zR!lF20#7?AmP-Spc-@KUR_L*xvg-vGPlQV3JpZr|;qcIrj^~sv3`5z`XGdjI_x5V9 zf=)5Nwi6=L{6|vDThyE=LYKT9F_#OhE0K%eVMJwxd3%sOmEI7iepnDeSiFoQV8=hp z)Xmmkp9~^l9!wMlAVQTZ5SC$Xr&7ZR$SgDq%@-!nvoJmuEt2N@?t};53M!f<&%h)L z6@iFOO8e`iw7&;uBbrd2+w7QrabOtziE|(I^=*Ip^f{0X-}Squmju$Focesrz*R+; zgdVUr51RqueJtl~ScJjyDI%L}k;uuT&v?t2Y8w`EiX9mDee?w5PEXHlLbPhJyUF{v zX)`lv)i!gCpy-+D3-e z`8h}JHyOI=3cLy#^*DwiDqN87xTiE1Wj3WsjfJJ!Zj@ykZPw5ZUA482 zx&_O!QA`?*7-qW<`fH+4B5d+PYnXN<6Y1ULo|OrCUq7uAQHqq`vBRxAd2ua6ZccTw z|7>_v?$_#APDGzyAp>I8uxl<_gsuys3GuatFeTfm&?7LRk`|mQl|iceo|<^ul)XQr zCKoC2ia+yJKdigF}lP!t{JwV6=JhVBz1S za1142^+1)Hwop3kZs#%JZBDc6c*GTjz!14R@gX`XJ_?YV}1vHd=`VNWD-*|Q>P zp3k5i-LZ(nK24;`GAy1BH!17@SBo4~EeAa}9=m2w@FK%0I6ICuCxwdlQO-Eip>$F( zhrACVYb2;e)ri1UrAJCniEnTJ@GuU%~v^iiY$xDR1Gyl4QiR4S9OIBUBU@8tWKmwB>XJhRTHzS37~7 z3|gX1aZXG^_Uk(<2d2+c4}4Dc>j7$PDezkjWzQ8vlE|wkQ)B;w;j{NDbW@D(*2W_R$4MZpEd+wDS2C=3m%XWR$xi*NEU( zaWCRvD;;KpsK|ct&D7C0I77FHMA#oB1JOUo5V^3e2!&alfxV2jy5&(N7}hf&7`FJ9 zB?(Nd7(kUG)|QE2Yv4R2#LmkPr28*23s*p}pRqWYYu4V+0_hNLKs)R3Ls&B`+V_Z! z?LhpQ(2hUTVkI_Yk96kj4+i>;-8g7KJ@gD_+32dEBQ#H3&iLOH`PH7t*G`7M3KxO^ zzZ+CZqXO@PXkoBA=;)tHCL&J*r@9o`u)-4y!^;>XRj~PEaISB>avz{3aAo-GgsE3c zAO?BEmqrvNOB=p4c2?3n8jv;IHy~?xs<7Zylo7`_|8U>uCd}&igi%6B)@=E_u;5tp zuON$fCKn|#iBvI}EJ?w)27}WEf{WPgm8jrVZ^`61Vmu}+u;w8};8E?R^sMGoV>hp} zw3l32IlGj^M7gmxJ>zHn$Hjz_Q$(*0wu*`1#RWst!ND1jH0yZBL6yJ8b7iQ?y4m zP#$&W0O3DhID>~mnT4KD{UewL`F0eMTOFRQfg?FP!We54MeYyI^Nm}W)7JOS*?T7J zBEjyDJ#EVz#xBIPLetCgT0DJz&inC{J0<1rW1g*AJ>RHM@Y90tB*_eQif{3q$As9k&H z#liZ2eX#yo!(3}B=W{uIEw5HPdc%3VODi;A#(%vPf$t<*pjB+qyU}Kn28_}&Fs&76 zts|#4L##V_lWd_!2le>EdowjxNu_cC>DN`#BjT|vv_jul(*6#E4 zt#()@y(@zwv7-`?Afg4tAv82+W$XK6SG^BMcV!wv@)>B(t+dHomdF$&Wtg`mtyqv# z`s1rQV(ie`Gi_i z%^b0I!cy~(2$T_K{$h>@$n7HKpX$i{j$O98<-Ioaa?m17ti_JS187%7>?Kz*BFgQ% z7k!TBFA#DoftNoQ!EQGH{RlR*X~vi2aA$G^r1V^YbNIFUC-dv@k6DGma)zY(oZ2BG z3|%wElHALnCy?B$7GSJ4G9YtDN3bRb2m|x!b7b)(mhY;b4907at3(LnIN1`H#Q^gM zpojQ)j9n(Hdm?)a;>+1--f;m(lUOi{FjZvs9bp*g8nIjX~u$ykZkPw z^m2Auv83azKkxU8&8bffoL#@HCJP5^5~}`6H4*eTSd%pf#r>U{JpbfCP4u2Ait5{j zB;xFO7H2j9AJDiy)0mk7Qv%ln-hF0q5i&{E>_AZq`YBks9UWfO7Pgs}#l!F|i5<%y zt(Fl7C;bN^vQo>%NOQ*zLx}fiFjUw54gtLVt?zoJNrTke9rTT~&x_SLw&+ zXbpen^uXJ<@n2}Ym1qqwTW{ah8rJdV%gt*+JM2i47umAi0k5$v1IE(#GRZAm#={T+OT3WrHZ{f!bLl9wvv`lJx9SZ$j`3R8t#mK1Wf zyw9#t@H3O(qM?+4i}`~@84+rmfQRKhUiRK3`0gb5?|9!NlAEdo+5HkJo=!^fWD-1> z1b=t{ULDvZQk~>d9k6n!{+3mb>O11!^YzwJ316VM@Z42BbCU>-2xMkF#$}sC!c!oZ z#UbZ$=`7#pbGb{JWoJf)Zxk6%c^@8{eN@}fmeVR?hlr@PonGv4ctY$DnL~9wt(};d3(%jW|Fh9%g4R{g6N{~80cb<$>6P}_9_R2He50{x|sVU zGSf-H1VdJo8HQbA&u&TVjV=mYEDVnv_7l{qmiHJtZZx~WiOehs7N$rK6Ae!=BvO*5 z_<8;Y+y*7J*t#rDWF*45;W>adiD)G$G<}kMSyPMXm-D5!GPPQljUf7}u!MxiN*rsF z5og4yaU>*sm<{M%oILWfIBVBofeMU7ZsToS?E5 zXk_#zYndeGh7!-UL^%<@`AVXko}IPhSR`+;NQTW2)6&fM>;jp?YKDis&@rfnpBF0L zZZ>cmE4P0mQ+T4YGn6NHTH?8!-GfyCGbitQ)=EhxY)aXGcF6wo`)g?c>mly|*u8n1 z9`(xARH@ub-622s1k$j&92vq--64Ae7q@4@^(3!HNvQUoR!Q#5xmkO(Q?9DmeOmE8 zwZmK%&zUV{%aZ2(Rk#`&!%Ey?-W(4A;_u~0tJj$)@nr{ly(?#D_%0wXTdS^3(*4U$czY7OH3|Ma?=M>jof3YY6oQD|KnXt>fLAZKO08aLl~{fE%N8PUtG+u9xg{Q> zGX8zN-Ub41t1eauy|vo&vW3Xosy*=-7rtyE^0w*=8wk9u`b-ddBxPW7st(^O zGR7Cs^+^-6Ok|A9p45+@S?iG>6!^N4G1iX_TbA>k1xJSc*6!Sm^wg^3rI{#oVnfJW zb+t8@!lI$GsSGfxt^YH?t^YqA;3OgPaY;htGQcvLzxBjN^D0KOA?A1XRUQG^oGr*^ z>?jM_Ji{a=YEwL_QOQ{;3Sesy@a5>y_M%Z5aT{H;>!#s;0(C6iK3iIq zrz!UwYyuDEWg0Jmd!SN(3K?1@YTo@^tycxp;9lv`A%R_GguMd=Yut#10}yMbT<>I< zeu*A*!nLdNJX8?R?V%eaw@XN|fMC6Ye4P4;VS)W}+q8yExT{wc6}8cSXaDh~M*br2 zKjS@b3;6xf){sjqf7>E&#~XvL6WE>B2zv#y2Lbeqql#aM9)Z3vUb23hlq>=$2l6So zosv`a&xT0JJIu4C`tf}M^BPk9^~53y0$r2*7ow7CPNW8~p!&oLefQt=melZL)Ivgr zsd|Rttjk<@kc8zGBsDh!P-+4iJWb?!%onetK`D`Maw7VM3bU^H((z7iJ|2eFuq;1$8nz2id4y`#WG()r?0Px-5ie4WrS;Jm?uqgxZ6lj zUElt*w@m1oAiNV{cqox0qTrRXIOA1et4v!haSglnXxoI2xvM^qM9#O;cF9{^EI2YE zY@NI<(^QtspgF>gvDK|=mBW7Ixv!X)oxEW6M1D;@I!WIW3;%<3zgKLZaDo>`j2C}l zUs&~pk#|oj%IEyjif>;%{Xc9OCH&vQ{|f$p#s60RkMr-QD>M0@%D-9+^tI~hUr;@| z+W2UotAEJ7gXoCI`?-V9$S29YvZl55{qwB0pj4>%Qb~QLLRW1OE?4B8q+}$};$hO4 zauL%?Fr{#LE`w5PZWk*eiz2_W^6o5vdWyy(Kej%_WQ@94S9$sz@gX|7GF-V^!OrC1 zcy0D`1OIc{{}TW0{J+otasJPw?YaD4%YO;~ck{3MXjP9O?YN5MuL}i_a~CWe8Lc6c zqlv|p(Hh;nH`&8t5z1IT88-370}7PLsDNlnm8Nv^<=3mX2?EXvzXHukHk1K6YP?lV zl5rwe1Ac835gfB(Dlv*V%JoiLlo=RhUdly#Bu%uBt|kPq&s}c*Y-OC`@e!=VTrgUd z=vpE_hA&Qxp7E6vP-%u*X*#{V!YNQ>W2aKm6jjTOLN{V|TZlwNZ7hAugF+a>S+Mji z-<6|Or+1&FZ~3Mir#ij6EPc!Eay;tvc3JwCuUq<-EtbCJD?;RTdN*47mM_ZTrqjF5 z(zkrU(zkrh(zkp@2)a)1Q8X{lT;8c?}BWCwSP;+qTD5QX-Vz3SS8nnIDx$u8I*bt6zb;sUV^wh2mOw^)HR$cNHYvg6yk)4AWW)a-4x6 z&aUc*jN>z5%u(Z8H5X~wEFy-6CFla0>Dkw0s^i%OQ2`eMEqOch)vqB+ob zJFwuIrTiweGlfHz(9SI4eNa2|Qu&fefJUNLFH#b;Pex3uKZg&*u2g`W^XVH?t4~!> zW(LtyEGWD#6DmhL>5< zTV27|W7Vi+Fe7@a=kT?--5Rnel2E`VlSyNylhh#7kDsIq-3j%z#I~S_vPlt>Sa>h@)-WixDoV^ty`I^ zoD)XykR3I4mYNSh8HirQzu zkr%65k!uTlQ!l#7Wt87!JkW3K1jU?bUb&bJ#}c`@YmrF3NNK`W3u_>P79{;_P^&Mm_(hfW13Jup2rVfDF zhj|&Aajw4n2oCKr_Q`D>aYEjMrASp|=BqdtaYG?eG;MZ$w=MEGrD_d}m?s^nws09w zNk$RhfQq+b8f|HtZ>U%emsj`NXeMYa^ZV6L)fE3sA-q_LzLjra6g z)zg)9qeyV1hUfD}0t%RC9=U@0qy9yF)!8w6?Tb5GOd_bFN2tzrBO|SvPIq@Dy1N{D z(!54hymw7#QNTTuzhB5bm)8HU?+5a*0ZJao_*F-mM*+m`nML^f-_5@ z^p^x@-ej~RhlMhoQrySY6@3IzKiHCAk{*~$ zf0Sg1V0w|esU#~{G7gp1V9BJWlF29?k`*FFfwaydu?@lSEN=I?OU;I5GP1~c(~3!i zIS9j`MP!&>I2GJPbEh97J|tY>N zb6gEu@YV=h0#3wS{eyG#Hl&sET5`~fI5N!(MH&?j-vxE_i{7$Q#P_?jAAFbo$ZH|k z3PT1!Z>c9BbTtiSCUOv&?QHGaFB?ZO#uc?-HUa2LAww4}&~!O!pxg^iYbh{a`iybJ zGtpMPh2jpPtf21({czn;z=dPp9&-&5CUkaq|7M@?7M2m5O_hH`U%F#Lu{pteFs;j< zsvJ%tp1ZB7GB&L;3#CsHs(*Jvr;4B2+}>1qB9gMs-sC-jytAlmOf}o?6@g*@*-GHj zuU#$nqypzQS7z27wnxt385udPGUiX8R(T?jGOhA&ezEk(oo6RbE+3^r&?BFs+4#6K zYIKhNG_3A3M@o{i*+MZ(8s-x@#{TX?2(C&4UxXW?EO^aIJaz60Gg`cvsB$JeBzs&A}?60X6zb2cIel~L`5KKt&! z%$!!}N}hC{eJd`C_t|!)8hsY$xUOKL9l=9qr&)cBB0i;%O2~$6^Ka+T1_)Eydv-*wLJhfNW%>xhM&A>>Wi5Mt&A21jgmcDSl zLG4r!Ej|irhU?eZanV|nYSg$RAk4g@&ECu@_W3YfA$riiaJJ6V-?EjLhI3?$ceZ|# zf};>7d8qb;(b3wS0vRRy%&qHO!AjG}-U!EqLfPJKl$I@zI=7;akhjHo0}+aQn~O8` zw{6_%H5DI$y3Pufy6{R6n&-kR=!vfJUE+T2FND!X4ksCyUS%)nRhO=!j@rJrd$!1P z6fDd^t{Hi(BUsWd?sc}Yxv^jncbd~}PKwIp65&-U22}^k^16+#3EcCKAjB@ouT+S3T&SH`U*Sv_ykJ-V{3q17hbM%%ogN^HWuV_4QzXQJ+Gh7_m_95imm9syD z+lLl+aP8}aV=8uY3(wHa?qK6{0@$B-G^ahd_8H!TjR*NxYyHSk5prD?Z0zA9)Y!|fcstx?kQMXHQEI4Y$n`cpL9&pm@iBA>O0AgA z&Qi1RQDTN9H7Ge4dXpErAC&&^aD@4c&5|Q#B^!SOn*keu!Nxa_NHe8Dp~ii@moD8- zqe6|jKuXO!*o?W?G1f{1$f~u{enb6+&xdnoGzvgJHdJdA#+CHJ4?%HO3|U97*|z?R z$7onD+4X(I)@!}$du&Cn^95-x3MykxGb%{>Rijg?HQoiu1-O}Ig@?;duGiFqyd7uQI#(JeEKUMoRRwZ+(h z1XnjT`KHt)JNTki6V(ATt@a1iM`Y$#sn0~IkBU!ytmKhlR`PZ8abtWulP5-EIAjh)rsN&4=0tF7=?&yai01!TiO=CyEO zKa15PG25 z`=^%bAt5zYgKK0pL-b?@*KX%6v{w2oxK^-N$`*@5~}AYmaJ5;1>v&&JL}W3`5z! zl`^JKM!G)17AlI@%GjINQlz)LjA71@R4O=ZG}BAo8e6Pv1D%FyFv*dqq^?#?W_gZj zjYH{EYoG*+h8m@$lTqCR7mBOxQ`RcIeVL&aSx%#@32VV=jgLcd7#3!yPd7+e(?e4@ zI5016(4Q5g3Z4>t`UQ9@oze#Il+}#L{W6awO>nJb5YB^V7)P)%yB|}s{t7rX;EcaF zd#p@ZHopXaX}YkFPsOIH_Y}ef^;k$xAxc4!-&aE~XSy#(ky?XI46Cy)&--FD8U7PJ z{Z)G8_}3HbW&l%$e?r>$Zu28vWhJeoNLfj>Jil}$y+CUctxrZ7;mb%ZOMkzN+*e!r zH&eglrT@8&bAGr(RaY=Z($e1nP%12eUshpRpsOVlH99hY^;a{1iFq-I*`#m_vsqZq zd<8nZxt;o16F4kp`%a5Px|qcl4w3nMDh^SwQ*el2os)6M5qfP9hx}2#K8Hi(nmU0) zWRuEYBYA5LPf2Xncjc)#r0pae;#zI2k*W#~u}}v{13YsI8Zq86)=2I%KqvC_s8Rwy zM%VAE&>DqKGXGtQ`!6-r(4D{fDs%TTze~c9ivz46MjXSL%GHqAfZsM|4KHW-@YVHe2&cz z|CD)c6;U9KeLg&g3#K&>zK zZ=_V?zp&p(?y}#=^Z&*DX0ME;>aL{yM#ks!1grF6sksG}xOH~5r4X^Vevs*+!zHGo{T??%^h|m_D7NITs3JA?c zLudwD(B;WnkTePu6wy=`@?~urr^AVx&q>CKiWJ~Pk-&*C#!kkGdkT%VlX2ps|EoB0 zCM6HxL~CO)LTcN}w=H+%*o7UWKl><5E!*NgP-daEsRTo-0R=+`8J}QCcwQC*NDyLY z93iIpzIID+tx!RT@$L&Y${UQMwQjgQ9Juk6U1JpUd zwdoPjO=%#by$)QQ#NJVxLa$kvSQSw+_e6?#a*uPpfg0#9Iz z@B{?&<%cBKzrzz~P0;gdzmM|-niNk!(lg|z@&pPKNL6JW=Lv)q5ug4WYp+m8d*Qk& za{l~5a$Z-*bC=Op#9B`f^m{P8sJ56$r%EV%jRa2s?ybcWkReP$vDSdcNv*0g)u79K zmT1;!A}inU?+Wz^Ws;+orL}mm-zSJxk#dHj9k~W-5P?6#wMMuDbO}jVo8jl-UCR+3Ad?NV#N$Qlx z+=MkHSvWg6ZK)NaYON@nM;Z3-s?{X<2z8SkeB}#Q?7TseN}>-E`$^S2F_wmY z0kPkKsExW_8v&gZMNHr=i`;dT0cl} zVj&@?!}F=wFtCub_E5cy*>U4j-#GoF^IHFsvWP~k-zdYqGq_pC5Z=1bE?m>S_9vn~ zqAD_*4N3vE&0G4&OJ=^sF%m0b4r6P`buQgilMgG8#st?$?dP3tKl{%!J~#4Vk>d1W zZlTrFdG?;ZcbsoI_XZVXPCve^X5?TmOQxlfUxbQc+%sCcMld8xj$43+Mwz(BLaOAP z6NZK3GGF$5wk(A!5;-*SSt;iuvXf-*EDwQ{j;I+ z$M2GL@|bgDygbq&>#99d9{3#mg>Ud!RM_&}j^ozfA(4%N=u~?P;%g+t8boOZndOf4eSSNZzk@P4ST0##s4kYMRB2hrU_fZSZfRK{cJY!)sF0 z3_be;fsrElkgjd+^E-jIZ5>Gw&tQW31jNypO}he^UBRpf1<-* z&x&066{l}mNbv$o&B^aeLOf3PFHtVOez$nQ-T{8+EEChBZ<7}b-Rm~qo>q#C71!PE zxT?6G+fMz@=U@Q3eJn~>&NC6uxj6XhgL%Vc3|^9_(~zFRc=_3vB0=&YxFdUie4V z(7~HuoM#;}h9C;`b(|>L#$ikC7dMLJgj`YYP-dTh`3x&L&~G}grdmT5Ve3j=6%Vzf zQ#D)rMnqKMDxP9qIxc2udMAe> z(MvVvyXYVNxIJ(wE;K{4UBPLgMiah!g>^Yl7TkQ0FSBx_TAFZI_Rt_PRL0&iQGJs*SB{QNpOUfP2K^#Q-PD&c##|s^N@vw zq4Xw13ou2`)|qC<0^wb6Llak;x2zVi2pmiv3@&&*Z;x{q*ZN|NYA@ba7^Z`A3y&nH zJ9LjrxUcLMuydp_osCi>fml^6H$%er!r3V_GXC_oc)EhuNX4-Bj`%>~5?sOTwd%*+ z$-`$csu-GT&3W6i^}$Kfm=0Gc{re%TzD;>Gbo)d+w0iOb!)NP%aZwRZ+wxB$j%D^4 zLi@ZYsF%4}`HC^+G2@2E`bByvunz~c{-LiS++oX!Mp9lI$IC|fL4Z1^6L}caDZY;5 zhT?a@LOb`~(Y^P>v|f_wpE+uf^TV3@yT)~oMfaLRz1?|7@!dsIo8E>D&5K&og{nw* zDE+J%cqa~Jt`51Hf(v%(Em(#$L;osKJ;nXCDaLF!*kp^rI@yn~Ve!UJ?{*19qJrYD zkusutKf$>mqLO>_j+$@3MN^8SX`dQ@N<*i+*LZvHhx+n2oxwnFbq0MY2@3l6no`qV z22T_t=b69xgACnvhEDEn+Azr~q9VtFl`VLD4hFWf98!?gthq=f(A(|$E-n$xizrN_ z&ygu*M-FV4bdm&Nq1PLa)>c%f3xX^SJu^|NTsoh2^raQ|N+#V%nwTF`tmm!j3u{w6 z-kzHAh=}OzOt2PmcP>YK>xjN!J1u=38#`KR(?0qr0xC|qv-55z zwSIRmcE`2{kPzxI$i2nWQCm=*lD~Z8vV4lU1XW7z92u$(7qfg=s0Ljygkqc}{i9o) z+k$iZ^<79?=$JP9fE;L$zTgNFZ*PVQk(=tL4M>*E-Y<8Qy+_*PWN-xVuD@+gGo9R#Y+?!r|c6ExU z%w3yl3^lPlcwS3;?A}S{aRfdhZh0F`j*Bp8 zIIVCpB1;7)q3QD!nvMyYj+@C0Ep}tDrd!p*KO(duOB8OcI*kIg(~s?J%5p`{&XPvp zZGRAU7FM%XEaX1*0)53KJB{0B9$+I*?ln>35L-Co$!tJKezZ{|DhuhRiR=pY(ayh! zz|P3yb@!tQ;1oNOg&8@8?%lYnDm4$262NSlSzCzO;=i6}&%pGj2>W6BGjbGN=TCih zAP+KNKzzd@N3gl?j{F72)Tg{%vauVlt@W|GWI($Nvic8~AVH|NH#= z`2QjQO8nNGOF5yEEc1Ibj4|<#lsakz3P6O?Ck$0tG;6PQ2!@N zjyxPITDZ7&)7W@+k+TH7;YZ8N+67At z19{fhle8}`b6p<0w-oB9+`MDA92#kX=sr8CtVkc50sL}t4U(a#4;lVtRnj%-)>0BF zR*?%wT<&xt;nt@Z~Ns%IRl^vUPF4>ej2YdNF$Ynq|YJ)3Ram+HF)tMY@Tw z(2avOOkT5Wm|zR#s-zGR-hIuoVM2)XWg<Z~2 zU$1v!!=$tNhJ+21*DM<*oz+*xV_f)}Wy7SidQu#M`*3WSbXI5SC)qF=ia_N~IXzFr z`N?Tb>)-_<$%4ty+Ka@SEA+l^7^+*hjAoAFH6=Z5tADHs`d4C>s_tAvw5$|pN3V6A{RNS6`IP6tZiC5h_G2SZv9v- z)Vw%{aYvLXh>dg<_}V4Lu|-Ej8F?QkYHPv3!O-F1yL`usj4UIx3ccryjjHP!XDO!sG4r z-+(8G*?8ZgsZ|jD{^{C=xhJ#@SF{ZIqD-~`{HYuB{Q%}vctTS zyk}J*g7_&kCB!6F0li0r*eH=gOOS$g7`vIGpdRjKDv!`Ps#1-jSi%8M^juPXXN2DY z%ZcW0zPCFtjO3RI^PmKWP15ONNw9N5juE3^&E-DZW9-N$X!N2#rUan9i2^}&*c%-qFLcmq+{9N9oeN%0u= zhgK154Uj_)?0&?xyMzdh4pS}+x+mGig~4_n%F@E=To`O7@VESL=e5Ce2cC|Hx2{l)b3@As&0chHMG=}^^>HMh1(+W?1}55G%H^I0ykco zuz+6ZmS*bryTto zhzs2j;$~vvCw4{b%2IQb6`fcC(VJ@$)A+-cn&1kTJ@vvNfMye~vQ!C4K@12GBDbvZBVmetvYu7BkLs4-9c9RiyxK%+oJPbA8A1@gs>?ZG(FJ(7*sFmGGc9T!? zVSew^igx|VKFmGp4mt}JEV)dX&Afv=#2eN%)|+Tfu<;GJFbD05ZBK-e%gl1@x~Thkk09M!T;rc&j*cg ze866WwTRRBJ#Rh5?|CuCD2aECN}&9n7qc?}nQTGC?|HGQAZJ++@q1p}tsp}DVVXev zo)_;^kfY!lAmaDDn9YoQ4q6cLdtQu%Bp^c369=Ey%I|q`i-HPi0W{(Fym+I63h@W@ z#kk+|;&lqT+Je3i_j_La9MF^eo)um0L;yb~w2X=yItv8sQvKK_z3&a4(0LmA8Ory;w|K)UB!I&!sw*u_1pVlDLX*LA! zk*jEiS_GC^COHLTtVygh{oiGnN$mV8L~*Z$wT~nWGl@MOu!g43;Kc}u!i0)9$BNfm zy)R*oNi^KRj%Ia7!W@$@34nB0zXhe@X6ayYR}GF1Xut<{T|kx`5ho(^zCWVou%6f%G&MnZ)FB!Xndkybtaf%1?zdXC^bGUQCnI1-$)_J*NLH_Lwr4 zRiX2&6o@TrOyhsY&8V`&Dvs7(2Cmv-UD^pAyU*x>S6}}*3vGj=8(0!cx=X1Qc;>YSR!3HpgLrL7|9>)mC zd&}O^&KTEpk$liS_LfG8Jb)tn6Q-7O2OtAhmT(8r@9?Mgy~o{c-vLg3Tv(*YVd`^6 zki=wCIrk6y8{!_5xa)sC;S}TBHL7OH!qRj(roE1rqc`!5k=97dItG13sa%i}HUH^! z*^E!hCKesc#yD=a5g#AvI3-H3DY*Xi;J4_k3ga5CNrxFbV`H}49T~B=xwRu8U=BE; zQKnNOV^zEngYh`d42#F*u5KXq#&0dVyF1?-XDQIaQqx5ri{VJ??t^{!<1^{>UivEA zI0Ip-dLe|l_ZoqT(foWatXk7OT>*R}N^R-%qsIL>k@A0{$5H}A^>4UQhbf5s8Bu4h zTyaaXAKNJE9y2{yY#)+$cylvT)-=@+jM=%VX|F;i*SvZfuap z^i}!)6wd^OX_d)~4l9%OJjvvojg*2Z^NqskjihgElt;=&VJE^lD)qq=OLRRl6pP)r zL`F#)WB0m}0+^ySE!?8AE<|y8= z9dLVaI4`zEihrP-rIF~6Bx?6%EiI7DJ}4ryV|7>OZpI3W<(ipJ-ZY8NhwWIP#HC=W zfm(Rra$~A%9eVCN#BM^M`{~KV@O1by^LqF02nODTH(~6EH|MRTrLW?<9QKwykhb(z z-p$8hzb=Z`&PdJA3XELZ&!k)0!;g78_e6^pEp<5n*!VH$D*(>Ki8#C?Me1fZX1T@G z&dH6!wr3Ci4LF1E%iD6bOh7EOB1pmYZo6@ZX~Kb3d?YalTd<$%HX+-@p*??UQJ6lfI7~#iV`E%4Oaa8k zWXN(P?mbex3Md7#o$InO*~9?PP1#g@Gdo$nJ8+RPd%Cl0Req@#t10Jfl&g)Z{KBlj zDE)r_h?;bLlSwAlwJ9txGwos;idnTWw;RKA2{7g?Z4HaNsDNCo5<}IB&T1R%tg`aY z_thP}TmbZzE}eX?gX`AVn472|^*Vp)bpXT`-qoR<=Ja^L4dVdA%oHOcujB!qHlI0v%G0 z@l{x<=)s*~KAIuSByqd7l?wu+Ly4=;Hw_QQgD@Ou``t8h$~!v0+?>TVX7HShHb(~K z8t07T1^uj)Iu9l_R4ir=7OxrceoO_HmY2_0j(6}tD%Lal&2bLFb$I`SH;3l|DL0*R zBbSm+5|y&FL#~HPNwWQ=5hqHK)$P%8&MHOR4$3aKT8Oo)kQ*852M){xflnfF+>y)5qZ!b!gyMB*(93 z%oefVD3{{RsiG9qM)~mwDYKeWBZmd}E2&g*I=h5Bco`N=pdYEbNG;5OkLZ|H(5%%R zyVc)UcWiv%609notxEEz*R8aV4>)H<#4|(Q9(*!9>W%F-BWbfK+SM`S)*>p|5;!C! z`>$44?rzG?n?BTkDGDhUNc&XlRLfEmEt{3pw3u^Nmtz? zMO8IvM_wgp``TssY-_|5nX?Q}*RqSWUl(Vze(1oy!Y)s1&DkN>2%o*w%$vkqVs4pF zjFEYR{dJM?O&DjZ4F8pzHvQ@X@Gc7<(T{TTg=q-u9jCIgB4ua(CMkvgvbY_;p0aZY zlQKoDPNx6#1`_7Qek$pid(((nZeA|&7m3@^_+~C%Io)}Cu*!3|*%`^2ezo5|10lfs z?alT=BtMSj>@0E~5_hKDI?b^AT&3pnuS@aF*rS1A%n3wg8K){%P-LF5Ejc01H<59y zJASqQN@=ZE(pfY88lf#K%x%yWG@BaFX0`Zi!D(tz_M4B8DdKO7oco$bH|z;KNA+RQ zvUJoANIwYC2k>@)JSusR57p>KO-BCAH@JecFtZRpjNp03kP72Ogf6?3*rAm!BUQSp zx7<9BZ}J_yG%;+R6zMyEDt0p&&II8DE|Z|y@t_}DK|b5Y_rS5H`EOK$Eqtl;x4Di$ zwn~|<-UD; zXG$G8-Uhv9g(!l|oX4DDRw;rEvM;u1Aes4HD6WAop-svyaSc#J3ij=+`(!~6%fD9A0un7L8E~*zQj+qk}^>y|*x>Nn4cjI_+Q4X?)?!8@k zDKd2iuaMmOmY6Z5S8H_###Fqv>o<;!{A<|dt}tI<6I~R%hDFCc-_m{OLN6}l$NUlE z#)}rUo5pbTA6vNYY&V_hQlvF$>%_?DDE;cy^a<|f&F2dxtg<~Rhs`R7col@S*gAqN zB@$G(?Vl-QlZR2?FEqnF>EMNP)p8o3cq7mn1wwKr7M1TCv6zKO%E%5VUBGJWH%*O< z<6Ti1&ZI@EvtTHeSFkyZ60U6APcjo=KJ6zi)0zgSS+#3a30G!)a<&ZDRmax zRHpv1J#YxKnrTfV&EHDw@sVAK+`l<6C%FH=VPd**)5Hwp)`^*Y?F@d*Uz)SEuZcnNmiCB~QgL(9HQ;2vGF*RtqLp$=4>;&&cIyTBcHl-5XY!X< z(WAqwJePq^+8i*0A|G!)4dZBIwx4LJ>##{$ksaLsdr5#0Xdu+@kK9%HU zkt@D4t?Cg;i8#f|>5zN$ScOH-{~(_!>O+g6n+;N=$EGr8mu`nn!*Br$KtSUGSrOp> zqDezcD-bmx-m&~Z#9-)M`CYS5e%HP!zw39&@8<3NRz0#Nsve;p^;p}b9*wW7$FnW! z@vB#OJn@6ZULJL$^)ap)0*^y)eTM4=mu`0^M1d?yz-I(9Rwk{m$E|Qi$GWYsae?L z;0B(~H8pDuB$S0~YO|6k31SA?JKIQQltm8o^?;JsyU18~D}%xybA_BW;0Tk2k#sP4 zJyUI_JI!>Vg|I_6T(AO>jVX;%FVMeO@>8c!xC?uCkens5S&8CqPi)`}pP;37-&l?y zs`>sDU&^B2Us&$5zdF`sYbp0R%)Rs{n`ik+CoYVerY|j!rM~0JBA$3qgVH2m%>2J=pEF57`}qC;KR=Q=XP^C8d+oi~ zUVFXvMY2_Fc|Z-smR07DG)Ch6>PL}_`;s%pI#NX6>}=&SJ`am2x5z~e4M~_5pHWwE zn6^Jj#At+_f()}Ua)4kD!-bC{h%Pb(Yt&@yrS_+!-KV6vr_82(SxYknC9i%{P|~jM zYn33MAHp5^+;@3DUz_f~5q~0q{BqT&tK@HBj*%MdOT_xP&x4suU+S7~2Kz>?y{=Rj z`9yGsOY1Ri*58Fxv0qo>k7)7{i{N{rHi5OBFpZ~I1#pby=enpgQ6Dcr7w!wq zg$cQU$!h{ZQsL1tRLJjtVL)VVS}>ig?o+8)J-N-O1YG``BsZ}CB5OJoo8!=+Uf-d)Qt^K9PVRX3||@NOU48$Ts@oF=Nne;Z5`c^8Nu-ig6Mkv zUGiI$u~g?E1xnrc4e9?QQTl|* zSW|3N0zUm2!Qw9erz)KqtF%fgo!DRLK?KUNlMUW-QmbaC=B>=mf3D`^QbYgY2CNX_ z!8a>3msN!5W8J05s5lbT$tCeTl>6dZ^5zOoc&P}@y;cjWJHMAX&y5~I@DRDdzdqpNX%T5?BKAmkV+li zR7~gfCs^9Z*I|ZD9+KuyrD*QzvjVn1p;-T+tbL%4J}_1Pp7|VoV3K}M?C}P@JoY$6 zzdiOiL7x?SyhfiAd(6`(#vW(ums^i7+)Z!wRNjI+vIe>pXZD;9$$n#XXbrorbQt<6 zeJ2aSZMoT75F=T*8KapjF-8KS7hE&X=ARH*+5ysz|?uJXD$(UTE(=Jo<3vA>#+?vya#r_p5I>sUJ4(EnRvK$ne?qpN^Dv zl%!*L&fyZPsTpJ~F~)K5Q!QU_mb`c%Lk20OdMkC58y7E?16-*Rjl+YzNFof2c62ci z3ePK&$R%3ST}`@u0W@2|!%b;Xe|nMri)*cJawA*R?>GLvAKKUWLQlRE1N0-LIu@X5 zMHXDYBu}Y6Q$aB$25hY=81Te01tgQvW&yb!ZWADfSwJ@9sBzyNYA5-6g!WAQ+Xx+P zwsby1XAZzQ2%()+GYFx_RIPzQ2;ECcslHN(Og}(1S@ zs!!6Nx5W?aZ7C}MFmF<= zOj5D>zo5XK=M{+6F4BOn$-FS@Z`J-c3S1d0Ai^%$4#r*++s37Z|Df z{ZK*HZ$_zJ$czz@dK#+bSgrF^t^PJ6S^qutn0Lqw$c5Pla(0%My%rU&dy06?Hb(T+ z%x3Nt^%AkYrTQe6Ot!@40KxaJ*v&n{0Uu+LBew#3n2BPDYmt{yI* z!}#3iG(H`q0j7l#&-O*B7$g&`yNpKO%3P45wPfZeRUeni^%IXnAceW(7W>G`isj;! z_AjRI|8D(8KH~dtI{m+8mf)6wJWSzJC;2ZYBt5leRwhM0aZzU=MF6HNZ|Og#V128B ze|Ggrxv4>ma0>ud@>R!_p}ex6?myK|LTIf8#^7K6po4B#;%1RpcX4ih%F4^N!fUWB zE}k#{HP3)q`+mB9;Ae4VlEGW3I zEHJiih&IcmC5~5P+Zu?&`_~vo;XVo*&gLQ4m0K~gkZy{*ncm8wE?qAT6GiJphv;FE8Xwtp+UzD@1@3%GAWM;6am ziwkcHr#!U8wOwoGYtGC@y!JEqV}oT5vghTU`mSF{4doS1Vc~;4xhtH`{Day8+>CY4 z^SfLKx~MGD;o`)6dq`Ez!oykM!N-mHpj`{#>>Ul9D4t)UVj>^{Krl8%t8Dj8x{FXK z_3o;(6)5k{76jtz(&yrZw8$+Y;}KP7b2(fU9;}MoWH|aFb#BAimxtFD=O26 zBO!qdLx1zn$UVFct*1Ue`o|j@+T&bBVAn}s^dz9iD6m!gtooSDhs@8zWsYou_b)g& z`twx@YhB*m%UmyH8$-L>d=p1}-hFgo-5k4ijuz@1-p^OxxkYC<$kn>+vXYXb(qjGa zW8nJpyVivkyz82zEjWm`!4K@@Jr_tM|J7^bf@kkpmRVBbyNArbw=(-Ckv-?|osLG( z<*hsz;LzopbSDKalL86LMo}Pv0+kfFYn^02XlBki?ATT3sas;tX}GK3QUv-2W2P31 zIibUA>_x0`pS{eyhvwT5Qbe_w|7-hl3l(ig7{_%|6FFLEw-RH!%ky76ur{cOQJP;Q zmpByT$fEK@tQ^k0&Pb7-ieiGQSSWEU6l0883Yi>B3f}`3RZ5u-ZpoC<4eo=CrS@Kcxtq5)@c8G4pS1P zjT&%gw>jj)u$dY(>5Tb;O$J%^eo$KMY({N){lEan+l-NBW(JtHVRg-+<)R7e z03aI1R7I@0zX_UKnR3gN1?m1&?_RmjBj{B_6{%=(O`2rmjhmMPy@8?O+wINq6NwN4 zjjC-}do3-wol3<3MkieMc3Sm z;!$PTAsO<=kfDF18~$BJdW?)Ttqf2_`wZ3OgI9GUsY%CE+uKqJu_!ZJ`k|_*w=B;$ zsYu^;l?=!j2|N(<9z)imuHjRIKrj|_5Kdh<2p!h}&mt7W6j#FSQxg%!a3>1)iS#hV z!Z(a!D--**ljo`Vc-+rsG4tTL`u1pV)sim@Lu-op8hFxBSziw8F1d-40wV=xm z^b+i=VV1MW*%CFsb5Y4A*a2p_=-5#)?1W!4Rl1+C!c2w@fKaVm!JNd5sq6xkb5Yne zk~3oCB&!ySVUS5k0e6{a`);k&CHzdhxog4I=&2|+S#Y?f_F$kEb&qQ3YcLk*pSp!l z3tWV{Iz>&RBVAH!igA;9yU|EoRx#YTCbz(~`pUJg-1ude;edXrx&bL5w_Wkk!^gD= zt3S+*Uk;n9t`f9BDlP_9&{3qOSz6&FR71rUx58Oc{?nJ~(ws0YbC`BwiL??jCOM15 z`Z4rCx^WPu%I5zl=Wy<#^sju}3WF5vn}N=(FP}qpUxD9Ux2QMRHwQ;Wl+#dGEfQ>Z zCpHCUh!|qSUxj;ZMR4>D#xZxHYi-hMOt+TDH|O`*4Q`2IXXa*^6y{1#u`a3;w&#xL zg`F^KO5qYj7tEx?=LjClNZh^9@6;Au6YTrW+H3@}pUglY`-#iAoXaUon08b(phloh zw|*JuHNGAE6{=&be8xY-lbnOamVTJy>tee5h*#UcW55SyjP z<@s@l)~eQw!P-M3Hb;x`&D>W+#2UDzf+^1lN1&?kF6doxIb-Aj+{B`?V*N&VI2MfW z>V-%pML4N?NRD`Ne5Y)s79NS9TuH^@BFeNk=O!@45j`mqBGD&ZclDyZ(y?t z5#|(|Ej?Y&4+2k`hC>(SR;H}L5-bIC-0fLryO-ZpTE>5&#pz|zy8fpRBt2bg>VWf% zT?07*{gRl^{<1u#g-SISm5lqpE24zS>>P!u<$N#o|WzFo{Ijt_ax;m~@YZd0y(=$~#ouuHaewnoMuo>Tx*@ zB4}UVzyx#`m-#S|>(#}7AACSahLPL5(6wwB#!ILGq->tc$QP`&6r_v!PRDAsvkz1P zlV;ZP%UCV`ePTTevlL2`UWC%>_$#Nk{%NfoHq-R)Loh9s%dbx`B+8~CFJ)a!_<(-* z$ww$#>T@D&3k=yJH?|q^-sZ|7TJux#Ma+GE3`gs;JtaDK{S0~qW}2me4`lmXrJ>1^ zQC~VJ*Piopi7^G{NjcHiCLnI*g1?O(gJ54$;81k3>Ry@vK3*Pe<(3wj`DJFmhwK*J z?oWHbOjG(zIGLUH+~h|o44)K}L(dK8Z=tPgP+)TU{m5o|%Ci@pl36T9G^JWjc$SO! zyZOCaY#FGoI#)ZiPE=2haBTr`O*x&ehdu z_kXji(H%%Ej;7shs!B77ueTRmM$X3Hw-1NqUT11pVU)-N>GiY6x$0)37VDj3+FN=1 z0>8DU50sFwzypOlr#Np=C4+0yqqsq@N$>N=SKU6L$_QvJ%>@&xCCpKZXbp$($9Kkv z_x%;}-?flle;Zhc62<+t*-(@#OZuT3!yy-HAx`vCSDtYdZtQ_99^J-RNnkO^Fsl)R zDkW&HEiHL!XF??T+H>Aq*xlvLPg-_^ z=MMo)T1*ksP;9*@jELzYQMvGlh{+wb1XqJZOt(R;2Z@;UV7|>pkY)a21w{YLwm-`( z6!TTJaLe>yx~e%n8rKt%C6pd)Nw4lJ0$E0rJ8G`&^Wv?A8%B}*nw^#tEF=J=*=DSyzMqi^Fx(`|FT(OJbbCgI` z_vn9$honR$TE?2B(3+Hv8rtFp>6>kVab(C{lN3ny*~#}2`IvM*u3lAJlJRC)nJ-!@ zR6JEbs4Tu@FBln<89wknT$jFsznaas0e;XsCuv2J#Dm&s)|5d2&ge>ae;w;$b@=}@ zmy2%)3GU?5Qn95NV?vxD5Ff@PA+$Itw3v(?$O}lR$Q`S)#yr z{?g_6SSRj^@x?F(V%z}7EgSR$tXr`}WA%=i zVnU0+D|^A2JZB>WgT+qZ=A#_DO7xZk08LA>W=9t3KWDox5AKQ5qP~Oe6l>sBVb`y; zMJG_O5w>MPulEc8?OLeVof2$uIT0;NZGo+#YMVsI^-|w)yNOdNC>xrAdkUFM`{`hgbjiTVgN?lb-!v z*mX0bEH}FKf4Gse8Ub2FF3JF^d$u{_ujDYK1$xVg|1!%Jnxu6HXSrNu`rRa#v!?Gd zYI35&hnDHBhhqR&pOTA028_s*xf+Zg=J28QPji6r?)7KZ+oyiRfAQ3B`$tT@$?qvH z(?uR6lcj`@n3~px=YmoVJNGsD%n6tCPMLYE>PtIYgOQ5>= zsW;U(1qhv$y_XGdMrebd8o07|!zzADM>m8vtmYvz653G3FX1TU?}i%w)@`WcubxPq zMd4a0h5%D+#+WSO@bkQP9~#}jA}z*huU2Z|Je9Uo_S;T$g@J3Mn*?fsZ?*}a);UGh ziS7O-$**4}!Ms^Ax5;!5OK#Ne+4e|}BUF1oZv^01f7hwMkE*|qslQL~w{F8m{;KGF z04SK@M+)#G#&jrD8F{9jz$q|F1yxh74GgcJYsXmS>Z-HXatZK42Zm${)aWMZJ%LzBi&)F zMlNmmH+xEU1a>y;sxQ(DW~eDh@p~wK0b)|50ao(l3mWcWpDh3SB1hztrXXI7ENMy3 zpYPIt9d9a*IN_hwZuG8nq4EEw{-PX>nqB3*W8#d3)PGCS*gmN!P(aQ_oF%b35$ZQC zNnp)b)RLgFXmX3({v`bk_-eUSGhm-tRoQD3qnS?2Xhv%k z@~O-(krpIJSGe^F(sFY_s?FM@0ra$fNYGg64i{j=TBI*ww@@^X!#iQnB(pJ;zetza z15N8)Q|~BcpD@dohF#N(r~p%-f3`S%)emq&!I;q{}o{YJ_GUkVu})UQi!uZT|5XIxI*1+?9aYA5_O2h4#)L4NRu(S z1ha)P(@PkLQiWC_=X_agq=e$bNn66M#|WWAS>>!VL_ySy9Npw%7IpmaEDZ^#VQsS> z$ccVXw$z4>r}!x`x7P8(4zRTjSL{HT4>Cim`BmF1$XlFZ1P1T1HzlY+2AcjY@=j=2 zx&C|pvPGW1$Kv87oAm{I>?`Ja>961tRl#@73SxWgT}t^In+F6JOSS2@lErs|OgiM`Q}aji@+R#R=_?^>-0;FP zq(WK1>fdzAdc!a(PtF4ev5ABa$AIXEBGZbwOk54-V$A>FQxyM93Yn8{t3(Yb*5Z|s zRzm{=b8$z87{Q}&GtJ;h(rndJlrffo z_ut28iP}@-gIk+9H)OZk!XV6=cywgq)9i)*31VhCCa5QyCoUE8>&YjOs$w7WnECUl z`Lj;_sJR+Eh3S4LO!*0nt%Bp-04JtI9H|2rwZzFQVB`6>ia|533HE$>ezISZ(-1Br zYt271PyqAE3yF-yp$X$vTDDa1meUoUm5wlW{Y5fTNulMg&~kV9he@FqB|Lae!*kl6 z=bo#tMz@+$H#;$XM)g@Kwff`whdTb3q?#N-X#s~UN&ScupT!|opUObXFtDvFzPGG; zjZ283r$$(~aIKEGuPD006-`^M=A)e z`ot71>;G03-K@f*>vLwwde5T>d%ENt$b;i7qhaM6r}AZmZ@)x4;QMf4cC*&$dAVkR zH25q=nN>qm>TA+^X~Vb(hj-pH^P+f*Cd)MFpnHaM|VMNIgdDg>S`VWQaqG^bynvQ}ko1tok7) zX>t0VBc#lNo0!W2o^(4B$5|=WtCZy+R`?Ih)!WG+Vj0W5^0t)FjJb4Uu;oo|coq)F zr^1C__~OE|Qeo#&t)t!N#4q-8;&4pcd0)UH(J1@&tW?eR)EFf#)2jr47I*|=-D~gP zCC2R;LgG%l87)q_ghaHgb{-ZpAOEo2mGk^VwUy!r`CS7$M|~Ayt#UaMubeB{7bSUf z{8#eTvJQ>MI#kKh{6n}DLG>mlq_TEK<73p41DwtH9iC*d`kKDtY1X9QjEX&WvNwOL zrX>lz2~2H?6C)rYdo6K7AA4SsCn2PrFVY!T8{3EEVEy>M)SJFkl)X7lO53q+(kC2> zWzfH1nc*a45x1=KsX&sF={iBB#DZ++1`FIy8FPeMdof-pJdyjD4U;HOO)njq5p0-5 z(zH&Vmz}KsLRZw|9o{@IqdQJ}z@i(iq-+t=;yo0&0tO@Wnk?hcn)Hg$nlT6j(6_Eh zLfKlXFQ(y+g|sK`;|RP=%Hv<7sA0Gu9i}S8)+BR$F&bXoVOAA*rK<81%6#I#+T@Qj zJ)5U^Ht+PFT{%2ldP(+P=i4)?5?nKb9ri%Cgsa1T;jM7NsI7fKR^@i*sSnT*eK#x$ zXQqb)Ko;GcjzzFPxzmKqdun6y?N=x@2%>Jn(E!nX`6nicb9_|_2*TEbMzmJHw|)ks z&VQX}^Kgivci+l!;l-CY->E()#AW$K^$+)Ka-_JOGd-JU*k^GW%t?r2Rjw`2it9e& zMQlV*1f#BH|En!(FQLyZc>B=0o(Y`5X9B9x>g`Fs`0zRxO)sg?9O3y61nuEl91>&x z(~hSY@>2a*(zgcjcrowYn8i%6_8c}jm}Oy$`?9f+*}MQfOiz7bYR$ZqOYlIEK!Eu0 zLsl4BS5a*QL(c}oi5&fJ>|rpNVwlUWxdHsaWLPruXj&C>;wkYVkEYegV-}Aw^Ex-JWwEm=83AC+_0tj%!%c2ByaC;jG z{e9SQyl9UBl_|k^ulCP2fx6xV>YFA|-%vmqqfK}&GvV==@Qk2357i|CN%gU{U_{;5 zP?_b~e4zUs&*sm$1?kPqx4Vyy?&^LkGmuep*gpi#r!#n^Z9eiJEI=$XDuMeE;2zk? z@pnN;Q;~ZUhP}}0?>j0qJ2Qv~yMe!E@7aGgoO&ysWPA@CTTJ-aB{30EF(-%+xzj{SKMm@?`RH?|&AnPB$4F=pL`Um`K@5PE}O-gDKJ0OU2;pl$pY|3(v zevBycF-fa2Q^}{0izk-o>*46j@uYF&L=oYp5J+G%DcT`qQ-}R&-oE9-v%68DqdqIr zZyl-@tC+dM%P+0F9^c6t`6ExvSJ6o_Uz?UbD~TKBk39KO^p99~>Pv$pZjnFo!t07?MSV> zl}Vmj4#v;`<_`;hL9f=_pMh$JFm@^yMYD0#G8DiXp$tQWRRX_ro#{c0bG)`IXCIzK z2X=YiR+OL^a{|mLjax)o$?;qNOzkn$d8s;w4+?^TrOytg$-_>>bC;;ShnvaT$Ke8d zV6W+!^spBx{gN1~TDtU*VEUl^ktbieE;gl>gwa17Dx>Z3or^_WA+F4LqRv=ha!d8m z0HTbRkwC}tEJiYy7dy2tb?3+s$e%-?**8{q465Fvs*e>k;-hI(7^8t8yroN1q@=3j zloTJQH>nnxD2%o^DGX{?926EWijJQx2lT9<$-Z%`!qmWp930OSqSpl0}Eg54~sVVLebzXmKssAfgIhF+`-8n5zvYU$KHFG{%MB)Rv(mSg=9n9s@)w=Sm*4@`qxTgVXbS zWvCD}l~bSrs)K_$C^f;#IGt~oDg60!n85byIwhZ~T->4qSC+meD+Tcghr^qrem60R z)oKV{VN;4KsUXn4_yt^{2U>`xJpr7x4Nr+Zv-n#Nd{R%r@$#YH0bALtxLJt)Vy#;n z3w0W%?Y2he09*q15lKv{zzGRVb*;DCe?_plH@F5_)CiS38JD5{{N=$<5Hw+36*z+R zFk;eyT><$@66T!#v50;!_qXBOx==6^zB0sMnqA}V4o0l zOBf55)ZdRqb)sFm?7L>^MuDZYAC@mgx20fF8g^xF5FYao8lzC)AkL28(&CB4B*C+V zH&qbcsFk_3ZxNm&jHPkqj!=Fp?w4E}g)=1FDFLF)=uA5}G#x4m7k;R#(212vOmgG5 z{|u~K1*X{XAw!~nqH+!1dqy0;R9GnCSvi^c_|;A4COp5yxv8Chcz&gU4Ow%rm;t%2 z$>Kr5KC^3_HaBS@#iTC${UmMVI;~OVzoo0O0sb*pD|cmo5xPViQ!#eTDb{;AzCmBg zP^XNaCJm>0d+@7GS=c!=p6%4QNQ++=(H(#m@B4CL=VkF~FSQynJ;gBq=qlAepA0Qj z!Iu&V7s2@B)nH5#l$Yrhw4u~ymuhp8s4kJ}(#q69wMY+Q{VN~g{zR2^NFVx5boEN- zidOp}sma7WYn%|{Yf{;AQ}o_jN%uCFhaa$u`Ge9tnUq&Pn{-!SfOXfP1>YIPa&zu# zID2Pj)UMUlfa(61s=+Uh47`+b%9uN0arJ{e8JtBqCl6=qr@SNh;2Al27x%{ABPpVF zek=L{2L3jEo~(^ngJV%V!B(fi6a$o$y@UJ%VB%+Bm z6ROvmNV-(Q01%vl<~&7u=Nt4RNAKVVVDfu|YeC#dZB8$Fk|YmKdvC}``PSqLwup5I z6$V}?)nAfylX;U}QCU{`M^>NB%K&}Dpv%Zus$T$rJiO#-W^Ig~E48-&vg$sS^AhbL z$+S-7K!_~BRjFRC;E9dU1vNAEV>Qn`zh-mMF7J%a9hgf)2hSy5s%XocSiB6(rJ;Xo zE+t$rmlA^K&m|nMqcWG+eHw&ZUfFH#KKd-U;qa_St zp{3EuRoI`D=`3)WMuS5D4HZx^r67VmTaiouvy}NdQO!@s#8jlR*WyIupK79FvNi?5 zm<#T9VkyDZegfMfvB^Fy)8An22G=Ip$^*x%qsndboibg*R+Jk=3=v~|Yxfcp1W%0| zH^kg6&h;{b%cOXD7g@D}6mOi{N_oaiN%h7hSt&m?Q_{V0DOO6gnKH&3mujVa&rHei z#-&>+i_DZPZ`>Fw z$!Ms@DLw*XJ+B^@%Hxyr_>fQwpQA|sDR0ru8+p^uwaVku^7yHGd{!P$@ECsRB_5^9 zdsJbwts=dYx8RQ1;fGGiXX?&ZpT|H*FZSWB16+_)#JD2 z@dxVhZh8D3kHH-iVu%Qy8Z%e~DDuB>;R(I#5=h~qG2CDquW=>hLWFcVIBsN=ks%d6 zjBO?CdY$@gSYq7#iBy@6P!=9dndLe0cIpYwb$cTcJi!0`X*L&;b4KXKz0B$}ESIq@ z@o&7rDHzJ04nP<=kT|p@{>Txw-DbDe%&W{4F}Mlk(@(=S8zfH%;Kt3Q=m3|5B{eC;Bfo{%jh*53`cvRq~d8 zW~Nvy-m!~9;KE&)8Dhy^EdN>N?0VH!H4@0+o9KK=Zeh}ju^P7ejdk3>hG$SDFBiWc z7lDz6VkE-oj^j?M{0y@e?BP{K`c1J~jJ0@;gjdS`(>!ES?VD>x)~6$#!ZXtG;-tE< z1-DY^$5d1|toF@SxK}7qRgM^@=zE3SDep79ubSg0-CiF*HP_$O*H9Ns@$=)SaIK;=DZIi-y>I`^cRiAnQ>O3%2HG;?2uIgbTDvbYwsqp2;Pi__T)5}u7?1e7~%RG!rbk_{Y{)mBmSgrm6yS= z6VploymSJmc8R58+)NOFBE8~uCK@5r5z@3x>*VjAQ0;1dBOR$)M7etgTWx^^Y^XPEA=%(!O@!hpw##be zRof;dO+Q*^kEYT>+p0(#Vfh8(UsU@N@A_d}=ZcGujLFiEfy?r8{oHq8pK2fF8{;ue zVDk@=jk+oT1u{Qq>!m23n6*-6ZC9vv5_8S7J`&m_7hKN>g$0OE_!?fS&*eaPJU{vc zOF#I`IyhikOYNib1GFvbHgA|o_IyvxQHU9p)+9?VZ%wkQw6>MhU<3no4$R^Dy`mWD z)*|>F9UB+XW7Z=7ki72-N(H6u_KFs(kGYT1Gn<>{-4d8-_llfDNfTw8OOmBkFvC5&dx8&t>>i zI*!wo>}^tscbfnt#+c5CvoTa#!y7fN+#G%|Iybbfj#qO8xKys@KPbkv@@K;eW#Oc) zq3}kj_3D(0maZZDf;YpC|LDai{`B1H9y|l-k&_pDFq1FW8b$6s= zEW!B`TSJei9JhW;in%qln2a5vO;Y)*lOaQ9)7cwjS!iEGK8@4532Lj<5-)aghIxM+ zM>uWwf%Z_%Wa(nZ`_f_Y9$w#IQIW8#mWpgzO`J(SkP|3jTy~H5i-BHF3_c^1-)b=U zjO1kpv$|__v;_7PqG8OlI(qYk9mVT@a7S}?Mmokh8~#Q=U$^>s!{6`c?oYp_pIC${ zY8NAMvVg%T;96u()-`1A$^R7n6ds?mG_^zIyy;6Q52+c<&R5q%)7=ncMUi%e!hX;! z{ODhW_V-+|!s)wWp=Wba?XN~5tKEDc%~BRpEWAm{zHvQeUrjSheW!>Y`HVIO$u-~pQcLY>@>*`&J``)Pj8a)Y^gQs zA^4`PK-=ZrB+EO{4jFyXECcyi?g>4j8jGr0j9(1eS4@BgA>0D!8WSKc#`Lip=PK&z zd03SE+BRvI=o#$}1+0aQ&^Bq=>Q!jH)XUg?uuk<0ER2%vL=a^n!79};3WDJM!c&vU zuhm;m%}8DbdMsoHLj*;DyPsCAORv=fy`Dp_w@Du2E8uYU4$FC<=NFyV^NwwN{CYUV zXxOWu4sQ}{c>c3tS$tSwHnd4LHsti9w6Ju`+`htLK|?4k1kM;6S}$9tIWII-jX4=Y zYLk;gRH#;F0Qa`nTqGoK>T9;ZSZ@!E8z>Dvy-lWsjTK>L>-d0FxRUOWVhTcG>0hqe zM?E!ShoJ3;toiRHD9MQ6)0<+2g};(kG}Wr+Vow#&yR)`xss?9`$_mn)CjVl-YpM{b zGeYZy2PrVy^Q!g&KSQ)PI~q(Kr-S@ zA&F9{vD;+eDC(&RN*5Gb=XlR~YCnNcnbSheD3b_CgVuCnB0ceAYk@EhD>b#)vrP_D zJ$q`0%cL|HKeJ#bmN%L!e8XsLGHOkV5cn4v`E^~frizT|ISm#88k8EYeUvN%4QHAS zqid;!81sqkN-aw4i3exxm`9V|CQpruBc>*bOvgM@gU<+U!k8vDdlksm4AvBQGLJ*+ zB^$A34#_MTB0W&+l@;s>N?^~>qk~p)$4Q}khlKLU6x5)6vi}L?d-oquJ{gX`LHREK zXn^ubw!cmJ9z)st&rv=Txw0AcBTTkbi_kPZavtS7r|5xDKDB!Z<&y`Yd@_s{<&*M? z^2r0ZQ$0(naiBUeu6IHcXl<)5YbiD~sKjo9EJP+@X8LPO`uc3U>GBZXR3Mp=u z32SJoUopxz&?XsWhIM=i84$`BuPC4Dt*7R!qF%8c3q=*mC-pOTV!c)mGGm1DDSEFc zpAa__TWZF9hkBuY1HJEfLRwVtok!yo*qb)ehDqb3^^hVQVa36R6tgchPKZg2#;IIt z!!x(u^J!cnO%2evGa&D)G!B~`i^grC-WZLOHas;i3c3}ITl_a^oZ9e(#tG;HG)_?k zuw%cFxD|xd$t*_Pq@lou3y7OQ@;8W^eEK@#Ch6x9xAz5hg-=D?{wnBH)9uVMoPz!I zQ%!cC&G4w=Y9=Ydd|~nbF^ijB(Y8Gel%Z`e@fY^y2mM{rYSe=8CEVG4+&Q>k=k=&f zGfTed&l~8I=lnih@ejIlu?bIDk!9AgKwhNZyaGW^Io?131`ysxPwhQwDJtITL3>nj ztvxOtGFcrW(Lpyh;R}u8UkqxIoa@#?Wc+ z#N>5QWinb9&aiy5vI|D^e!n@QFMxs;B>{5-bN1exH@i1g2{zRbRy9u2)JJdYN{`5T zlj{$dXfuY^rB10=7@TWL(6A3MS6SfI;Mt*^Nrsj?&zfL*w@EXeTDez&$WPYB7a&lN z-;r~;YTNyMwJllfPmW12<1a^Kh8$aK5|1hWei3EUO?82X>*<(+3KKy>;(M53i1yq> zL*|f)u1T(p3ApMK481#21;*6#iIURph>doz9PfIup}@%D6Hm1}*@7dsboPe^?v(h;9EJ*leBb+Ix*6?H*@Jz;lIU4ovP zC6c}+24OIiu~@o#?{`i>_b3Li%FJYB)q{2(xqre)6o(pzoP-;k{gGaYDU^=!w^|{r8n&)S%xBdQ2Nk3>bPad#oF1BMzp|)sYL7QMPy~dC zK#*=-*C#GGVu?^7kmYpgJDDunqI6>P z2to6g=2^tcn5E)nC}pxQWoi4*QKTMiv&?whdQF6hFUnG4TX4&nS4FAtmz&WhTrber ztpt9;cp(|{myuSSh!^zv;~82pfpJFjPsB10#Ea<9XZ()p)mt#@1zUmfM9lC6p)`YZ z;=-5$KDRzoAR17SmSHlk4G{pao*1v5;&~b^M%-bkeam*b-+37x5Mps2k5^j1T8|k< z^oKyQ{*!0KqEwW=#+2HftFpYw8yg%c%g5l!b!>V>kH2pxp)_hGT7hTtZ1;p^msXGl7691qBCJs2KopBuM@AF>T&Pww>30P+m2v&0PJ zF{d5x`a{sMZ}mj0{x!KLG%mb$N@&R&;W_Osb5I2)Bs9-Cm=Id>R^5_!2vE6`z++2F z^yg7IDxY0TbtRfi^kw5q!oQMMSb*3D4YQ&E1Y4%ys{C20zCuc2V~ge-i^sf_ zi&iuHGg#S|>D7=7-%z;{?B)NW2N&Li)1u&LQCh7C91T;P zE)_%ryTJ3~h2~v@p46Y;n8g&l(wc(fzB&cFn1R?GNl+JNVdTG6<@y`^ znp0S}|B56Pq(+QG`e~xF-4w;BHBU=QU&*Ut*X|NcA^}7j!?!jNV5QaTMQ>Yy?YcudsRMtCWs>gc@zURF}CwaUTsvNJ(te_F_nd;*9)>k?Iet?Bvv)i7oP{X-Ogu>9KWX zT1~!!!Upey7W`5c^k%-JwMO1|ydtH2w#8IK>;9qMU49JXdW|8F6M=B6^ayw_SL$c~ z$Z5(vOyEs1;T@`d)(@-%&q)Ea;BhXbOvJt!u;sPg9XaL{z=Q3s{U0Spf*;zQ4UrFB ziH(UZiA{<5y$aZv61fmJP-fj}=^U!H=?#gUTLg--%+|;!BXgqcqu|6phF>s(TRU$s?%xCCF`T}Iric|2>)TcuMbwn2_yo8Fq(-Yx@SByG3l3AVROU$wWi<{)>( z9=Au1JG4F00kr5m19tG_q(l+ZB}O9c&P3#ypg55*%Ka#2TD~H!3FH{xed2(QH`s_G z+u*UiDu_?))FNf{DYGf^$uP9RKHCzIK1uapx9Y*rSJluvw0E@T%to0a=KH(D?k(3GzRqkPZAr0$DGcF;7L~^k!-=V1@!=~70>|OF*2eL%iNXONURv= zz#5Q97iGR7_|9Ss`T28gf%dk0+cAV3KYg5t`Zs*Zf|<7JPjahfXL+RyXOLvi@44v{ zuHje%>_V}=3og7|+XtOFKAjFnk(V5IB<|PtM2^R^N0?7CjX_@G{>brRiH$NHEC`EB za~jGdG|iDpBG6!O{~0oaPZ*Ju;7PZ(E3uPF#=F*NO|njTa?x>`vpaW2j=K_1Cw^vu z2z*f^LBUtq8~kuOx7q%4GqzP`>`s}n`xD<5p@mzfK+1WLf+?~5&_4zx<-Q@Fm-m4& z@TQQa6}DF&7nFgOc6Nrw#9j0$R4CY#LXH7EmQ!-c>haab1$~~E8_Cy?(dnR%BoqjR zBrfQ+kR*e|{sl@qu=Aa+?T7Row|Bo&Hu_CqP*Xai`;gBjUhOHYLn7|EOdof*`Q|p? zT;^NKLg5jKp0|$}vqMJjB(T&mMy%ZI31%S2rw7{`&`Rc?0Q|@&>A{oJwLPqzV|EJI zM(>mskI|ypUaPFC{`S;ce|zf9Z0}@7Vr%!&C8N87Cmo4A7Vmt}MQ)Po=X|23a-~CgI$oqAi9tiS2M`Ll1u?Tu5k}OhI^x@f>_F)Av3h#DAm> zs~*>53{1?k>)&2SB=^ChCJfIG!^OVcQY*Kilg$=9>sWT^7SA8{I`@RaZt4pLmOmibwz)6PCoDjhIE5AMKZcTIxUY<|Y# zS!L5y&CYJ`=@t7!+s;T8`&pRY?1=VWENA#}0jEAGB|P7*ZIa6CXGZI0kEFG_Tcg4d zzptroo0Vw_5!rh+1(69lfIfUT1v?&O@$J{O-JU#&&w6wn3h=QOzq9@3Zg`A6{XvpLac& z;NR^E4~Es4s&9VT0QLv9pw&w6)=bt8QTsXnM6E??R;}%;|JpiL0kZ}Zx7s_Q^=gd! zR@`h12|t}Bu@Oo~eb^EW}Ajb#T4BS@=|qA24QG^{H`F_;Yr0qxrb9K~v_d zKp)mNNV{f3+9s*CzkRb`+V1Q_p|Aq(y%poi23@eM+F#pdWm8!+b5vEG-lNNIwGe#3 zpPl`=cBmfLNDw?yO*Lzx8Dh-i+=dBTP?WuuDGnM zc+k)&PzGVg{}*F+Y|_Y*w(#3ueh>Zn`{gYl6=2#fbB<>3VW8D)_C{7uGbb6|_UT$! z)_Z7nxF+EMkkcLSyEK^i5wfL9= z$v6S4!X4YikU~A0g)mVv^Lpx5$S3O19#M5zZTwhLg)fyDNRT(VJ0TUj*}yo&5aV+3 zcbTBupkZb_s&As3mb=<Lg~A|6LI}oHjE06LyO=^`~|`Y)oV7lN?MObq<8`f0UB=wkG+CnrGXiwuMF= zZRmbXYJ8V>d=D%ruordQ%p>r#r7H;wf4HJhXoMqyYqEolcF%LJEaS9nWzqV0(r~G; zd!BM-Ek7+6gYC$OaRuV%Bpy^ciXG16DohI%PZX7R!%i-_QlaTC z(%&8UHk@xp9DY8_#I+?~L@YS)PJ^BxjS!{&gWQud=GNQ=?v)9i%?{2Fmq^r{=Ilo8 ztzf%7d*7`WdhcqDF&K-B2lgXIE1dwZhu5U4fM)sZH|5@@a;|~F*8uO@EJhFna=9x( zEwt89ewIY>oFF!HY{gP0W+{q0#||MHSc>+De(JmId(MV_Gf_{?w@8SYiZ*!R55;?F zY|D_Csp!WW7>*t>s2Az`GO-VcFKr|<3Yvd%x zxA;Wpv$6Tr4zWRA@`^B{wF^>+l%Jm=C7!2*_t2UlT6c#?&2x5QV~JVgnEtYEtE?HA z#&|pI8r|Oyf2Fq(Ti57Kv?{i)4-DA4ULze)wyt;e+q#nDb_H4qMyx$C;Qpso$%b!` zFS+>{fy;`DN=gACOP@!_hzm);S-e|I_33{R7PZ?LWqR8gZQ_4&wIr%3o70tZxKtmd zGBt(oJ;kDePssVRK6_ig>Fa>|cU0(IU;^$tU+cbCq#qStZ%5XJGsog+^xL*GUw5Lt zcr>C#%#*yD)M-4Pbnkx zOxl23xHeMB+`2Iv$pNGROE#?Yc2p#?8kfp9~Ge-g1tDE|rW8<$wI&qvdA z5LflXRBT??1%?nU@3e$mWaq!+*=+N?R#k-4LLSb0+4%=U`CG8k{K;5O z|6I_aeW3+js*kUmJx!le;61yV`w6z-yf&gNoUc10^NV!%0oec!GHU@L;4)4PB5C(*zTI!cL9{-fc-I%K4a>VlQ6u3}>Eh88`NR|It7{71oZLy`U$ zUDkkzjxkvgAoBM)^S2la*i$^O)d`?8=&X0I=O@2aM0kf(R~0<#3N5M$7gmK9v{Gwc zu~}!US!ea2I$xuds`DiPp4(9iM(XeJOzhg>@{g^b?YXw<#23I@m)|b9K32ykTnX`d zFbcH%9iGhzasvU;q{qyy{Dz7f*{a5Bx(N$C@HxQBr%y{K0&x|2>Sd)H0^1% zCOtJrWNh)g+?8Fp5x-Fx-7N^CwfxrH{HHxXYUXoQ<=%wAR4!aBK}_&$IR9xC!S>nn z>k;R&VCB2yeSjuz?MAvVc=Lko34i9p#gUa2Ghv~^ZsVpVAM(1_9JgaF{NyX9Hw1|GisIj@4IhY2xcrEs}idNl2X8f5V`H5}?z0!y5+B`KBTcvH>= zCwi=(7o>_-p+~d|uvBgfqd0VYA;byl>)aoz?J#cwQZ6Z-qg!q z-qVR$IyE6MBs$C89G_gk{VU=8FT9=0PK0YEN2o9*yECln!Gf{WOe9XdH7oH|R^RNi zT;AqoTv?W)siwTUmTl-qrxOl=a2$(8t$69Cel}rCigZoq`jUQHjI1o_|F~=cgwJW` zy!$=D9ZV|DaC{8c7NL_QEXie_mpx2nZX^l}^(Eg~5lSI85xPnKh6{VVk!6R4hhDmX z*&hze94HQlrD%49=qy^Kh;On5>E5l(T=r>bQmsxlrhIYEVg61AL*Y}TWH&*c)VRx~ zKAQx@{t0n~bAx2z9LHbpp=G-{Q=%y*vcNWh*7KtRGWTOFUZw^oGnpEW7X@P|lk$X_ z*$6)=P{1vWW}9u!%d!XW&ahc}OHs0Q2*tyix$YPIn*4(&7#6%7MIR%AHVIyjT(Ms- zV@KohJwkAQz(OYONT~7z_Fn*CYF)Gi?On4S6YPPDReLBk^U|dqX*x7F!??I#2SQ{i z(xLxy&2s-$)d|uxw?qK7R(YaS|FtyJL|sLS-xsecRned>=;LaH_)F-o3w4x=#syDJ zk|m`sGYu7XRt+uus-T&Yr~8U-h#cSJmZ5r#k$usdjo0>E4F|^uO5va=HX98UA4 z`hw>BI~c4{mGM4NlC52X$jDWz-B8nSd0xvWCZ2z?I4zCkkEixGlpkDt6H)-NIe&U^ zVh9<#(J?sXMF3#?ndeRqS002KmdZs;vbl09=^;wLxxog8TKf;YO5h-};%A3aN-L33-^T z=J!-h8RZ{iRWXMC$l!&RXLOWNC_7&lUl=8*P~&e7zIcCi6PZ}J=ijvVcJ}(SIOGw9 z14VorgCVyr>KDf`VXxfL-o-_mK2Evwe4&QPWv{VlQ#eL z!BcL}@4rp-!r57_@l|U%sjetcqa!TJ(-AdPS!*5s__m3y3mhp-xX!iy#WL@z0yEl9 zwe4&>9h{aWKzM4Or(`&}p~R8$%4jll{DyZk=f%ds-$3eY1Semeg$!ooc&Whu`jJD2 zCstAI78Z!jxLofU&d+Ew>+5FL*Cfx?k_Xn<*56j%`EBXBST@`l!-uElPSoK42RQN6 zUPS@J_PJ*b2(0i-NNcAT{I>G1r^2&*-whR>H4PP>JK}72$cJsYmJw7 z@-?1pmr6FWFfCNo<^m!MB1yaJ+u+tu&Fj&lq37#-)v{Z>hgM{u;+rhv(rI znSy6w`d{PWroEn^C}+4xj3_QJ&bj&TE{`YhAAoC>dbY%RY4`SMm4}vJiNAJzdR|oG zz38;uFtsYMhBjm)IUb*gkspQIm$iARVHA*5s3S?r)vZ$ZNZ#T*u3jLff={2sA&^4?L)JrSVH5xThhozl^7W-fSlzCJ;Y`2=WU zcfF2BtRrJOs@H#OF%NeO^YH(`WkS3#HDaJL**~pemR+$Z7MK31)k3~x?=|w^)G_`^ z%CeXG-THdiwQ}P_d@CwX=rzBf9WH5tn3n3W;S%Fv@UEXu5*zCUx0YyucEtnSDWwO@ zj4Wu>bzyK90*(me_)kw8faWfsDT6<;%#miQrq{xim1;fuEr%7+RcTKkRW2{m|C4T$z})^-Zt2DxHyVkRmnt=C_SwrS_MXk#hW_c z73q`M0Lse^n42Je{-Q}GiO#TGO>Hq z_LGV~eFh8Bvhyj#uXhSg&fLI)fTU_F)b~ux@Kkd|8`}2@wR`Q_bvZO?*XhX-VTgNw%rX8PD-}r@k7B3n@wDBoJ%ryExrqb z4)O6$SHBJl>ySr>KdZ9h+OPQd>ch`4?}`#8+jYr zb*{1Kp@IooYv!KFF|+(mv;0o8{7$p{9#wvC;w~++#F$#a`pjwQicBYB>C2H!>;znb zIczH2f=GUOm)2VyT@h)%*+M7q#%3Pd$m4C5J#>a!Z(Gr;|DE=o-1-Cn@z_IBLrJOU zf?OAlEjU?Xa~Vd+28J5N-2xW=mqH_q3RrvDj$)ZZDnal@S4^ z;7NPrq>02mCKC6UNZjLK5D3*|c4Ciof+5p(W$yN0eSxV@l6N;|w0jiX!}z5oAl##1 zKdxZM9$Qj@pNbUx{9XV*DK5ASWwhdXth^kKaUa&h!_QGyMf}N>I?3SA9(8 z^cj6RkD zDlexOXAmX*)4c$E%6(l=l!a90+h5ZYwUEEk6SE)xOgCC{niKc@2Atc>>13kqa{xtx=XuJZl!qmw}b!5Rg-Z{_vYYFTlP$I!d+u$}-KG{crg*837rg zk>0l%g|`H=aBzZIMp7`#*lPQ$kAr6>KbgrLh)--WSei=~OU*7dyr79-dA@{bS}OGY zDPhZV8k&^a(H%U|NdyKsJ6D!Ytq{`+&qeTtLfmYyd3uE>72b0BUtls{|FfhdXDBXn zLfRzr_tJId@Ay3PcXE;Xy(3cLDW$T7?G)T$8{f&j}-OgQ~d9FSUa9xpO3C_q? z`ltvp)|)oZ?EL~~y9p1B>V&jT^;`~DT7H-X~1F+x-4l(trDa3I7M= z1Db`slxZa&^_b;SC`Z~p@^)8v(za%_Hb>;o$I7rS61<-^v6VVE>jg;IcB^#xrf5?%z zRx&lc(jk-kTNG89Kl3aG%8Qa4`1Xp7uE8i?l>CBfxjgd98*IuQ7sEybBX)32vlz1T zclk{fp0b^k-ua5~3WN!w6Z5kAPYCkh4mQe9~SN9$*u_5J#f+dewFQ$@aS z(~m~FYQIA%yWhpj>8{4SfTQipgpd7>w)T#_&?>!v1(Zkxw(CC*_OMxY{e`-Z85=tc zK6d;DY#liyhqw&J)NH`f)b=rXPCR~O+uhXsxY@vrz?*HSo2(qMQ(sfU&Ok|1+bP2# zMR@r=c{=_>n%xW9q-GL_wS6KM#qlOou{z8q<-^B(=xA#D&GhnU+l4(_&&S?@*6P;_M%#8CI@X`o zcC4*as^~q`_V|14zY2_MYj`(#K9xxt6_VypR_ElVEp*&w^r!=|ak>=Uopy%5ZitNe z_wgfKptD5|`8!gNRJf0D`3!Cpgcj|%mTtt1^br-I4T}ZF-^t%se=mT15L$mfFIL}79M~SO4@+P69ou(yU!*>g<$2rl8UcG; zLO0)W9tt+46wNQz5>2b7@;etsGyg9%+P2edwe1577!rP`!1!R_m-}9?e;qLD+Xun4 z@5H_n2|NA~WV(l$+_p2eC~je%#GXxittS0rLA@^1uI-bib>?r4S_@4_4)NZmH^qwF zlVv-+?O(vn;or&MuG)X&FETF+an>xgy&Lx{klUoz=Rn(LdtXyj_5#!fY=Kc+;cYMW|gt<{JpZhT^pL670;`d;CZ?=F>)-CcULGW>+m&M z!5s-y$t;}dYC@3Z9#n^2B{W&0Eoa*`S$8%mG1)XEU1JfJW%$!raOxpxix73ajX!rb zshqOLQnN(Th5?$Y)kytnntz%JFV_Vif0K%@mihvh#VSr$6$j5<>K~&2P}Qz48ffH5 z+$j(M=D-+OJofIT@A58GU<)hDw!G!t}9K|Ri&rX<_zS&1$AC){u_9dhXoVy4)uNQroM$)}Ul zV}4SvS?W`}betS1l4CeYLEYoOBd8Dc&r)^gniZt-nL`RTcY&0a{B}X>5u{>CM;7Tv zY^cDGEb2TGUj#}li_EvnJ7E8)SkQ4W~6Q2V(%-KYG|3~rw!2iqMyTCc0El9^(e{;zAV zy_p#t5AXZD-|zc>zwg`gyXRWx!@bV;T6^siQtfsGe zU`nPQ;@+AVFc8<8@v{KF8)I5-s>5QXd%1l1SjMn&+lZP&m}f>!#p`RDd>?#6;BMi* z*1)^aJcE(fc+BuBx1E?Cz>#d04_-=}gNYI*Jhl3oE>NAATo1QsbSF-qs=l-{;vxS_ zJ9M@-^j5XU?oC^}d&-Hb_!E^iTfl21dg@f}UE@$tAw^+HuZy?vzf$$-p>+%j+9d{3KCG=7oKjU*zITREKyFq(+ ztsx_th>bi(Hc{Ezt9d1p;xq~YRe6KF!IoIFO6C;aZ}b>+sW$dCo_Hy`?q>78RJal> zakDkYMNqa*mqJtU!s0AVO%9yn19NpX>GX%olda{6HAz%M2IeDg?%O_9PgNz>ywB5Q z_$Kr(vcT)8D)9O$qE#TaG|{I}+US+TpeUb|H-+rtYwTKLD~T`P%KuasMQOnW7(pn9 ze=rySooG?!^8_T{><-xM+(l*|XW1C!1P(;iPGI zoHQ-k?Kbq6ye30`%-3js9=aQ<9Z-EWv>F-5XKxM;+B3A;>J6DBJ~XAdTvfbn+JnjB zb>Uffoy|QHJPOSWK`p=DHW;~oBW@E#bS(&cS+aS}LHHDUyi#!m_wC`f``2V^yzlA5 zPmDKJIH!nDdCrx5f#~o0y>xiJ?K-SId9A!HNN~2OzKN|{&8G;tCJ%o&|LD>K4tK~m zwXb3O2zeFlLkK_?Oas44L#^YAMkFA73B4I!S%vh=OL?EPb|}BwiVwzl;(aD-^F<+d z6S339?WrBb#+srYvd1E$*O&BGi#KY%#4U(a-yk8DPcO@2JKrxO)wfqBPNbNKryw9s zI+)Qt=4$sypN!_^8F=xxDyQDTCC)u#Pm62*X2*h}!02N$uD(p^07p$>a$?5Cm!YAA z4Q>+Ph9(pncvt<(#m$LI=ycfZs-=Z8xJ`VSuNOHc3B8cGRB;C8*i)yLme|HM7|rEE z0X-VJsHsy=RH2)i-{*LX!Ur{4ctHn-s~F>DC-vW-r4u^v#0%KUj^2q%Ztls0Dnh}n z^L39@oyI(PezsxU60zB;YAO%12lSlpd!h<+GEZ#^MmqLPG-sq?7~p*$`yM8o zsGjBZ1@}j){_nN>Z%+Sk@cs>cUy=PPK|dpFaNDV^ACF-AKK3XKs(w8YsTBzc^IdE~ z{i>{zQ-!!6O663zb~H(2?I8@^QR^bxluNTS20RXEOCT6_|HH+n5yhMfg| zkyFe#+^jmP+Vig^|1be@w`I@L*0!A08gC4?!Z+h_N#iW9XCFUp{*enq~2h?vYyXMvKeI-x3yoooxk+#Z|tGUqX$#6sh&{U0ebcrYDQx0`tD9 z>U*#W;yu{=#CxzEaW*0qJy+GQ$2*`^ditaCqRl=OT^rS4N_}5pRXfnpbbe>Ww6l{b zW=d@9wF*{MpXh+oS!Z)mB`@vJ_%6<%B2tSF$njcgjPyADw(Y#f390)PaP#cah4pN1 z5x&L!VU6>tm=YdEe1Tvb&bD{%8CqA{B6P5T%)iYP* z+VXeur5##pE3jIJ?^oz*<_qKVyiM{!9LMGNpE#azjJAJYObph_dnuh9r!V+4Z@8Gf zBz}o?ZQ0T&F`waA>N#BDLTs-upN55Z5;lnNEluj5g}x}t13N|7-=hJ#X&T5|O0vOn zVIc1<$p)ynbb<99H}8Fln_vU#a>bx9FYIXy2*a4{RouiKU&NM8yi}8eHHwLcYJy!5 z#YjUVoLck3b-l7t_Z={)*ZC9q(t*H7MB>PL<(CrHC-_3W_ zqU>;};VMHxm(zWlRVQBWruS~L-H)}nzC`x^3$||X)pnt&#a0}yqQ*id#&6qc&p}>` zPFG)dlLoLnypwcA3w4sUF9S%?oyH(k#T8st5*L!*Nh$cprS3&3DX{XYa<58?!wd=G zhUw(7K(jPV$AOD}PBi#|{fthUo|jZh-VWRqVFq`tKN4s@;ofByoPM8%MepY+Zqi zTFP4HvgOGr58y`{-yqKhhgtw=PBLi?&0{fITa}g?p z0dX}F&K8JaA73g6;mCy%R$U`;(EXKjzA2o~d7r>bLh_s{F{5~4OWdmP4p+mr`RGk> zUC`xi@>wVkTx^qhvg!-=kEg2918^SX7JezRrPkKP9p^B89y?Uf1RJ7Hk`ejS$4%^N0{QH`HhOuZN zHqGiDL8034Ef!l_OfZDeb(=TzoZrrXge|7!mU{3Zqx;tx*uM9e%w3+ri75`IHyYe! zjSs4DM-?-ije8cImXMPSB&o9;9Zsal*U?*jFIVBs^wTBF7Qr5F`a5Gj=!Wi+PW=*(`_w?t(we^{{B&jQgu2EJ4IOlp58AZa;u$8x*Zs%pGvCfsR zDUbsDH7~OT7Gvj*rzLT5*>8AfGa|qXSXaE`ayra$j{(vBVC@*FF|maN+=p%so;`T% z;J*%5Hs52rc=`9@I=J$K3+(b;{@MLtUdv%{m+zJ5I}C2;ddK#G#a#&je)xtc450G4 z{Qc$1qVFZVuY9<;it#+ST`PfU>;}ISLDBeEk5YvPAL*(=%PMM60zW$Abpp+K29TUljahL1&MU@hE3U~^W>F5MYzcH}$ zx7&4s9dp8>^=AMLIng#0Ez`=P^_PpbQg(^*VZIO69;}j!_85v5RROL#$vN|H$eEu~ zv|3TL{wUB+a?#3#98=KrhSHs2Q_W^Pm`pZIToxVR)dy#QeBHO*wWs;Esb}9swQn$O zs+z8VDa-LJGY zwLY&4&eaWpvU8}VQ^w`0j78fdcN|_gUxxHZN_w}jV>YaJJxb72dypGScU7=ytn>G_~@~Ij&f`iijGT zFuI0`s%($DI2-USSFE=ddVKorLoKG{&~YUKolXl~%S~LTAa`7AUkH>kX(cj2|`n})A| z%#`4oNja<;67K!-rppo<`ncM<*vNEb*8y81rC z1HA(C^`J7)AyW*FeGiOX1)8H}NM$xJgnR|5Es8=3fc_ z*6^=_f7Sd8j>BJQ0{+bWL&ou!jz44_qsvVERm*?8RW~#Xe`WmB;l5L;?jii8Jc_?U z{+02snt#}*MjV^*2c{y9@9>v$l-er%74i?<#a|u%_=Jxh6EkMgpn?6;v$Lrm8y6KF z7Zx|RUqo~?^A}B`8136qIh9o0Vp?1kJG-^9q8p2LufW z3RZ( zHQM9Labg92d9xCO@X~0s5meY3wG*2CHy)3t?P+!N9*U#qsQy|nyb6PnQt|_0y)AW8 z^npNc=m!ItzEB0Dfml&X(-r$nAf*11Sq5~6_8WBQpKLH;6N*F0Ak#h zwgTyXCy-UR9LU~fuYwhd`4L55so-%1tAWhNIYnOwWH=WUeZ7KL6?3VXu=h~V3kV5< zJN4`g~eDSDlv4^Z@hioUO+4_0unf+33ia0NpZj8HI2!Dt2J6ifgzT}cW~ zSIkosOjj@y$nvr(SO_FPmjKC^6+pZzBdt^Pn}B#1McNDO1Uv`C7OCXZTAEf1Uy|{VRbj zfu%skvlGblodNa$`n1q!1Av2p)K3R;q*wx^`?WwFupEd1MLGuz1p2qsXzu|=0Qm}F z3Xt{K3~U8l3}pRO>t%^zz7|OLWkB-l2$1Qy2<+hCUafCxeNxL?tp{p-Qu(j-)oAfz zpOl=OimoP|NrXBr-I$gQG#e~whHPPGv8F>!O>(j|Gm93`BqwJWELK%L*O+F_%~PB) zoNQH-oNP3tX3@PRKP$tUJU|Xds0Yewvo&wXVA$swE!MoOG%MZOOor^NnI>bpq?$5; zI+HOUAnDT4L0XNtuSt?dj+CU~!zKAF56_i#1x=eFNx5cYo&jm1`yjX>@B5&jf*|Ul zw1;KJ`Vutq9;rW&Wr<*<;qVXV(nw%mU|?@adKma1XaT4vJS~9Y9@zH*1%dj5_|fi> z@H89&^n`i==s{4*+zhKBHQU$=epBYAI;hF5UQ&v+z--LTptY2eoeM*q#cHr-rRiqc z40-9ooQyHe6zaJwFK_^6HZmQiDT4iiEpX^;3;T3qhQXF?)urW{RByVp>|Bd2Po#@w zYRJ2}e8?XbnVXeowITQESr&7)Ac8FwA%7aa&e?YMoB4?5)#JR%dVlc zm9kXyt^;I!W?dDGH&Q93eU)z%RvVR2G_M zURq{>gD2S1J~zkAo>Mm=bryPT9j2Y^BWampFy)#Ga&m1J9m>OKF{266d@#ldGiV*i zAep5s^qx7A1^@X_n}BIju9PPkp~`?sDpYy0eYP|JW`Zyuzl~puRDjUZAe|4AgwKIc z1vA;wY&lF5{R8BO?cyKK%*A$7h+;WW;YcnmDX4=W7*{0lllg>CV&1j}Q{6;z0r=nUo z+Q|uN;)P9HFaEo6e#F(FJWZq~UNXs1!nkQuucl3#0zeE`IHZ57)PLFU|HNM) zO529=%$6{~;t4d_va_i?(>#H^$67fp=)v8yGGE41a8X@5{B* zcl_2n?QZI&^iTdC?78#M6B+-w(so$3ynaCdRVBt1eMhApb&KrwQv060CcFxlp_=EK zZd*Dfpwrub?wXlXZaJRwer+BLT-vrjz1Qn$U;D2;-{YZ!`^@K0&F(VtU#pWjc|@vA z*LLii<+C+oV3+&*xoaXH=uv-Y&XjT6cYF_KX`k*_>J|Rcy9+vYT^_ps$={pr-?_L~ z^V$VFR`hs(EGKQD9Z!6p@ao<#4O5T5_ull~J`tXO%xt&y;G_>X{?&Wg(g&k*iN3Bi|i5~d;?!=;RM~ypU z==M(42<`CC^M3#78Ef0AWi8&i`&@R_J=()RBQ)(}ou?Tln6mD9$iLGMaUOjJpXmQ< zueh`q=j?f_-Pv)=1N(Wk&ozzhXzD<2hYsu+`o2e8Ugs;{9$eA;vwhPtp6@s8^s?PtGwRmczB=}f-{z_x zPUZO;qsIs4x2wLCv;CRyi3j#P*K6{7KYZ8g=h9Vg{Sy-|WM*8&xr=p6x<9fjr&&_2 z$Cu$dW(3todA0hr)6_M82G(8Nr+>3whc$D)+xkRA)hDK{wc8l` zb;X>OZO$Am85%m_2mO0ja`R(){`?^ie@WT_li%|`l;rc)H$4qgo+#=xYum*qY;$WO z`W_gwChQeYuRSB1Y*|xNe&CnMWq+RjD8lXh`iFG^mL7Qr7hTzO<^#{RO+!5Bi))`hoIh-A=MRFqjM0U>|NO!Ie?EC^-^tj= z%Q7dwHY4k!rNdhX%!$MqN-3;UuW{{7i65k%dv|HK^C$j#{nIn&Pq)ADdB&i^B^&M= z*>}^I&zxM>!7KgZk=R++?`=72%1pn_$3njzcF(-bmj}+S+V$ZdTeH$RwLWt(x$v(K z4^IAfscziM^@%UM*X-H{ZV$cHcU;zUUv}=@Y3J^D4}DuXclVHkK9j$Hwq*70mrB1G z-@|`yT?_9erI)SRE<>-jiu&pOLXxY2EH0w93|W{`hCk zjtR=YVtVP9(B|!S*8jstTbF#HRrZb^ql&NWS^Z3N%ZCfDdp;HvzCO#=C#dh@#J9BP zyHrS_^LPK0x~XQ?l1lAQEBBS}n!Hpy`O~QtN51$gX~3hK+&e@@yWhVjro%dESC=2k zq6=Ot+%xQ8&_B=CcguP3rKKScRdy)YpEvibd9QB%sUX&U#TmKlfV}SPk?GmF3(vg# zOVzL|ujQuZPaItQRn62sCzDPNKag~Nbhow(5=*Dre(4!MGrxV>#-D4GoBjP`*8Meu z?p@%E^`;XJ#$G-No<(T)G zRliP{+2YlkCzo12A7XiC``FsOFZ4-%P9NfX>EmZ~e)r8gcxu$aZ6~{pY@2xM z!zYt}`Xj&i>tEiOHmSe+hzY-pYKCwn?TL$%yd!6v9<||veuF=LBlnGcdei*C4VUw` zjjt#_G_$(C^n;nh_Pqb`l=AH6t5-iKZHt;V;>j<@T^v$1DW>MwoLyZzUGZKNIc`?v zyGIv9pY~ey=F2u|$?!A0;Sf|3Yn#w!wX( zo40g-ZP@T@fm^OF$xjM@Wz*U-BP`o5j9#!Z)%vt=*AB-%`1Fl_ANj?-yX?LD|21mx z^Dn$}>Xh5H!@nOY)D*0_523rgyejUu_mA)H`~HZK`(nPbRLsozSCi70C)QtZ-}(CK zNo$67*B*Vx@>s-{s@4bG{hu(uJQR0Xd%k~i!+_65g+*WaF59!Xe3P5!4-5YN!la#s9jWhD ze*eVDe+}AX$gErO`Ad7-%=&iQGT!ir0%%h+Wd2vL^lo>tnSSi=2i|%!|BdcntbKIa z>wEj>Y43d`V)YlB+H5b49T>gm{Xws6-EwW++<{%A`wiUJX5@xvu9=5aP5Hl#2^~_?)K0{Rr&ROM;tvpXJ!9i zzV%L>4&2qxaYh5$2Ip{J286yvVbT@cq$$d|33ix-EXESO|V>{#?TlTE&QR{<4UNY?(u(4#wu2F&I z%L_gTI7#7*lpeP2r6u3K*f+eY<1dA;_Zx9(_K1S9kA}bVOToCG|L*pZ`Mc`#2fp?^ zx^~&;*;4wCA0Ll>)8tk9+AEPyTgwxcUrqeVy7$PAO`}X{Rf&72eRA*FYi+U(6L)qz zw&l4`R>%JKTHG&Jj?Z}{t!bZE`=4ChjqyqN{=tL8r`(tSW5^rdjF|Asp+$XPxxCi8 z`^t_Ne(5wTi*ZBYgO$#IB9tM4J$51_4+)q=YoFU7u(j96b@_i>9L@ar~^3?nmBbK(8B(1?k z5TT=drNZY|V*LEWlb_*Z>T~4Pf#b}5I%fBIk`-%%>6rNm^>o=)8cftxgfLH*&8^D3 z-+)smz2+GxStjp`HtyY^1~p zmqVepz$_j1HmqT?Val+xT$swy!tQ@EUVBOzde>XT>?XOnu?qnK9n?_rQB419YB@OZ z$8bds8Mt^D=2+}ujD`idmjR@Htdt;4Rm`%$(*metr7(~LR|XL7@Ka1UwYMmyRMUS1 z=Kc&X9f*2fM)@Zn%<0QG{_%O2$aw$5;w zAeyxTaj8S1c~=n4dD@KnFhw7(=;MI&Gf6RzS1=LC@L8`IH~GwPazHwe8Po}s2Vzc_7K7*?bwOTfzM|+~12SD}L5$~Z5Z!GAF{ZugU9-u#% zLjJ7ns?nB$)`8Z8Hh?yPwt~t)<)8{sCFl&O4pa~F=!X3ekPk=;@&oyUI)QYc08k*P zFDMu^7&IId1&RaFeG(`IlnF9}7JwFmNp?p~Zb&bm3aBeV)u0+s9jG28 zbw`>&{-8im2q+qq0y2XZgPsDd0IdS81+4>Z0Br^B1nmVK0UZaO0bK-n1t7g3y4L~w zfIxkC#H&`Xur5n)U zz+OhUu}}ebCZUq(K2J78-r-Mn2fwyFStUv13?^x`F;$8&?_zmi?7_@azOicgu^!^2MIO%x$>SFNds3^cAMMVwG$r&noo_LH~ z>~CW*f)Nwskquve!$_7c>oZ{jKA`1f0psOykw|~((8NjyIBH8 zV;jP?o2c>F!x8b(mECNdGDeI>XsJk34D{qr7+N`zdZuceoJN7i09DlarUc_xfUKyjB z2F_G+6xpYmp4n7KmSs5_FuK<{{8Fuy5BDR ze({e>m+P2 z2lVLKE3o%HefkFV>mNK|;Gn_x4!JMn{-MLLU!)H;q++u^V`gU7tl8N)rd;!!Jd4#f zH-BEi{Ats120G>D_75#s`0ykDyYv6QJO2Ne|6!xTBO*tSi5fd@d~{6g1l$7?pD=k! zV$#$H1pjYt|NjgAOO1Lz_Ht_fhuy_H`27#`fNqN#e(GBDcBuAX#7EuiKaJa+{Z#*s zIlQy{I{Kj6%SHd+8VfY76x?9rC;34wOtny}D!RH|PMz%=yT7yC+FfZSyIQEQsK*l( zg!woqTA?@~DM6tmMW3$d&5C}3q8}SOTFNqINU>qDk_q!^U|>A{dLwM;11h@F!SO%alX7O9FF-N_kwd_K3-lo3iAZa zaV1IDPoqr%tprtrLi=OQ0^;6vInXN@=NdtUpbAjn0N8=bK-}9t0;~oF4#XM-Gcot(ilrCcX)uAB+I5$R2bP7RT~x|2K$xt@^WIr?R4ZkG<8 zE3kl9lv=Uo*v}S_OSktgN}h5{)h|;m_;9G01QnTu6fD*orWNZ*}=39~%KHS#A|(Hpk^=XQdV3go`4WOfcr&;f1*zG3+>)n1_lp%JYDfI~D>x zfJ=asJFWy$?zjd>*vc>g4Z{Q{%WqD;l%G}C7FY+Q z%&s2T8Ym6ZXqhiBpdU~RtO1S$)&cdvdSEC}8jcU-0la{eyJ~?fFMlA* zQwJOa3`rQ?vngg1H|s5ZD$t7}yRN3iJm?13LhdfE|J9z`KBEU?<=L zAn)c|0_+T20qg=?1JnW60lNY>0lNWr0=ok%fC0ecz#hOFU{ByhU@xFF67>P-1H1?5 z59|XB0QLn21A~B3!2ZAlAU}DM0vrs?2Hp#t2OI)i47?9m0t^AJ0^ScS1r7yn01g9| z0Y?J&0`XX?1{_ZNK#qm52PV-Tm`?M)NDs|{3uq2p zLi1p>PnrYQ&>Xmq=7Z5bX%5^;b6^F{L%=WUfi=_*2fwHXN)gb9f?vR9K!0Kc(n*X$ zI*HLpCovA~6zH)4?G)G%m<|+@Z3*)uO0Ff$x(qU^Ynv!VDw~E`7$2@L1PU26XDfUt z!xbppalXMP12Y*unQ$SH(thsO3U{3U@!`6P57$wAOiH*WMQ>L0X2m^c@q9Q>;=_47 zAB!?Sut;+hJtgmGJ8&UTxZ~WDPZn~-hv$R&SQ_#p(v>Ys?z!J5^chMz=PLQj#JrV{ z0jnN9MkPHtN;(Ykt~B!{^2gaepG?I+=dXNbEAD42;aM=7=3`UBwJPzOm2hS%@o+ZE zhcjnBvz2nlY2b&j&r!^0D)}RyXTv?`-Q)!4%H$O5LlDY53~JViAZe7uv6%HD2yY9< z%WBTZ!zGTbtPerh+2FX$xhvI?P>WgY7-H`L3IMutP??4ZPUF8siT_XKkG&iR^&8i{a~6IKI=#jRCL37LN&u@U13~w z!}`MfGX1PGLC877v49&iXI@x$f{+3S9}S&V zDQDJ0wY*ptgW#I!qo3#B^yJ8C zj*-*Mm7Q8HVr>>9=biPN@)6b<)^Vy?uUOBiX85e@$|NOM>q_n?&A`N1`2$l_o+h+Eut*|HNLINC#yY3^@sI zM7tU#^HH=J;a;>EVQ!N1qqZ6HG)^fqk>WU*IMyN221L7ctY_hJ9=06hs2Op8oNVzVEF(*o{ry|c`a{5G`!{oLr@+^2S@*FPLWsAaf=D{&O zkv@?JHSRc>OKRNFa!HA}fc)3Rr>L{gL=!WOtR5x*Q`t5T$jw<9+o{aqCT9|y~8on>EWV}YpnGN9a3%B{gVzEV0@njK0x9#+ z0L!6gxvzxY19${_jziVJCxDazc>>Qt&+(qJAj*d7p?@9d5u?#Q1JnY^=T5+nfq}sH zfRr(CY#j`JF_1E(roag3DZ|(Vy%#VJ`Zs{nf%|}&z;!??a1XE$_z&Pyz%9U)z+J$# zzzx9l!0&)tffc}VAjkJ3z%pPp@Gy`vD{tUA=nn$xfhT|-u^R1BpceQwuoLhTU?6ZC za4_%)Fci2J7!7krAeSq1}b3Q=XR)63; z=&isI=>36 zfJq2%1aKboj{~Ei9|l|u{bFDp`~?F`pkEDK1JZNmU>)=i1N~v|2fPaX6xu`I7U(rm$PE2}D`^gV3K$H06&MOE z1qQ->J7662NwkN)J#aeoobgc>IUJY?{bN8Y@NHlra4zsE;3{A!{B;1Xgnkffw`~f%*SOQ!Od<|Fv{0O)T_%5&%I2yPC zXaEC1*NLf<(0Y^%Z;)$MX0l6NrX_(l2`viJzHMk3w1VDvuZfv zM4vF{yh%0B#q35^&v7v4ibXZ&>O)nFm7oZxKuI&#S7Iig<}imBCgMLE_UamvGLayp zjA05{fRGZ2H5m6UY0f!EDtvJ-%X0y>FPFWTyRn4mM(~k!R;bgE3%lCU z-0q&cuBsbxwp94#ziK{H=BY5xl*>%0bFltb-I!#a3UeW0Qq|| z9Mn2>y)NPvasp9zh5SjJs}=GL)tqZP)@k$`2sudpr<&;(=aq#F#vs=RA^Q=MM9MXT zz)j{+tiwe;7Ha+rS!WPdiR@p5d}XH0A0Z=|E!Qg{s}Si?Weh?`Ka^|Dmh&uRDYNDN zM#zqYq*BO~g!EF#6-0d%vLGRy6fzJIs#rM*DIsNt%mdrESktq`tLhxNjD^gH|Eerb zNNR<=L6n(L^Pg_m_Eb0Ik!TM>h9lw_{(IgdguB){q!p8NTHRK2MEcD-5V zs44>#(qbV46cS@0=MnJ=8G)#OLM_rM)cog~iSjEUT^4dYp572^PWFAOoSJJ;>V+?1 zPkDf&J@th)ddehuih*`&y<~4nITKBt%^mw-;ogFJ=E}V=cXm%Xq{;`X z)x1+i$9Lu!mymCXk%sa_=2q}goXSzvl$i>tyO24G@)q(_{?jk}22mbDX37)rlmjxK zX2ht<1cfxoG0(z8{5$4B$Vumd-x%xBR@HhY*1&9wY?`#&#tckvXeX1r>achRdT zo0lCE;RCh!io^BmPmfX&_V`r&^+y_T-m7tKA`a`5K5pG*!1*pCoWm8N@?SL-S~${0Bt-)aaNe=7?BkM@r%wNfM)fr^RB-jYl43QYm1V;5%20v4E?+#T zLy}sLm}qluzGSduv1&?&v|PCYTju16iYt4m(}zQXMOocsNUyjqQ=UQ!Lqr)!h05_T z)lR`P%}U)D=X%M&Je7lOLE@=jp_eCVTW>gCG!*XUov>unS7xeNAbUtRw54ZZ%4MZcvE@q+#Nsh!)u ztv{AADH-{${_(>V<%x%ID_ZD|pg{xQ*LR7isy{X#{?~kGZWXv*KR52f zz~bMbPx%pXqnU&JB35W$zJE^;u2Ne{I!&^G&};dmpSFF0IMl zwqev4`m-_1e)s(ex2vVh`lsizFZFNdZ+!jD<+y!KH+pQ6@3%=Wakl5g|)4I(>DE)>(Bnyp}8CM6Fjd?+^#Rn4-EBQcWt;-eX&#Ue%tZE`1xr$e_E<>!B}c_xiH0^c62Yf3@$5e-FnB zDmzKHTfZ+MG__QRo9f~^U4L%FZvC!7&t&#>L;lw6&v;#5u79=r6OV8E3ZG=Do8r@Q zZ@K=|ngg$G_!0c7_~rbmseAN4RbQX4(FGtqhsJa{yGO4JxRBj)5b8&$d+}P+*ZN1M z_1V0xb`bne+O<*oMsImHJv!?m;xk`a@bi*y^m%LMb?bf|{0i;2<;Ny_^$R|qyszyy zH(qgJ+Hn?l?ly{{7z6SNG{H*+;i_e+cPI zd3e%@cKh{bp6z!bYxMx6Uvs?gOZ)Xt{I;m%k6PH5teBhB<$zu)Y5%X<51S(XV_gbg zJfP2f{=(&j7JRX$!aH(WvkLtqJ>t7hn+<-~{B>VpRE7Sl)|)p!I}7!(wCj`sPgLkX z`2EX;-y4v=O|$Nv^Hqg@;I`mp-27MA zeD1by59z=3^8MoEzlNedeD?OoK8N)KD?jb<;y=)yUG@JCZSOjO1b1?y_QmwP@H zs|$r*#j-9!ui~07s$};n7S0fQ73186UPawIm9l#kt0xP+ih0qDyF<8 z^eSe~7J3!y`wG2^^Zqz2`&Y4Wi_ohm6$!nH^|3;);=J~Xe+A7y9uoOcu=*pRSF!S8 zp;s{~R_ImSbhpr}xCZr;^+d%bJA__E^YcQlVp5jStGG!o^eV1tE%Yi%XDj6JRjk}D z^eTqDDD*07EkduNd92W@Sk_bMRa}XaTTH)-)f>p;uAr zFZ3!FUj9b*ucA~Y^bLp;ZGPYfzs7twkl!DsY>^*qrmT>kc3Ra*tL2Vo1TYS7PK8Os@EfDxFxjrR3v=!O_lE6I*u`MClD=ZGn;DB8#8{j#pne#3 zD96gT6^MA~=YMrPjyJW4d&ZuP|lqF%0V0KV|fWhqG24 zj~=+0@?SD1QC_F=-37U5rN-`9HygW=LhHhJeaRc;C5d&rv3+Cx1cYvtbXYHD;Gb{c zaP7#qx^>_z?^7{g{b&K7b(oIN!~MSVhvYgxOzTV~ToK=`%3UtTo7SIOhr{=Am=}AkQR{}Qyg0kOkw>%_ zwJQT07w@!*)Va>*?ZR_xskaJ;(rT8!^SgA8{fgQS)c2QMr$?>#j`6CYQhv*~&e#?? zxVqwxnvTY$WzVPR4{u%%MSm#LVMBgyG?K8*c0_op~Ih;gMczq(-*vPhgU z@!li5Uyd0672o#!^SdGma(J%Nmy7hUAHI>>&NacYU5XKuvjOf#-kNtka2M7+$kFsW z7$xuAEqfsg(oF7)-cN^l)nvJkV_hH*)p0pm?%&eok(qhoPMdgK@k6c0jnm>PlzR|n z26$T_^YBjj(AbxRbn_-OQLEK?0aGga6YdzXwlp5`_zvzYn2V8vI|b}($rCZwxlRvf zCM>zeq1oGM9QQyrcKId!?LAI=blxR zuRW(K&$z?EoY~)h=Kj4-ZmHb)=Zucwk+A<|&P z*c*D$v#IqH-+_?pZV#m6=JBfiv6%B2kIXq^)sz z-HhiqPp{yv=ouP^Ci2VM6Zy_~8uG&VndtxQ^-hcrY}M*qo3Vv~OZHjc82GY+$M%`= z1cz2I8X>a{#5S~mo2OT8=l@w+UAM8ub9(muVy1g1?pZtS^L{&DUHj$yjyu^pc}{lY zyx7ry0$g$yEXESCHd6hI_IM|uxK4-4Pv%tZ%NUPiZde-OXl;DQpR);9vrW2jT!Yx% z8ynRHwH@*HG45z{u6gUUapzf#7aTX3GJ7AwcyHcb#N3x$wzuM&xmWYY(8bz?WzY7> zx$jK;>^%-+bsY}5!*W&I3R?wN%c%&-k*7D#^jwFhmb1Nt?=-v{Q|_8ij`?;Sw#cpP z^l_(Nj7zTlI<7YEBz~^>xg)HuewaFSJ2Pm(1vY(b(t*ajRWHc5&M z#uYE^b;>cFjx|ZtNLPOC^I~VeEFZ_3A=WeMjPvH<@m66o(k^5Z>>C=7JRIj(<8H;r z8-3ZwfBVSq*ndt?=J(FysOvb?KAdYwBXTK5t;TVR^$&CEif^1z@vcb9+n67o>Y#}$ zxq_PZn}u~dp3z_VgdOw7X}=!#%GyaWlU_6>a)Uj#$Mm(GPW>HQsGE6kbZK0*2JRZ0 z@!Wc2Gs+ZA6HOYwP2$I4I z-YgEB83+GerSs1G>yRcfw?HuR{8X%1)U_MiL*uxGRGR7HJ)M-oahAY-n{(i~kVy9f z(T=lb#=yBRYYt}yqD5uEHRrrE<%ux$K2)Ch8zZ})1Ut@-gpUZgQzd!MoTOh@`DYp1 z2O@P+G7{BM8tmkb!(!`E&I$9AvBW8Z za@-Y>lkbv`&c1IQBg>pCX3G9Eq2`>OGZFII0AgA5%%zyi=EEKHF2*&cMyz^S2ZUR; zHJ+9 z{g{2;z*6KahxMc%%AKu=kFz;XdNPNN&FNR(?sRg`v#L3WiIRw$h3V|Zb@{bi!hbS%%t zby<`lYnk1jbALzPkCoGMt9n9LTf1GvH$9=vL`-UVq(LSdQxf=lu}g5MuA0d2@ne?PLnYJc~CQ*rmX( z_~x80XPL1p_v*aL?(+8Yh+F5wE;YN;e6US9$^^uWDPC^-Y^Sc})Al}s_%x1xB6!SELF}ryuJ4ZXX7&w^=@;>FHprH1-@&+9 ze7ADXnHA?7YQM#K2YV#uM)aeB(21Q7(Qi=JEuJaJuj^E}3N-@qz6sz(1Y&R<=B>h0 z*9MJ49*ofVwj9R^b&hI3!NE~nq}Em4w69^wVa~oezZT~(R9-TtuKYGGpBsHU%eP&H zVV`B*JpP;cOT&Cr%=&JO`DSTW_e;d={=XKdbGs+!#4eP|Cs*mrmur!T?M5GBeC0j` zXHE=FxODU-+92a#ol$M*&ZLZDHhJa5k(~RR9RE4{b*`zd(x&!HqOTD*sWKMFSsVG7 zfbf{UoAH$4P$q3(HBrUhLcBZL10&%;q>hhczsBB&eFnZ>0^i&*5k2tD;t_LIao?}t zlf8wqA96hL#F7>DlPy{71E}qXoDnl8`?)4p{BbUCAtT~=X_s`nZWqq}m=d-pu@-gq z?b^S6{Je2}Vek35M{eI0y&2CKyV`GxS-8qG(JzOgT*a<9z9)kaN1^>^;77N7rlD_5 zL!Rxsd^2R-G<*TRnKVOo6EDxyIks~Q<=THbe2cjWR~O>^9P6i;-_g_U`OQ+h)p|lg zeGyUuj!nGxHWTS*Y4=0TW4wHB=EixRe`oIPb9K9W*ZE)xa{c4Fe^F;zYQMp`JL`y; zbqFa&gq#C$%Wh*{3dxo@YtRjCU-S(kG|o}^l@oQe7Hd0>k?cprNhXfl9PeGHooQC* zYvd|xho~#A+zbBdz7O|gqq%6gVsvNkB;-&c$LcColo{vow>q!KG^=NZ#0e<&oFYWZzQu}`-@{?;WM0*| zgkw9oF$Blno<3Ng$yvt1ClY+(%!})3(V|4XQA5v{?S!1Bap;b`8Y_p{c-#?r=c;3QY)ww2R-4Ki8 zp6!jP5M$SUOzwYF4srna`@3|ufojU^c^60M(;6xCA?oaQl)w9dR>5vnBRigTkA~oV zJ?z#ib};&npS7ECR|(E}s{y$|5deK(#fdbqI@oHbGyen9-i@_&A0`Kh;I>zf_XWE|;XopT>Ium)xW`LEAy6 zKvzK>F1yL!1Jo313Oj+HJog_0G0jY2PNsQWJ#I7ziJk<(OeG)V#JRzEKFZtO`>Sx8-{eT?# za5?k#8SI4_=Uf_TFPPO2pL??!0`7$Y^csnKJ^g}%(=sp+RH*v!NeKZ-;bC-h#M$ft znpHZPjg{RUhZ*Z=KiTitaJs8;b|?Ei=VTTq`>lf+`78X!CDCuav%4fZyy|QwhbL*2 zJjn4T$l-Z7o5}9HV3y>DIA&&KrDK>#ao0#|aFI+ueDuz0K;X+Y(s6*$2gsA2<1h=f zn_1>5PCZZ`hTF~O=4M-SC}n25-HLXogfV~q{Pa|guUG$}q%qH$7BDtUPNVd!quB#; z8a*7%5`;T1n901M-vr@K3$wkRh(}IPfIOi-zEUHlFn{SR;DCXMRvP}2M$!Qlvq3bQ z4l^ISyTLTed|4xvFg)RR2+iu(Xe58TJGrydzOIqZ(99?paq$g>KcY^_aduj(k^3p7 zp2=|r!_1-X$#GtNQzK1B#uRsQoYH!Yw3ucJpV21>K2wh(LEDzwrfeUOClB_OFk6p} z&3>uMmqsg`@+a4Gsahk2qN4Z9v*e`(1P@^TLeFTVidmkjnVfc-H-4@u@3mAYsOsJ4 zv(-tGs&8z+N3o}ytx&7(jyQz#=0lQpC&)3JN(X!EBa-$B(9P^q9+$KxkfVL$bbP*6 z*js@QgVc1ab%=-l54&So!4+Lj=?QSBZ1un~5jDfYpy33uxdHsjg^Jfg(IF@#~LX=Hy? zwnn&L>%vY#SWGkh-0VTKki%R5gmB9|_&w*GFV!#Is_8FR_(lELVQ$)V5Z$Tvj`^ec z+@WqVj~Nfsug1qbF1Sg2OpBvi>et`GEvy^jcdUmre{{Skb7*fGD2?a$A1wr`xj zgBEA|#{PX5JKHz*uX)VbenTVvf6&OjariGi>FnRJ{V`vGrMI^Ce%IN)arjSu=4{_M z{Kt3R+WyJC&i0MNUw+Ej-jP44Ke%Zh2050Gqy1RCr#KzN_Q7$VV@4pxiNzQTInJpM z?ai=14*MwBuZ6w;YtHtCu&;)_1bdF90gdcSV1EYo39w%e`{9l3*TB98_Qzo#f^j#x zk$na1&%ypG?2}-h(8#_T_I0qYfqew*(;L}qJv7oq*zbjXAnbQGve&`B9`Y)u$ML?4%o-RzTUxJ<>Sq8C3yYOYVZ`4`6|ZWRT^mv=q->5 zlmz0l4&goz;=JS%=m*etP$7uVP`K-)xLu4e<3LxT-;Xdq0;MDT<-ob1>c zP?J|MCjt!xg}|Q)xEwSNZnpv}L4Se*q5mHCeAX!#0Msbz8rZjn+N9{Gz)xR<&u0N} z9rS#*EB2FMHVV`Z{`e$I;W5eh7EyA9ac*pGx-mK4iZ3u*r1xnaV=#y3CFCY%nFbAv z%*(?lN7D&rqp899cw>R|4o#!+!6U0tHBXcvC$NW{m1ayHYr@OC83w4Lvn9gxoQ-l)T-L*@WEL zM$=eRMs8k?{MjX}{1rdqOHT2|G+SPlwIIT1Nz2PJTXVsL8_kfCr`*U4^*c@s6)jf^ z&ohD*iN@^g@uu8-Q#|9XkRs>U4B4{!suXXu+RPDI2;Z8Qi)iM8K}-)hBT2uj_97s= zHDdJ8Oft@#Xfx&&Ofh8JjFIzT25E(HZg_UC#W*3A3pm+Y>V`at07TBGx{1t<&dbdi zGkI*pK&g+Lk`879K_FxnC+0`-`65!`A0H%9UBZK~fm@_CuK;;Bqa+FvB|oMN_V#rB zt>#0`XI>Uc&7RM;YN#j@5$afz#cIgTjx*#LaxBQ1w}XrDY=gxDXBIinZmLbB$(9q* z5HS2isy0YVJXWx2R%5zwzypd1KxE31RnmP9e&l!>CFBP+C!=gx*-B{kd_el6@{5Ha z{iwPQM_wF*!^`X!8tmjsqGnqZy_~boYFZk&V&}1!6doNvHiAVc3R4=Px^5_$MoE|2 zt1cSClULLq6Q!muQjv&tII?0hhv%9L)L^9fn)m_> zz7#*&W=i81+kyuTku+#&IXMOsZt^kVb8lgJGv^|8Zc=Po76L%eVnQW^Tc!}VIH)Os zT|@npSArC9-hW@q>&by4qT^raED(nSE zkcq{GLT-hXI4|NYBf)K54BSB2c>sQEK!1UJ-_S_iK!ZS0pk&ZO(9@tdKwp3kfKGu} z&Kk)b@+(i&<))C4cw--`ImW0Kl5+MkJv&>%5Q<}%VcFU8C>d`wrNwc z(hX+bcOl%0Udq`l0^OO_D5|gGEYg%7Z?v%GCwS?MyI4JLPA$oH)InvK00^i%!WK-u91f}Ad-*r zS}W!xV5-#aU8*rxf_z4OR={k7n_aJ(6Dh}O_LfHK3Zp@@n% zL``goCSo_n#GY89V#VH##*)~@Zj8O{|8wr%g+*iDd%yR6@B7~O?}gbr^~{+wXU>#6 zGY3ZejhJZ!V+Z35;|miDlMFK)W<1RIFdJcx!CZiO2qQ(gJ-!ECFcvT`zXLp&ui?KE zrV{2D%q5unFlOJ1nNBdiF#TaPFq2>w!fb>&1akrADU5VEV8YnJIKsHWgu?WN84NQD z<};Z2Fe_nxf;j?n0pIpAAiq{XzBgl8o}+z!e~%o!0};7@%4--bdV zh11o{_dEn)*ugXx0|F)WPfpGOw_%ov^ht^7AvtO_bBEwZWF_aNt97X%yYGtY-l@Vc zD|m=}0;au`7&$RlgE28(e6vDI4J#xoCmJm&T^NMn8=I~K)HlU3eI?YK^!U$`pd-su z(@K0?R?g5cbST(6qKnTrP~eaujZRmqvzea>KZD#XD0f{eX1?XI;>8Lx^KCkXB{LoM zWkaCPEv^;?j(iNi7hJTV7LkXK4#0E^O2>b-;5``Zcjx)yO#~sG!3-Em#bmt= zm0?y1^mr4Km4g4DrFp>w7i5F|f-{Gw=454NfL(QCGz?@{^Ys*^*45Nt0cXP+NSD&6 zCu?w&IyWmXCkcFiMH&Zk^HstqiC>4-hJC>Ef33Tth(VJ`Ee zv8g#4%tLra-a?~c8j59MjDm?sI`g$YT!$#L{oS?VkB%ohH8T)(qG6VR>WTa)3}2y$ zKXomM26Or;7BfN5E2-K1C5%*5QdQuAw~#^1pR3)8ssmqtQM?u^vrtb-GEV-6!N3WxE~P<3)hdR}e{4}tkrf|~1-iJ?<6Ge#Pd zo-1%-A=yDidLFh(~sPKQ}**S zup70$R94-@g7MLnAzA2Rn4J`hr^N^`bOXKx^0J0$%-cg!b8`7`9%gIaFA^U{DM=ht z4JrZq)MVm+6{IZYt$^nrsG>POY?yJ7J(_$2RYWuRb6l#o#$?rmGb zpX`nAXK&6D=o=h;zphEwzILC7A#6jt9yYyYtLlDh`)A#H*iV0`gz=bw8h&$JfCz9!7{bO%2(!HzPe+7xsy-DZ4ivO<(vv}vD%TM;s=U;=* zzZ+`eprmB=;2|lgX+zU9GPAOWq1(wDo?kFxqz)7RfWV;OknW*jJ$i;mMD|kkj*5=y z6C2mJpKjmEzZ>0uS$^+i0}EZ2?_>knzYL}S4Z#29>>v`8rg+?ai~UOp#UqN_2sB*$ zo6a(Ck?vhI_rgoE`Hd;$-%AmH+k!I-WV+zDev4k~y!?dsy1z5NzYzZi#6LX$%KR40 z)%n%)X?JAZrw0C*d3=iAMhw&Y%tvQrO#GR&Gm5&A;VL?Q`qvB37)Cxumr}nUBlJhy z{k!9Do3X+Zj`$Xc|1RTtH>3Jq&aUW(sj1Bx6!AN1Q!A;>lu(yRF`yjxKgjodleZag1 z^9=TLcprnYgP8=k0`IOcAuuYK{xCsEmx(vjkXUSi`2=PLOa;tZnB@qkJcbKzFTAY< z`*Vb|u=f?*$#8EFaFW1Y3`1$B3gPzOZ;7?)y|9&L+rh-*le99q0O~Km+@1=O- zRrAnY>DdyduoC`KDN}eFZ&SGM*%?J~$O4Pbdal$YRw<5;iW{gPm#H2wz2HafNmtusx(zt$SW06!;>~1QmC^nYDi($=-O$ARgkw|JW^4B;? z(wC7z)92gV-F!7TwOK=jT+YhfSr>(;t4DvfkwJ7ZL6!Kj#iE(gC^;&f{(^CdgFmhV z@yA~LhrIL`o6zwhx~PN=u1P5tNl^e-*22K2`FwF9!?t1T8yj_Q zBoVhTVx(5K2dWn?yLM)h@>tHuS5JnYF^!wDitSsI$P5VdU_g13`mi_{OWgi&W$yG*{kFAM8AJ~6$OH39-s-acmQm=Dsty-|8Nym6pKG* zi^chDj~Wty(WG5jFT3^(-Pb_puT#O&bXQaDeHk*yC*8Tqbb$p;sgmgrtYu#ZR#Vo> z&KNZt!vqR(rPRLIqG@%a6_v6@#da+q4(Sq5x>&6Ca5#Snp^*3^;u|^mfq2Iq8OFoq z0z-U7e#9eg;3@JY#-(OWafsuHmlV8>h06^ticP7BrgT_^)zraiFq>>=0W#|kHq!yY zAOVveYQR#1mlc3TzfORXEZAIls7m+^LJmoM?YP2^aH2FGplLD;osvhcZgp~XVB6a@ zK{r7MNcGpMp(6C8j=@P#x&s60yy~QLWINe41*z#^F;7eb<+B=zIIZQ zEkaDz9k=0lphS-N)!X8MjvKId1Vx8~YEToiTS*|Tl!OwqwnCl{1&peBYJDq@?B~%dlrsGXiNcvgte@Qv1sktX#p` zp`XHA?y7)MD*N-@mV#9XPPS<3zXV}#)>DVD;%$UO6LXUD6LZu&wkY4A)O2jNm8tRu zrKjemsB<8j+ZxgpeYB)lWKJ^MR9q|)F-v#O=|4)nlNaCn<8@V7e2=xg`pmgB{(kqhZ35?BxPE`~$x|&O@^6kg+BmcK z{%LcpE7~MmN-j?rARc@DPaMQD~jAm+Ei-A)C-4}_b=1FY}IhU*P^O# zV!N@qtcrpgOT*ai3xnA}c}KPb)nWrnS#2$1AB&DDb5Ud1`kN(-N=?d=LzGffBg=*g zrNTqu+S$cTDfg7KtqI}>QX95up;h9rB@J2WHENCf|;OP(y^)(hPPe zn_4lXLOnhyMWe~~>e3}YKi@e+-x{2gvNF144^7R=&h3(vlU~~l+9_C}A;88r1-$vj zfOk!EAiF<)s3|xjJzG?yCkn=Pc_{fXy_*$}SN}~6Q10A+5(fzmExOazniX%I9lnkI_<|@`@H5%JW{{?*l%Oo1-FYyGc}4| z^YNV1;yI&LHuYa}jYAzr26kWbUB^FSI_!;WG@LH4-)A-pRU1;)R=0dl2;{)a|Z0oZ-wc+|A$>OVRjYBq1dg0ULR#}hjTvYCic{5^6 zLdT}9FIclvvwm;FIETdVKOOOQ=^zE3el^=BA z=8P>LlNR#xj$gVAuxNcTs#A^`#P3Cnqxb5>uW4d~W!reETmKDWu|*UAB|=QWvc%I8 z%Z|0|AC;8_;fnfC9gM9|8nw(nPm_{Grv*sfvLtUIbzS5h3Z;vO(u3qJrU>M8CFg%$ z2l2Or?SeV%xcWF6nc7 zewp1#WpMQVXJ6-UKIr(&X#H4?s8#g~n~Q-*8{SxU&OK}M>-FY$T1ALLB2AaSEUvyC zTVnaCm*|dFvgT&;>cJL2q-0<*akMJb*(dPs1^el{4}MzpYWYZB1-km+ZMW@Vd4f@U3WN~_78wg?B4KpSEOwn2-#i)QWkf?tu-kR4Wuo6qnFznC` zA~8#58&LZ>TWK#0h^AU0`IeF}OFL_|}Pab5baI(b!)v2_YFH z*>DWMh3*5A(f6+;W0xeoSbPU;%^U4jQVat2pETsmfs?Nqw)fB9Y8q0ocFnI%Qak5= zuwC6n6n{+eFma>mE9XxcF-AT6_`x`HVL47k>V+y(nP1+wu;twmdA&Pw}zY{BX;xiESH?k81iy z@NLOVi|h#tvIB>8J-c_jq?y^H+z)q;zBnNH&Z%9Yr|u{&9bI<(@RG-Ik%`apZw+}c zam}yH8Oh`$lU&?=Tb&-X{@AKNE_UH^yo+Xx%6fTw@bH|3mi7-S$}&F8V4UwcoF6~I zX#3g?SGOc@@AROD^?+HIc6KP$MqM5>#(cH7!KuMjrmK(p$fQ;(MZXf$(X;RL_h=%D z+SuhISew%bH>B&Uwyt@7xT=D%>`O~}?{2Jh9P*f5^=))zC#P6H#rFlaUw`sxy~WWR zsuepU^#I~^YEt)T5r7fWpAHvMt1CAv!lm|%d_X)Tg|xU zkGp1iw|@JoM5C$*&w)#V208w5{MS3fzB%jmNo?Tj`o2@zHE1=mplZ*@OBN1n_3Uc+ z{hu#hn{dlhwK3_$_~?xvUz=ClHxMR=^({owieix;XxkMXi3hk{06ngssYC<`Tz64YDx0m8?A#oAYoxd`d}~v& z<4U{MujJKDvi`_Z+&ba7K|EY+oESJ~?C@bWmKfFwE0Ib$>*1_$ck9ne3&o;`6-7rD z79C`Z_WesnNA1`TXmr%T(zbSVXiI0Y+RLYzE$JaFx+{bviGy!!RW z`KgP`XKmlNw*B*NkJe^aZg(+Wf7@oum{%iqZ1=w}dJ;9H$=v(h3d5Tv&)ie>^79j; zF1Ac;b85HkqvQRT-!SQLuzy*f#=ER%SGIb+x##yC8++e1?eyvLX)g6U?J*V4+!n13 zyVLTkEw--9gV-a#xEeK{@#*T0_g>7H7tp%WJ^%0nZe*OYFXNtBTZ&p-`F|lSKpZ9 zb0d1(+Ff@HnB+BJl6=Y^fgg_wX}L?8(!VsL&p$f`1h=Affl-Aqpt}<*|DPHIz617~ z(_Tv&#kP~0v8MWAro`lp=F888o1WQla%MMgRcrh2oh(|Fuh_l2vFh13=ItIB?eW`p zK+O%*411lOvaB%vX^WxPe4dT4GH&y{p7!?%`QJ{yWS%wY!ltmy!%wz;^Ua>-d0#Z@ zvT^&yt49=zQs?*T^JCL?msclD{LH1K(RR+NX}#-hO6MGnwAj<&>D{}RxO$g|ygZig z*6){J;#PdRIm7*G?#UaoGgQrX_012>2y5k$5qV~eqJ4(O^YNUvKWlz}d4b(G?wR&j zq*1I(o3q^$ZN72+a^I@Sw@x3}elqZ3ms${mb1Dr~OZz7YzD1 zJ|(OuV#9H(;4xEe^Gr^44BMKr<`BLl%u`47@9yGN-~7g8e3BXl!Piee7p`B_iDVrk zeuG&5X>(zgb*JS=rJJiO2|iby(}kS>%VPO&NvdyJHT)^R@gy(V?U$A_A3YzNI&G*X z#^U56Q>M7|#hpQgFAnC-#WU3RNmUZWsB*p4%N5(bW|eSZxtJ4 z{(Hj2aWR2rry|ySn5+pn|M~KtbJ@7|#S4NP9=Z9bd(pwI_6dr<2_X;qb|0@8XQxRV zGBv&^X4T=(!#~@$bMe5ViI$lTyIVXPU%2VF#Fru4I<&GM(j=!vn@KBrCYPxOts3|$ zzqQZ(xV+_?Cj~_K^*(Ao$Jlqs*hhmaB{PD?+&j|qRpwRj-_i#oJDJMwpNTY{`olyM z*Xjb>l{daP*!6mspp8o&o=e`mP_#`}|MP-_FMq1s8#M5vrgKC_z1l|TCDk=Zs!M-4 z@jdxxqUrs_w{}vkVT&qA&dF%8uV^}3^ch<;xn_>wEWvUl>!X){Vo_81JEz&Io?%^* zvyyVV1gfIDVBc>dPJ1|~Xfjy8n&e{E#ZDn>#eW7H!ha--#|M2+jkA#oA%qI>-P&bS1tXz-Q{OL-5DAaXVSTMaZr;N8=^ls@Gzmtq~Qnr{QPI1 z8_-NU;MqsbTxZ$G_SD=9u-LO=>)736AGnY1-_7>H7bAL1jbc`Km^XXWWkiwsmv7b$ zy;61P@xGBvR!=s_zHQgTcO;8uOzzk5rypKPFJ{kG^yu)a&x$Uuf4KUoa?_yrFVDvL z-T3{`7lWsKqD>n#_WZ6M4W_ogZQl&X%BZVGMzDg;pVSfd@l_sS--Auc3ThGx^`ESZ$EjsbxcyfcIG3UJ>!PYAGXYR zl6hIvjfqPd@Bi|2qwZf8?mBXC$&q`bOYUFv9#;Riftp!uO%l(nRM};1{{15J+jV)p zg`aypQFkw>e!4XD+b6%~j7c1tmH$oA{w4L7dv3bDd;jU;wK)e{JzQTZvMYSivaC~# z=jsp7>}lBNi;)v@y-lvoa=KZ5LFwG2-<+MXdP%_iy@#9o zM_d^HtkahZ8V_cVZ_+4w{+9Idqc0|PX<5}~MZb05p9|_bQ#I_=ykooiTu%KpZrjmc z*~(W3u0_0PxOHH!8(-Nq_vrG|;l4)B_Zn8HT+iIA-@*3enlY|+CL7PVojo2u`B%-v zuqN}0#fR_-)}5c5Kb`k~Z#psm15+$J)DyNkNUWKuUdksw-Yysw;TbgL%DuBjF{RJ$ zRuq-6MTHB;{4e)c1|JBFj2N*IbJco@72DRvyk`EVRLYf#{xu#Rtj>eYhj7uYL13~e z70J6mGMNehXbIC4KVosqtzs-Y*5mCKr*sjTKAlDp^q+t!xmb=*z%`2)G6gsECP6$^B6}AW-S*@0i=B+GON{qoU`k*=yFW>vQzSi4Dx+ zBUTr?~SfBB~b$O}3 z-^(ALe*5X@vs*gnz3%qi_e^Q_g3p@m+!{1)RQ|%$z1wC6b{n&NL)JHW*ZSPpGyC-} zd9kD+=8pAwj{f|Aw?_G;r9Ydx8ZIpri+ZpvYE~Suw$!lBn!;P_M^lFymn(~`zFynt z?9kutUirdzZuE<|BePze5g%UG+IK0|*q$2z5O%R`$f8a&%b35ODdww31vUzZF z!l|PdPhK%D*mHZxPd_H--dtNUV9eAL(&+nE%FfZ}J*vkdof4-*U8*gkJ*$2V?l^t&$!1q>o|@Az zwBMI$9U85?-s-pIZaL@HOtD_Eq(l7;+QN>WOZ$zO`l`>#Q8_Cw99o?-;|G2c!7$#k zXjM>Or*JH>Wag@!!jiFv0AIM&IbO2vs&FTTGuzPA8~^xbpFJWcbx4Xv<`CGCK6!aK zk^GUx;W~ltdHAv~3l9%;mf_egz6&E-t_;W0v38?Qc5Z0e(DXy$sP@2yrqQWGGWnCq zG9Ou6d;M;P$Qb*!4Ndu5li(kz)(8%|vI-s_y96oSf<4`7Ej>X&=hw-;3BJ9 z$dSpZyeGJaztyZZMDR&f=K?gE08gRX;2JL?T0_%LVT0-HGs0b!$TKV?KBk{4I9?Ui zOI4eBrwDa&YN8NN?n$^Mr{^aA8HVL$^vTT4Rwq$=O>Sr! zoS8T%T^*E?l#To;K3Io*WI>^Ub%Ip|YZ4JL6;-j*1Nn3^vRVghC#nJ)_Wol2aY^i^E_)M82_jk@BrOR zBi!nNEQ#^;5rdFd+UYNQhYT?!y1tI|ly{WzKEf?A z)F6cBk8KkyDxz&WxxC$guq;$-MD7r}H^ci4=z7XK%BVoO(Rta}@P_O91UT4D!O9(F z?mQC0T`I_2FDf9%fpA-$6QBft)C4amYr|wZ=`9o3l-k2kgZTU%C{#rTDd=V%{w5n) z3@*r0r`JX|=tXq#4Z1zVis(Ouq2mBJ)`7F;bYrIO@e|&E^}2w-My)qCV%F9_*k-*b z{9qgGXUdgj=VT4ReTcH8#LP_WJsG5y<>Y0`QZr>iy`p6qxb?ufVM9}U0dUpbeY;C0 zFPyG7s#i;<6~V1~RWWTG|6atuOC~Rxz6ZtUqwAsBDss|seA zaFH=%a%6C#KI{U651XP!w%BmV>mMNh3$*Rh0>%%9e#kCnaK=GrjQJBZa7=*#vF6C= zOpTvEACclx0{&g|B2UI43HP{>B^!548=SkqK2By<8~7J8%rxXP3y?f0-aDQn2=_A# zgTr2A=uy}1OgatVwX-dL2rnWy@HTv8xWZn4kAF6W`N0^&;NX(scu5PzpU{CJvxHau>m9Khi5!{r>#IcnUw zqN5SfijRi?!w1d)L-%|8-%o))-0hMpPfKnW%(K9CgUX#{LH3&RAp6iNm_m^Q1GlZR zaxh*FFr8qWVH7ZKFkUde6c|u0wO92ni?NSELjIf<-U&?gZ~~Ov4EW%kBDm89H&r|N z6Q9!CWR@t1DGRqZgWW>#vuehO{NSeZa}>u`c*}&h1Mw>S9m%b=w(;|0xNv)o@<6q! z4a0C(2ktmf=h2rpJenJ5#vPB1Q$9v&Y8;GG3meb+c=P9L@z&$P!-t!tn0Q+WZyvAG zvl{pEg**^0Kw4U<`QTPPy*tz_r05w*)_jwaZS_hnzLY%Un9|C;Yh07kV|hooV3U$c zsR|_Y(^^jJPP-Pz=*t;s%vOL^A94Ju2aQmPe;}NG^>PeyHJ=wBiN>e5( zFDCsLB_4!N5Fw$d&lTH}SEWmt4S^{8lAHpdkENwr^Kna1x?7U+Fow%FE5g}l989$= zL9L3a0>iCPn35P!{?jFGwX-t}^yq|^SbeYD)EIKw$Yi$ptK-D&}s_5!Dtx?K> zW+gk#0K!Uyv^EUjAkWHM?Ywtr8c?KRh@^{QFQUjXsCtpco-7tjc}TsI3oj+ei3*28 z$hdAmv-BgJ%H~>@4Kyn~YhLP44iJne3&cW8Ohf`z!|nYzdFf+K7&p*_8)fhEID{2x zx^f{dWg;$w1>`iQthW=LbAIX@G9Uc`dL z0y3gM%l*m~NV&t30rqCxsj^Y_q1 zPoyuYB>tD_>fa!LdxQKP{v!X9$`qt`cq_f5L3$SH9qQokP=kL-Wj4~YZ>6^~NN(dHhO7gfpB7=z5H5#dA03FXb7W^D~ni0T3Mn-!es@ zEwR3gDG6gm;L)z!et|vbdJtEgH6OPds5}m2Mdssvf{hDwDVx9tRpyziROJ)=5KUlC z@LyM;B3 z%;iYEUz?Zyh*W8)Fv<*RY$*s(>?!z(dFdp<6l$YWup=$#R*C6b^%9P(S{7<&Va zTuZI>N`&cic%s>ME6)3MUN49viz+|r1Q{+HIp=jmG4y{TlzM+1gI6GtZXDtftyw62TvhMT}qx=<+Pj>BDKHY)i19Jgn$hVqw%&S z9B#EQdFEhVd>U3=oCNdI{c!N{Fg&Mt*~q%Gy&!| z^+WvdvMmcqFAu~pC9-Q(NI1YTWn);~cm?AW87HAuRs?T=l2JrmPZO(ay708uHRhYspPT~$|tO(px^_1ooRsEI8^fiG{usw z)s)J-ll>2K-x0#oQ!CHFEe4IbMADfT!#5;{C zpFpD;RhnqtX{s{V#8F!73bLO@pEJ>tk3|I2=A8zE@QgZvq0GKSW@Q7f5X+tsgWrSs zo06)6ub>Q-s0k4&LIpqwpzCqC$&~7`zp;_Vi1RnQ)neWWOko;YU>>XDD+F~1RzHFi zWfN$a%x6H*djLA;CoP*`MZQ!2rooNk;uo)X(t%R_ophj{ zpoR{G09*beiFP1BjiD-dElp^ug4g30uS#A%1=HvNuAREq9(rv8oXWVA%4U8jE(#pH zR+XaFwnoWWgrK5E0bFV22nB@#I*oA4^QExOfzAAfN)@c(uyPX!dnEV-jvF%pT5CNG z*0Kq=VK4u2DSpd_OUvBSL@5_gzglbJ_j$Ik7J1mS{fsjXuW> zyi$YcTEO5YQw3|S)5*?ruTX+pP2ZK8m=%$9si}?VQr|M-8L*0PsDuR#RE(|2|A?G&Tt4Mtj$YEF_VZ~ zK7oEc1El7qjS!a$m3oTIOL20ODQjHTLux=w&~Bp`ajUo9(`xzJpFli>!o9PWi9=qh zIG#BuMG7rSApy0A(#nDskjV5&n3k?2{JwFS*xe0?Yr49J^DXJY-3l;A+{~<&M&t!P& zdLS|Qo`&2hGH{00ItBrgP76tYf@9oSO@xqN9tLd*@IuYnB1^4xAsm>7VAN+p%Q@V{ zwP<6ddohyvx%!7ES}jI*78`U?iBYxI4=DjW700+kWg#|IA^cjI*7}6tUxT~hf*8tA z%O`K=jIxh5<36p)G%kUQvZ)^ZLaTi%)3{j9)#eQ{4HPEAdisXft}lji!C12a`i>aN z(1aI5WBW-VqOKy1N!MoPr3=8Q++51IY=FHj%~Pksr@Vw@9L4}h=px9il4>FeB;8;{ z@D8ytA2*O?9Dv=~cQ=0%jxMmPp88ncvqd`L%k zvSQ4XVFC>a#sswfBm>jKkkpg0(sGibdN_u>s4EWiot0YG`y)6RT6 z6$eva+ww6MT=%CagM)VwVQ*ggsV=%q#dUv)BvwEY%_tubx>9Z!IZqTS5VPYh@OB|) zvsH2RXmHAp00k-POtp_lrpU;~8}sbErW6T43jm>H6c)C0VLHjG%fb3KLB}GWAZ~$U zU`ds&x1D)$G{lx6Z7}w>M@&bZ>;P^o=Q@C|cUpks+XsSUz-L%~J&F$i)ZX4+4-(su=bq8cysIgK!IN?r>R##0cRkCVp4Rr{kfX&1XC&W!Ci@-#{4i7;brTo=F9mGlIlxU)!m{{G>fU~Zl#^j4XyS7 zqEmpbG*uHwV$-9lnm}csM;A#q=m(DXk)nK@J$1QNx>unHooO^2{D2v$U?mC@U&Bg$ zCOukf8IqPwP{B?3EfeEjPoEgnpdDk(?8chZ*DWg17@-Rh?Z?UxZVmDTI6p8 zjxj?%nDN1f=A#q^VEq6V04Ijm0hkuhqH?z`_d-A%XV1^ik)SruiR0|4TWVQ}P#O}^ zQ*z-cTzWE;RCa}n#_9A7E*ocW47;*YMF@=oLSs}2mkHsp(g2qpSZVYwSZTB-Scw+| ztDeA!OJgO$mu7*2l^9I02I;MkTXV$+sPrr!ga&B|3)~)pyDP0+yoa%EYMuWGaD^b(1Wz+ zbT+bHsI^{!bqbmid?{fN46HP)H;2))O+j;IC3Q`o@U0Y*VIZ9DXx}%`jps!Zk64wzgB0LOM#!YO4Xga^3HJJ?=-+p6{&TLPAv+o@=fLBi0F6I4K>*qh;{HP~K4L zlDy^eOv;8icn*{0HWuLWc>rlC1FT}BRAs~LtNrk082rP_S^a7TPhbA~p;(Q6`S}tb* z7nQ*ICEptAgA11!O{9-8z2tC-UTJFZ+N5JsF!c@rRb?d#0N{*(Zptj!OSW4evoyoZ zSgyYd*TX}XSywK~kMj$X97Qi{Ww~2&%5sM)s`?C;z62ISG0iio0irM6fdd#mvu*s)TrZ#vYF+!HbHiAT+M{l$%r{Nl zx&T?h;k9YW#%YBXfq|(RXI%31wWc8#DU}>nnt4>e5`!H=Ot?rh-i>B`<0iZeA@J!- zZd;+X(bt?2CgsB{ls@!ry(*X_A70dPGrNPq=Jl$ApF-@QT~IE)q>@8QGvn%+luLIL zu9un4J%QZ#)Ei;;*iSl`>t~jiR2F=j3$`c=zF!`EyUhP($&1(K4dW2ha8p(O)zU{hFQZx;5aO{L!I9ePACQ`>R~OaWSW%>%(JhV##{%D zJI?K?P9^UMGoS#G;cr%5iY&N@hb22rxSzQ&lj_C@=gCS$ge*Y=pf5)@eWFyod#A8+ zKW0d>)~$Akhwo}blFQlyD7s~?uL%uPmCdWQu6$@M(X`wxO2J7@bUp` zEVu?`p=Ko?OPRa_{7NrAzg9m)$u}Tp`W9PF`7w1d9ygxgD>_)Fo( z_dbk9i5#(tYNFgz{VP2Ca49z0Y%^`Pg*Mx&+82a_%pAzaK{3k2>oF$8+7h@d9LYLr zhuLa}*=UD3R9gyB!I98*5Y63GRw~abk0~E3wc4Xtg&4SF3U`zHo!gcsstyMXK6o!8 zXtmdX7r2(b0AW0uavs#_UEKq5fb&2;uXKBpsNNW`TU9Ryp$Q|jyaUm_H!-N+TMYyY zP%_Mv>q?QZl9TcTQLqdTxq3j`&IT`SJBI(YB`^K~qyv#S&_(wns&UEQ2pskr zf$Iu&hrPo1D?qd0pMis68XO4Af)CbR_>G;yUN=JI{qz4wf%|FLqBWFZ4&hCYg&Nzn zV4`5sVD2YFk2l_xFvnn?!kmWRMwriFpMy6&IfJoB3uZOU9+=ZG_hF2OFpM3HFHC=! z9GGI5g)nPj4#3=mk)|+A2;zI;9RrgNGZJPN%vzY8Fy~-y!+;GKGnlq8U19pdWW$Vu znFX^R<`B#!nCCFIC>z$HnXWLHqcJ%!lVDcE_@V4u@KzviJD6A)Tjb#e{~j>(6k_y2 z&wUI0sI2jmKtGi|qk=D+w+bBi;@*0q-aZ2X3 zAv+Bs&DQE1VehXKoopB&s8Sq&BMJj_(ezt`vy%rgO!vSTx(b>Z*A`nBX@8`B1_={p z9O}UjZ=GK*@32RP&Z)^bbYV|(DaPI(G^aQ$n=}~w>InA$at|4tnv5h9;g(XG97+RC z>!=StKJT{AEzo)Mb-1=Bp8d#?Ba;UK91|L(+ji;9N3<`17kG_fXx66RdwBxxDdZkR zIzjOyAdQ4!^m{I8uW46!3wWbc43i8$t6D!L`4zy=pnNXmN9}mMaZOrut$4EeEKsK0 zoFsdmWtjx{sgS4O$LB#&X^24Y3gJ!esMdZ=Ul_7m;oSn}{qvti0ofaR$Vi77=`y2l z7-U3@)fBJ>{}3OsFk;4*)&rQv;H-LhV@g1e1qF(6E3cS$S}|HtYkine6W-58=ckKc zB!iy}b|F0?96DU20mRnw0jo&y`RVhu$>5Ap5($lbwQ(#KN zhtcAk)_S2ItpSdxztsr&D$m|B<2K($H14bW_$}uXV|k?#vC2v>zzh4u&?n>DWjW3M zd#7ARGH#Esjyzo#qrjqYGlk+C?`|Vnlogd|jq~f}H7|J~&atX8qBUINwkjj~@{h+) zYwds>7SNiej}>Fgh>^x%L9?_F2u+eoCYIBjWnwAqR@DXb6VH~FgR4r%6V8pW6R4_mB3bd2 zRK`@5ej+#qmz5U7S`iD-6HCXzT2(p)zem~=O{c+jp%d9=k)Jc!=D;?E53PVLo3|~5 z?LJmfs!ErV&4O$+yic$u+e+9}ylpLPghEy6`Wo9$HKCO?ww*P$J+M{q`5vh8J0ke$ zX4c%(Qf+~A3Qe!ed5YkBmv$ZT3)bsBh;E25!lTY?{Lg3LTouh0`O5kFu)cD*kZgD@ zpl@%=gItK;0{-*T_=PA%d{sy`^8rd2;>XzI#d{{e_R#|R7NM;CusXH|jxe9EM#shs zykUXW1>KCLH_z=GNZ5f7rQX}}tj*>8v}FN5!Kue><1J5)ET9?9-W>-fqR-SnR)Znz z{1~Rvz;7YOkfHFSzE%G?)A-R(@2dNLKkok9e~l(5br4i% z=i=jrE)?e>abi-PqmQCHJH>p`NJr;8DWvwAq2tJdjsh6>r1f1b1d8-Q?Hi7z#j(&`S} z>Cbk8UOWT}zz|bcCgvBtpKwkYGnl&hN2!z4sl)4*6x92UPxlsmcp5|og+)6@_lri3 z;;^g$8feo6)lhaGk(|!#mG)GV_G~JcTF~o$)F=XHI&n~ED5(a{WxN{n&*!x^<8&!> z#n;hW{S7o4`zH;{!};rIoOx9fARM=b7HMcz2BtLC2rdwY@}#nxPBt5aZbV+7$sjc- zM?tzA=hu%``zMjw?Z6au(op6wa?$CC*J~LM#|cq}NdoquU|?QO4o-~gHFt-@H<*rH z>J_LHS3zNXCrMZqV$)f{UW3x07Q4GTlP(O5NX!OeLl_66p6a;7R1Ngc^Jnbbp(wx=_X%r9dSl4Mp=xOiBT9iH;Dp^;EgxSy@B#vIVbz5rT#E zY=2jqo4%F=c&~t8EsYJ5P(IM$JRFo~&_ZkZ;O$z@LYbrLk}hC`Q2y~IIz8%omiMbI znVy1tiQQ4Ff$8eR93e*zT-h&GoequV>ZClK9&|I*Ftq}ziLby}&~l9mri%o0$C8to zr{F9-H|Ft7K<8BHblnuhM>%zs5S0apnQELP&LQG$pwgwIMWB|bQN^WdQeqN^FxTKu z`5F2R2cBV>sno!rPg;lUV!#HM5reDI`p~GN`4iJ40#QTY<5$3YuwLdwsfXp^XgtH5 z2XsEYj@jsxD$}W!N29^BdJbiZP`juN9U}>q6q$uf6h;U|Ve0AXOO=z#-xn^h*>|S> z^L2W|+aVk$xruXv)O9$Aw3gSFs7|FYGf~=bb>eXK8*Ob9=p2}sP1g}b*QIl&CM$Pn zPG-8Zx&S3FtzWBgT@F|Qc|-UnPg=SY8Inz?@>~#IK~n{=B5Gf}XGK{ZMw+&aBlz4%0%rT3tRRKzsm zT{u@Py!9_BBV`CYjpX#taxtZ#%fWK6B$&wzfrcYqpA;aI>ZmM4BwcT~5D2qU78+{k zO*&*~oum=`0}&?!%N&V_KLR)ujEo^oVu1(?;?wH$A{8;YxTuV9$TnD` z*1@35%1Ggj45=uFP6Z9abYhH`0+(vfx{-L+~|+H8ap9z(DNlUVg_KN<54uvGv9(gWng+epuk z>@R5Ij8K2zl#X{)4fI^zMbsgzspQH0kzrrHJiP=wsOG3VdRmZ6R}1edm#*$qNS_6W zd7!<@;l`wD|HG)jQ66xcA52<=#8IKkPcB zYf;ygT@QA>)HT5;%csm|z0XOXn?9z#-F*A`rugRgj`p48JKgs?-!;DLeYf}?^gZo+ z-uJTaJzr(FZr#GW4eK_#+stl|OQ+Ya+Lj+sXsvq4EfMj674GC;wPJL0&FjB;P3CC;uI#wNbQHI4Qyu@ro2hp5k*w zh2lHK2E}g0am8xwqYj>>Mz808RUhH|8`TsckorE;rsmvX=Iva+5_BNr z=4S40?H=V`*B5re2(~>@Hy>s&gX*96`$k2zxe*uhEN==5x0Q#>M}w|2<@4m<%Gb%a z%J<5T%g@VS$c+@{isp(BKx3uCR}rH~R18t1D{>V1iqVR%6iXEA6hDF52NX{gX3ECO z7RokCWcL2~w^4w20TxwVb%iEon+uUssrzI{*pfSOC@q28{7RBkjg@G)oW zOGJ#I^Y86z;xiX;KbC&-mBXGzg&v>89PPBZ)5b^P1(pwdn_pPg7Frwh8mlC7GlOY}^dHn?cBHUR#|Fcu4(#G(efD;* z4xgv{eDL&a^-P&tv+C0&XP#OAoAj7bU-Zp*hZ()gN4`=;OP6jK@BL`#=?$y4_pL6d z+?&&WO?1|@iI0-|O&qdkkj1j!z8ae9zhUi@4(8lc>&wl|GTj<)2sK{yg;zpeESuPA zUvd2#d*mJ+AD#ZTiNwOf`eyjX8^_%ev@xE=J3Cx88edxInwb1N_wf&zvUV93O|4h^ zeAaT%q`ODEt=ZU0c7h`1(ZwduCq#gB52qz0Z!$TP2sDeEgA#R?Wrs4asTS z_1m~lyvjo((-_uc4sqc}-2~^qAd* z-?f^m=JlAJg4gkP=rQYOaAi2AsKugCT%8>Ry&!&IcLDUf^qAeB{N;=38FO1r{^UlA zTgOIUExYHpK9D{Ct`ovPuE|@%yGONDGPP7|FJY`@7Q}oZ5xeQ|I=e>tKCnIxV<+y;Q^_$PQd)|0z(z03dIm12+_eh#OTVj-N*?&y*pKIcKemHOPz!SIUY1&glEIfm*{H?w{W`Y)ZvXPjCqxhZC+8l`Zd-4g-*<)WPxns< zUmmf%$>r|P_a0iQZC|~%Uc05L&QC3O{20D_aFP6{AzcSg_ZvL>*xuoP*le2gLzd|? zyC*TulBlm8gW~K@IF6ESI-NJJe9(fa`&t=qce-~WZ}E zanOv`kqt{f)5w5TtXOTE`?mYk85nJl-%2uspOYHDd++Ybe1;(fmW?s?%+4;p= z3eGCRt*amIW2IR9`9Cg1_0t3 z1!rRDGSe$KYurz#;H+6jUyATQr{HW+?>-KS&9@tx z6Y*v?Jx?BK`Q0C<9ymIc?2mKUAS)1;Ex-P4&Nr>xn#aYO%!<7I+SStO$`379cW79Y z5NCF+@IOz%*`Lg1{}}~m9$794P1T3%H~Z*DY=Gp_gbAMhC!V})bgXf&WdRHCckK1o z3eLD&hsu|gM?Uy8dFXeSkDmJe+w3TxFHG|yH?GVcBbCp#{7+JF_9o$=;OuPJE~_a` z=GorT3|w6Fc|!|}Hn9l}#&!B3a_INB$LzmX^u|H~R%w`eaf3YPb!cWk*)`w~)#H_y zKARA(JP|3{)_GyCPWw$7J+U*|bMVJ-Zamk&{FQU*%BDv`%ok+1i+e6+_s<=BFu3iU zC2cFt-JFs!Ie$v!&B5)?H!Fzm;8yTx%Zk=BtF~Ke{W7m*$wwjQINE zqd_-{Cv_{BZR;_nWKp@_=+GZ3RF9vVZJlfON!+|&t0(#2I2ULC%9(jqW>VjA{OG9Z z`&}mN@s9WYEatbw&t&dni^c|x{jApy&s~@1T1+~BXhLj{Pey#>)^w;mEnRzj|Fm^#|ca6FV7%jK-xopn)ox+?p#jcS(M%tX+&hy@;B^fhy(Q32TeCujT_B{onh zVEmZ?Y-aSt)!t+pB$>9S&!QM z#3Feo*1^t1zcWrO#r01jY16u!>GT^L*@&8y*ncAL#`>@}20rF2G&~wr2pdq^u=NFH znMRD%YE=u*j^F;WW96t9#d}4@L+5qwe!dagwX4YBX@CEq*xyG71HD z`nmDDG96JQon67y+LeNx3hxYqt!aY{AdBTOY?NI%^D$`T23ipviITL%(gRd+=WRr3 zq7wN9@>Fwzi-n!Gu7==EHQLK%4A*B$;ud=^vR75bQU8a<1l zoVEFUaUo95*QuIY*Iyq`8UOKG)2{nITdIjWrM_c*^WR=IK`6ZvG$3|&0iASJ;|iKN z!S5fenu=mEmPy5r*d8@S2ZKqwvR-!W`TH(t*J~>1MQV;~cYD%4V>Q29HeFynQ&*k` zc$TuS1FI=(WoL{Uj9~(WxKg@UfGwI`B|BlR*l#0VX{X0c#KsaS+nFf&L^yl!!z&HRwfkLU|CkQzJ>@^HCyI z68zkGTxg?X%~%O?5afq4Si2@nALOaujGYT$HKe8;xO5W<(s|Xv){*UG*A%2y13Ycc zrH#6@K^NPa_0?pMwrGEy#6>@`MH}p-BvypTMa$fozQEW8QX?8bb^D`pfHgID5A32= z)P#G~*y53$8%h`dCq9Ny>V}gMUEALH2@pU8146E0t5O4 z;t2|(R|j};pbnIR>iie}B?LdY#`okJ-;-;6Pp0`R3+5!&w39IK><~}>*+wxPr6Fu9XiF+xiFdt(Qj?G+R1AcEvl@cq#R>G|$cHQ!BH%=VN*QLE%@ceOK%~w6x-3 z$@vwBf1LZ_nGH%@B zN#7kj{?Ma~cE(q3LuPLJ{HLZFJ!W*;W#}!NaMbZu^0wI5{g?F5xjALDH5uF9J8yN8@=o%c!p0E=SQ)btH+T#E?&SWS>#*UG@18s# zlpDW%Osrz{kl8ssz7kSCzU}eWn9rL2d~V)?w!J!>PX74VD@*Q7^EmO``l3_gAH4C@ zijR)B=+f1k`2MnwcXq$F{?@&hK3`otD>!+^rO?Gz+~T{A>v>n|_Ko5#z8?{Hs;;Hk zEx!GT+WQx`_(nc{i!TD_!y24;4TQ8ysQ>B~pK*_AZA@`nU*oI=ogI4AcKPP=_$`ia z4}S2~>z@|I5B+%rZt+DNmkn+7*e$*w|9*kNb+`Dwc_;sfI{)i`$ri7hx-0ET8#hzc z?@5Ql*gLyBk*kU|8FGwSDwws-vR|ZV+^lUvdh!$l_N?*os>t}50sTTk)V(5u`m1|| zr~`uk@f&=&3F&kj2fRPl@62V13yz+<-2aXcLK|~g;^RB>IsXzSbW!_?8}qjHOi0L> zYUl;s4O6pKvEnvd&9>nMGYO!*G{-rlVzA|49yl`R8%X@34o_;vIiSv~yl|O$y>5JSp7v|e8 zRimn92OV*+le-s}7O<>VF1=asznT(1;2it5;^f_~q~wuWZ@*+5>mnyT`)%tuZOy@rm?0a5pTi)hNAbSa$2$0nNU7 zw(pv2x8JI|`LL+x=pO5jEzb)M&gnbv*1S3`la9E94?$~)U1Q~!HV;TQ;yu(f2r~F)2pt} z{`rWZNWS&eRUgf8*Y5oE!|yt#=q~?SrkQooG$(9nz^pOH4kn#g?3FZlh(7Op?ENGU z|8CD2`ur3%|3RB^%Sv6Rd@}gyjb(X>?p?FZG4Hm%>WI4oK-;)r6k z^WaJIZl{$xz8o?4TGgY;%bdPi(~ z|JDzwJKm9%sTwVxdFa9R(tQ!*pJ~2YCLi80+CEY}xxM#K*Z@Ddz4zqy-v7$mdq?pi zIF#+tAH0?SAGxjg!^OQf&c6Hh-Z%ZCR;->ozNXW?H)1ks_5ZdP_moQ`8eH7#8yFDW zCsg)xT7cI930yFH@LX>vv_f@`RUyR@Xx!rx9pP@K>^;KN_@?4 zpKG(RLt~fkcdQ!_J1J-3mi3;k+c(bdKDYR{xRl?&ifiS$=|bmgU*F#smfSmZ=Pz0L zcawVUDB<5&cAd*FN&3XW()MQ9gR{P^WCy=pHFs(F>g$UteaC(LQTx~~*Y4J=n9*$B z54-n=tbNNhaD5w>F`MO4TccuHtd0%%_q(|FCtKKt$4v8OL)ES7P9GXM26#MjzM$@1 zpXLx_>QVa?4VozszpPxl-D*TE^}}B%NPGn7xx}#94f!Y^+MQ|uVsOrUCu8_at{9W zbgA!VudAW&1!bRH=4ehm^w{|TUEJ$<-FEfOjAxvWJ?EWgeff)_gMX<#e|6H0_PHfL zji3DBo!p(x!v|Ns9+xt@yZ@=Ly3E_VX%^csbY`E~rv%9b73{C&C=GM-dliwBp zaO3;H*L!_%wQ}3{ecIiW_e%VH`U+6&BN_!N%`q`54ZS?Ntwx{Hd8f7 z)zQ~}uOTMEx1)z!_>|<_T*O0Hu@s0Jsf$3wbHNcoeIxt#3&ibMe zo~oXY=b@@l9;qU^vxtG99i$-}l)SPdx`>P%0*WxCC7Y%|RdcQpF$5Us;a10}>{$$% zKUBm@NkH;vsgD!~n($9O6N>HB!DtN?q-A*Q*dh9;3A&gGBjY0{#6`wvqa#&w>f&RD z#}CuRG8!fH5<`I4&OlL}G^;s?k-&7Mzjex#o>I6W1}O;`!l9=b4FbiusD27nMLjbB zRKtt6Q>1As@uhog`m9{& zMq_sFfZn}l%$VUnL+mD*24ipZ(|WOxxb7d1z78ZLW8kb5 z0uf@qo~l55li4Bb!b}0MK5G)F4IC678$T*s7ZaZ_K^q@GJUl)!Zj>%OvSEQ>6w2U- z#i0|WX7p4^ML__2N`=*@ChAB@Hae#vx2I}kHrh2VIdhVM#I<847~Tg}v157!R-ZNF z5!kFL6!A#KrE-aV8M)b0lGy-*3fsk38gEE`b273?dvk-t2C|I-QDmK68o&ps+7pIF z#>GVHCq%^QMn%Q}5Squ05*W#r9V{X;S{oBFAwEGHmjD^a$;rwgozFR0Q&1!cz8x?! zE^c_-gqY!R(bNN^a>Fn}`4hZaQH$a3@q#@JoZDi+sPZf)ahU#eOdZrE`4FG{O z2!Okt0|GiH>ax(R(YcdI2XW|Q*zKI}siF$y#+$N9VZI?%!V>^A0X%vg}FQu=Aw-@pHJ63I>4!-(gmMu$3-$n=Lg;Idz? zDmy1@Qcm(zRZ4PZW|mPk(V)sPWvVhVRT0DERa3K44gMY;ZoU%gWAolV`%1e*8|_cT zl&?3uh;YP|eH)6Idyu)Wv^%)*8-!z?-Lv6k<_2yQ-NP)57dh#K!%lH5?!l)naN!hH zY037uTCOA4*r7=qT=C?%R#Ge}oAqRVjf@=fJ3t>N9D1CgotFL9;4heT8)ge-T$0wF zNy>{v5sBTQJW>pKxnh(POP)NA+dztzlfY|~gNvvf(-B#Wd!PtbhHBXhvzBY64@2*1z0{FS&?QQd6&10+=7Uf z=Ex3d=E%+!k-e!2_=?CaD`nZp8vPag2Ve?Ulcty#c>;lJB$EoS$oqI2*_5wBpEEB+ z59eWa`bslqIVp4tTMC{Vo!DM6)r;RX+TT z*hQ{dILQ|sHTGA)KAKL@xbm<`Aj+-fZ-Z75sO4!Bnp{^h$1&&Qj=`O$kCJ zRUnDCB>1M=luxNC&vxILNYdI4X?tATDaVLXaK%a6=}PD|MZp@B$y@XU8w6NJw_@{4 zj4Yk7H@fJKv59#DQ6^RK25Mtdx_~Yn8BC1~D%A^lju8B4+8T5rbp!%zw&oeBWa(}Cc?_<)8$%=`%~IZhjf-9 zNm}y~Ru`RRy>A}lgDYR~Jc>r0#~7RFghJm%6kSl7UgO9Md-P^Ct6j8t6HBluqd~b| zNcH6v?F&%|RbwvN*QO}^2mwWVPMF}6{~NXLbWfdax3*#dRi11q*UX^`#%h^8TCGqa z9D}8I6%xU~X`Z}etvS)vT`%nAg~K*wJ6M+_iD<7E%7c#b!q--(4v4Wqwj`7wv$Uoy zAG+7r1Tef%ML_LIdI6;2Wr@BamT(uCtcq~F7uo3{FFZw57LAALKD`SD8&2&YEOAYWt3FDN$MUy)lLtAUJozlVrwlUITF6KD#2P=TY@m!_n1xDxhHwX6E@QpNpkK3u1;tmOiniCFAOZv zSAFpp*mx2x%Zh(@!Nh38p9Vd1q;`~cG+%HzRxfN_iJ~Hdu2AR6V};%)FB1sicBF6{ z9aB+g1Q2gt?ORL`<|*j7V2MW2J_2^Vwo!wj)*a`x;Ds<(oox@gEbn-ohYaK)rKYjY zwpULAfdF4HG)cx=mQe+XfhwD_F*Tj=W+}xKeXl4y4KU)EqHqso0M~f)LNT?})D(HE z7{#SQN6}xY+TZ}toQGHBtJdto3&ULX!YjpuQ_!h+*nf`I3Sm8ULXIjzsMTBAsO|C? zA!vK*0W-|vB-4eK=lBRC`~d2wb3CGR+^ch};vM($p;gn}c%c#Sm=4!C+mt7u=7|>$ z*p!*wi2&3?s*_DQkhMfzb&3L*yojvOV_0Ea5J?UIs80AMy6C*3=nGg5qkf{{1+bSF zBwh=9Yk5Ukl$!o-$pb%X^@_5r5c)gAk1yCAtDS&;JAT45Z0x0W){Gp5k=NeB=25#%M#xbq(%!E`=O&BEx!Rln{q)nB$gb$8-MQIRl8L> z%cmg(eRn;BCJE5+Wxs;WyZAEeQ#_bdGAT{`@pmujg|oV{2N3Mtl&W{E)(H#OQGOco zwL)LLS?nR&(A$avGWLxzj-;!S6}=oq2Yp{uswf&qN#;4e*;-+)Q|Mhq(VGAW+GkT* z{b*31>%Z4<+{+_xt|Ik}(D&4i8s3b7ATnqlx#~rUzJ3vwa2Z^cWtf9a8HED0=4V_lzJjcT0SM59 z%cyoX4;y8z$3j-je%29ND~KYLb)S^1JS}B z43ReF?_F7}6|q4_+2{q9L<UJ%b-Yd(?%Jp%4_J6JKU4S`f0J~U;ZD4H zT_qwb@|NH){cOq+C{`=9P;9nhY$;lgILycdi*`a1>sU%w@Ijk$3SuPrtJT?}Ad74O zXi&F(po<2B*9fh-FPku9klj8IM_`^KsisV?m!kMoxNQgY0Nz8s36SrXPJEhwZAj9Cl`;tRg=Lkg!T%0x&f z3{hj`r5J6&Tu-$r0%Ng{!V?QYbYq1l@2T+8`;Jj~YE%@HPNP@09-~(}tj#BxU-G>N zRC@Ta21)ddjR4pAnt`lw$tpAzdJp?7!tJ9dx&klZj}StHF-{ReEKJI~WA#BuB7}6$ z2qDQ^Ygu;7_PfozED1F2^uUB}$tT0RgYd^uvG7x%uxM>XWs0JA;Ekq;9Y zSQe%uXJpVFJ=Sx6G#sh48)bSyJ{kmJEvdDP!l5ws*CTHyq% zK@0%EoLC~{H4p*_4eLhD0gqPHSP>~s<5~zeWA#~I>@s;_8D+Yob(0&Nc|q$_(@Bu~ z!NRhKCt4d6`9(;jp}$$QSXX=`7B$9R6f33>V}hV@GB2D-(Zq_-g~Sy_!LyXIn(@sd zi0oQ}ghx0v1TYe(LgA$es4$Rut|$t-!2?dML#D^sqy{Nq(>4ceRCNM6=liIV$fuhU zxWp&n7#m=^A{}g^LFG|4Z9&Xmu<6TABAY1FqimAl8`{5Q6Qr7oT4^tevFQyGc+0X7 zMB0>10f91Ss!1-T#YsgWThq`GYhR*ZQ*~1@uaK>zcOqjc5Nv=;GZ9v|@>TaRBv3=y z#X@fZ(rq?ddz29a;$Z|fl#M2JC1)d+XX9HYd?XJnu&*Pc#UU5wV-dKx9HiNl{R!N> zP)&te-ekSm{In}?Zv7dSP9BYe2>Jt9K)BaHl>`AvaFO9bPq__>VU@}WPS0v%@$O8aXymJMR@EIYT?BKR#QjhX;!K< z-GzyPSjOC+J79iN{4t-H`o0KCz0am}L!ic$2`%f2TWE;kg(YlQvME!Lj8%R9ZmPI_ z+=Yo$X@$eqqnjxQ25ui(#j0tnV;xYq4-Vy*l_H}}`8g=!1vY*$SxBQi1;5#hjrh=V zMbT&k0bRiWkb&5^0i3caXH!PYLMmP_aJv2)S4Ghf%8WsY@9&@}>W_fl?0^jOkU?jT zb+su!rcB@>`Ub~YqiP`|qTEUI%2bAwrnt%Q)rr7f6=f6U3u466w1!E6T7YIGHbiZs zb0M2D6#0ruXLhA62QMW98VG1Gw<%XK3Xo<~u0y1R)gHf)#t%kIg~o}^YQi$HIgW|I z#Yo5e+c0ol4A4kmQznQ3u|4m$X9b9X{bGuUtrrC>l6R)>7(E@R&`w*TP!i;E7+%2-s( zsG@yy=q{5Lt#gL5VD8bdl6hNaS4N!2KE&IUzY$)V8U)l2U+_m+5yaMgUj!CqTFhMN_MDaKvf{B4Q#1s*mi~@F!Phlz4 z26Zj=2U(H!HzX<9ytJwje&vOW`Ez|rf1a}6sN@Ct_mB{q@*$xs*qx0NjdI!{#@e^9 zHsxnXvMG&37PddM?$vIkUP~(}NI(K$5x)j8tOx3`(cL^;vxp8`EFKo@*{D~l>$q?4 zgEnO&h7x5dHlMU9%MmL4-oBGFj+SUN#n_a!ZJ5MaJU+$V*QV^iIAqa)cbJceAUbEF zO{;RztI zLG%<_BHEM_?bLK&sFx#%!GowF5=AybW!o={U2{H;)zb8kL4Eq+YnF*c>h! zC^NbaY@sM}K@-swO_kLP8;LwRk14zW&u20^@GC}&&a$)=j!pSRYg)lBpbpDM|BuKM z8m?9pzC}K4bgKcUhhzi?UEz^?4T7w_r}%~h9mNh;Ej+YDYi!D9z$Ba;ujWvvBv0>`kaU>Gyz>*r5j!&rh(aEo3KaZmmw#aPC3S8owM@cx6 z9_I>IT0V@|CfXMck{DLd{t743_5+i;{(xn~{R&FMxKW1Tw!CG7dAc0}nAWP0{wF(6 zR!colr>id_iH5uqBGBYeFV68(l^c_}T;Lw+x%meF95 z&n=4Z4Jg6r#HC@R4#bNObV1ryVQJJBh^d3bGK9+|gjn-$BLzKsBQ<%zr#G7-ey6|s z{6X3FGKH@fMQTe15>`dnE+i)4?Maw8U(~&LL3ASZqICL`O@C(Mz&9_b6tGuIu+0NW zk$6*KO2sn;kCBLuebb?u$<>%PFOGKxbB9UcfMdTU}B!FA*SX96m zVK%MDo0(6LRttA067EqJ_`z9XEO~#5SY&b*qS}}vu@aH@!HyS{`QYu41}-);CU@ZR zu834c0KOv9R{_VSyrN_)3151JBT5i2Sn`lWJ*^glB6?aeN?`M>u#6fE>rZ5=@83t8 z<4r2!OKY}^&W;A%qv3p1u}fT--nX{6Me_`;mIono-D-Iy@M3Bb3kgSn&gq7`GNCmu zA{n(QUqUonJ;tMkSeH{4i^mWaa0N1(@&TsXW((E3U70CLTBM*^6_nLRPWz)u-?7O>Y@2vg~W{d(cZATNwW ziiKwYp(y+uf93an|J_z_Pp(+_4Qhh5uIw9|O;|ENon9Jj5{M0l(g2dBP|UMOLsk*#P2&5qOQJ(?knlyxy@)E9lgE$6W>Y(d5J-YQmsW ztyvq)TE1MWAd4@!@0#1h&i@Fr#8lSmdKMbwjG7r+q5iUmMsSRw`4WAhoHlo1!9*cV zY5B9&GoV$7Ic!bpmk3m?2a5}=V z2+u?~N#SW;M7h2AY=vh&L*$j0hZyk7ypFkEi)dyA7aI5EIC@@}o*=kSXD)1IFlz|R_2*@x6EcU$;$b%eV!L-$Yte+@CB*Zm-E6Az2Ff6 z;H`I&gkwfE{FsBZpo#5W?es57M#1c z&dJzJD`;G!Q(~R&p42-Y;ZrL0osQ^I%3}m4%-><20=A!c8K>BsAalI4OJQ^sb`@0D z>K!YigDTOdtbJ)Uf?Ag!)Wo9%j8;MW6l)^{IIQ%@Km^TBz7fKa5*JW|O@kA?*vGlQ zdqimI9A6wyC^m~Qif!i_v0Eer6rl*Kv;%4t+s=CDb|I&AZhLate22 z$~)_v_p7tXNt?Hi9$Ca94-c@2t)e`ob=$5#X^n#i>)XRtUDrqrJPp!2-$1K3NcwMj z@q%54kv*p${(8>v(hIGI3JDJP^yZdB%?Yx5<%m|z7^m=b2vd0K9WejO!f-Awt1XxA zFW`OxwxvCeFEQzvOEs!_UD!dvK5UX{Aire`o zR)i>5_(XVVYW)A6-p5jl|BU_~BK-^>RiXzf(F2v}Ay(p`;*)z9gV00ReH6z^g=d5p zDtysXS72pa_t6Wd?2HHbj^MmrDAV^SwR62cW3&_Hhg5ih0SR8!#K0SDjlMAWmacY z2F(X35{OE8tEn|jRxJ|nXg%qVO5x=o$ve7HPWNbw-nAA?LcC0O1hA~z;0^rUli><4 zCs}!&{G(VW5&i+-UCmwuZuSZ@doy4-qTtx&jryC{A}=EGcjT2BozejQxNtotC4UmM z;~RZPJ`)>EnK2OiLvRF?@&y$dg%?%%LMdF8zDETdwN9wi3zgvQ+|rvL>V&ch<<_Ur z=V~hc1UsVa1^j(#v37(uHf}^L25AUNgs;jv3}-M3Klo^?IcDrQ?HKLY25lb$rJN&} z2G1OB7Bp+lf}&F+^lMIqvP#lTn*~jqQzg}%S%`UjY!olPN;g%J`sDY2mjlIf9xY!# zT@0s6ZlUD9Be{De*I#m-XNd4B$qkm=A(A^qa(|NGJtfy(>Om>JT5@kmuG35jujGbF zZj$6qm)r%C`-bFhmfUY8_lD%U%@XmolH7rktC!qV$(=8`FG_T5lEOzN_p;LoW(a*dK(D7h;o_Z`XICb{28Zms0HJ}2UDBe`nHjgZ__$(=2^OC|R`$*q*! zpCs2Rxeii$`%11xa>q#Shf=-sr0{B~z9KdF8^F;?R&|(2Z;0fM zk=$l>pvZp}LG}XvF;^qe#Y=9iEc_aGn9;6>)oWL{X_Gh(?EwAFfSHt*k%}xE5MG3)u-}U(S*ofF`_7&{J9Q$majrvfRi4V9 zDc*PIVnHXtNkPvMfT8fjTv3vfumK}lr2Fx7=e-!KB3+Cw0qEFw0t4Ynv<6b#LF7a? zvd0BcTs7hv)G3(aXl#AQxeg9oz%LJd2@bnI@2dv45Y9vYLzz+>E2C8#Ip@%vb8aH@ z;T%whYV^Spz&co(aE+V~G`HOH~RhXe4}x;ac!e&mnwOw zV-l6!(bWNYTe5NE0>+L!$}4x^86VV)(en!VX+x;HUAXE+2=+K|by=d~Pio zbMAm`A#2Wg);6hjuW~DO&2~w0R&!hy>W(JjK5xx&hw-5KxR3Ex@4Gg*GJ=P~CWIZD zIdRQ`-MD6PitLVZC&oW{sw?N&p5waU85kwjnR_hGp@{?6B+iZH!I`16949)}DvrC3 z$7s*z(?b>J= zG+!~EXdwEDM{eMUTO8>3m2=Hxft<(%iGDO_M{g(2`+NxGt{~)63lMAkyg8e!y**8iGfDh>#5fT^u0o6o zeknhLVQ(!E;fkjb9ydIV@p#~YnH`P}_USmEPW+p|PY2+13`}RjIDmLaC*f4mKl&3% z`A4Du1K~V5Zap4a$IzNR7>%rgzu_EBbM-^K^oT_~xqg^mr@-HEo^3cU)BHTPCC9A; z47tzY*$zLsFW@-{Ke;}wF#f_%@3-ABU*3bi;oM1cXq5`{2IeMmPvA*{zZ&j!JTzC5 zOY`3x_#4iHG)G?V2pR|m?gKpA;V0J-^IH}C4d*$U@Al!jhj?=JG>2gPr~S$U75l{Q9Iq2(D7usjhI%f>xi{XBPrv!f5 zd;N%KJHf;K9nWs~sc+MsjQr$2pa*^xR+OFbxDMqw?ESbQcs${!b$Rz-@C+~-xKUVj zpM*aTZaJPG;HU3!yWp{sKf8XeqdD(qJb8#G_g)UxI|PF@Z%;hy$p7T`PvL-OWc+CM zxfbq_RO2-$?vT)sj5{P}Pb0+ckkEdH$B{JdcqCDpMSf=wUYO!qj~e!CcuRCG-9*sf zBg~4(T5kZ`l((yFw#XnG?kYFZTIR{M!rdBY;rUyF2{(yw+sCGKKLYGHOYMdmBkaD# z)^n(jcqM%e(r^{ENkj-P0U2HP%Ac*a*<=Ma=NT@hmIc2%M*xA+d{bgCh-FMw%Y!&_4@UPq<`e(ww{R{F( z=E3`x=v@Tw-t8>57~YyUn0F<-d25+>4ZH@?yAIxQqPG;@;i9(^-U!i)tK#xTthmUN z@cN6f)$raFi?Hg=&zIPg?}D|$Jv2%mBvnE(aG{HCo#Eaij*9WkDEn;T!XXiAmX5t5 z&HIEn`z*DJ#iJ84;d_>^S>U~&D4;<=sKA8}CSbVmVWTqhcdO8TbTfOx)3{tI6jXz! zyjeaG2oTB>LkEEYA)Z9jEFVm17Wq>f;F!e>7WuS|EW|Qc&$C33x#-pG(2JRq)M$kB-@EF}~SIPP&g5GEdCA(x5OI1j zf0O}=rbxqzU8fShD`FTZ0Y`^oow!r>I7vqs$BlZl{5V630SBOu5{ok=mhTVzVLXV5 z(8c@5kBx`wjK&itginxkB&AG9PtHu8V!#ThseG70Iy}{7qJ}AxN0My5V1 zWirQIbI@gl(UB`zTg!khrqn5%Bg>VXN-Qxj9XMFS%cG%%39T_X7RqpPx#^9^%>b^P zS?gK7zrZ#1o30l+Qp`{ANL8s4jL*&eqDW+W1k>du*B4eI6q3BGOzs&+T^8D82=w?2pZKgH#TaGCg!ZhI=;Um0V-lB&T8~c7 zFb>JeiH7#D3`mIx2O0jzdHBRMk~N5@Au~lv;JCBko?S+Xb8pwz+)h+oy+rErfhl}S zR<40(ZN#;9iOmhqF^~WoYiW}>*44`rhMoBt8`eXjOdUM<= z)H&`^)`iWI>c7=B&Nu}H*t|pRhG9x-%CIqBI$}rQ&Z3jHa5E~&b3m7vE(D9W+ z%qyYVNh1+;ywMrP^n~O|+|P)o@*2i{3OaR}5WM6m8MCF%Gaq#6a)SaVX4o0XaYs=e zB#*?~fWC(=70)!B%?d}WL5ll2>9W%7V^y4CnhDD6umeJgi~*u#l2$1$tzK^9A;)&9 z!Ekh!^4xaNy%XRqpg!^%*vgdlR3rTFcZAV zK>je(BsL6Sz=oyOVFor9#X=7n4MXUm;aMgc12|r*C(T@sE^wZN{)rrr;Si5b>A1W{ zk&@0Ub_DQ;_(o1xE+Z`oDSAhE9{cGXvH6EZ)OgrO9R=&Zaq#J2r!@v)9sUl1pME=> zuKiA@)_7CHes)(1a!&Y1=$-8maMxp>AOk)ak%pnzOl}geWk`l@#k(jtdtoqS+*u84 zw&YI4U%Z<^Nix`L&cbb#Y}{AL#7&kMxKo*JT6``DOgZpJ;jRlAqfO;hP&cJV4!q-J zdF^G9$=X~PttA?=L5pbim&ha>nTR0+yei}(2$-{li?b0Ls>aEb?ceT}m*!p6~vQ?~mqvocj%V&!ack<%FoBHX!a2_l)3X zE_mD<|7Ng%{`gzp=Kg>-018KFXnm5^QK(x^T}`P)>BbcOd?DX7`jOe=OkgDw$PHzw zkrTb;{ulf-R5s*oZOB^9T7GTp+S0XS-p+n|=G%F1`@Exi$L}2^#^Nj#j^`SmHE^E% z{)0HcbPqa^o%;?8>hyDy<**Jj5S5G9DFF)7McqKrw>q)T>=^8QuZdCX)h!ytDAbJ{ zW!xI4wGMflI^eOqPO8^;Z*yB8{?3I-XR8Bhx*hIxS;>9d;nk5xUez{gwPI_h0Z$BD zC+DVP?Ptu&4DeJpA@f}>&0PLbt1?dvTR$;u{lu{K6T{X|3|o5y_EQH3gaiil2?+`r z_ityo^}n=0`1WdbVWam`{13ZD)n4+w|F&ACcxH4!m;BcPTt18a=Icp~Tn%pH-W~SI z@hhJG7v6pKi#OAIuN1~OtXFO7wtLzRzc%ka9Jng={GOWA$(^^?UQPe%$omK8c6Z2+ zNlkQ*UodNQ%YMmQR^E4C6Vq|*s{5^bO&ZvB;HcuXa}Vbko>we*V%XZrN8LQn`}mzv z2bNjaCQeL0GU%n^r%!zSn$K?aC``$1+dB;kbXNv#SjFg-EI2_48*EQ?H!Z!=Jbkc=pJL#}2=LXLL;R{TVfrZY=uz zC+<7PWmSuV`web;a$?ERjW^Hq7IH%KU!Ifo;BwmZoTS#iH`bV^K0TH5zvlPjf`#&3 zo3{SEGj&(@8$*?eE6?ukT4akmH*v1wGl#|}(kk3OJ2pt=-~%>XX()E4=+q}SQ2{1W#_)!TR%pf+;nZzJD;n!ng>tczm;1#YVJpu7MHwR74lKW zjAo6!dtdxe+y3sh9RqpXvnz78o3IZ?Fw<`fpmc z4(i*9zx$@=!Slk0eTr?BALOZAi!jHS!IIFFHjI`l|1Hi^^qp zc850VxFlcYcD()hF`;WZFLN7mXVH=kUli6XJb8CWf0xuRmbiUZd}Z~-7N@g4o_}jg zt7+?ZZhoQWUVeyM9}X4(1#qK^HAW$UqzSe{xlU&cGlm%%EtOapP3 zQ2&)>>o@f6emwE|vbh=IyFPy4w<#;4-Pkh&LpAFg^Pf}(I(%h;Wo!PEH{a;`m}To= z|Bw)@GjOj~c56-k$#?RPtMiZkON=`AQuh!oTl>}((KCBZl+}4hswK)juex&p&S6IvUok@58$FnFiLa2Ywo|lx@N)u$aeZK*O%K9c2^EOHFtH-b1^Eum+zLMxP9}k zO>Wa3^M^e)_3D}tuxF+F z=suqYUF~w~kVE$Q9=?kE13r4X#P(AEuwL8N-uT5@J@m!*v^A55Og*vk&ZjpTwS4*N zkIpZoe7G`T^|TlDAt|d?Im%}=8#h<~!=8eDH}l?1Ee#LZdq*?xSaQ2&eS6%ye(cAM zE7#{Je>F}R_VioJ#(#79Eo0}OZ|sj*EbI53BB=fNl_!rMo3pgfxwxwMYvcXS7GKGa z{8sNb_soLwX5Ljj92ch)jC1=nVPs-(zv*8%@7-F`cFu+&o-NKj7k*>SxmP@|q@A(b zjeXq0_408yU%&Td)x=(tYroi>&}8$1MGv2QV&S^p!u5Nf-}icc+?xwt+^SsmzR%Am zcbP_i;G6i#98UM`+IJfn&J0+Amu|u72_Up8iKt(%*5K)}!l@PQ#wJY+ZNh{V&%XxL4(OZO|n}RK9!R z+7-{X{`r)_quet0z>eG6UIS)-KYjd;?>ksJ^_=+fivtr=l3MPa=J(TzbsMHRoPBo; z%nUnE!vHu|9Q@pp8|C9xEq)GMMo@7Szc{pgQ9g;9lF1zEzQ`!6bN zFVfk2U^0BCR<~>6fxrFK81jPp_F)5HA2AqA!T-y{^L$yyhP;!>Imh`j1x}=8ZVvNt z#s7o09@}SlEgqn{{Giz@w|||N@zP{tg6Hvf-MINhzg?M=KCI`2J>@?q@0z}4a9WS2 zYnAa`odVixTen)#_UlH?md;65kKL$_P+U)1RG1L%ej>Ug#P##AAD6HHHdj5m%lx+^ zJ*qC$#zZX$ENo{?p7g?m{Dh5%m+N0F+x_17BgxG&{r0xHzaa0+ z)5#Bpls&O<{l9c4{KUfb6ARb>D=l1OyW>E+7YvKsTmQNHi$9pTUZYo9Z|zktU(&75 z#(weLK3#sW_?v%QGuO>qpKM^}x?ey*aR1=odNbD{Qp8`IxdsA!$R?S&c6h0Qnd`Bl znd=r)#}L3j&&>6^BS!iK?zrsXo@2aT`ueRkS=SEybo`>G>cq>o-K_`r>EER3!r#0W zcUhO$e823)-r2)uHv8h{CD(ly_Z^>=@p)O&dGpD`b4MD_z51-`=rwsigL(ahk8(a}+qdQDQLZavF8|)AS+Db(TYuKoBR^@h`!9L_erB%! zWG~y`?Kch+{(EMwmz+$<8Tv(e?rXAQW8c?p-@cgh&5*QJ0~|(N5Z;hFd;6=+pMgre1tMGVz4U`?bv; zFAZq)ev8wN-QSG3o3p%k=!+eH`oQ34aJ*ZydvC4B3oB$%Qx>+^(DOod^G{Bl_1fv_ zQ+a0f{viFFlbWqZiiV9gH(B~zpXd8`+rOgh{X@@J%=++Z-zCde<~Qy5h4;moM<;im zYj7$a_wKVg^Zt;JT>5r@JI=Z$vHgU6P?JetXO5Q{EmC!}o5xefXm}jx#QPyZr6GH=l9uGWz=EreoEa z-)_FVc*|-387-~YreCoUTOkL_|UI^DeQ!JRV=_NV>9=Ti3}-2UM6sXDlC9GH{3i@DFG?qTlp zsaIM0oYV^D&PqKD_u$T1hP7~w2eMNSF?XiqE;NYY*(^-TAjBoZI)j1>MW`f@H z%Vb>ApJZb?Ob{P?5r3#Wsyph@@TbD{%Q`(Jt0@Hpz5(Yd`RjigT&uS0CDF7&{2IW~ z#jYicb9I1#E4Aw4dllC}zhuqXbtg z!L9!ToW^eFlTa~agpbrSH~49&9ysvKO2PN6)WG09=L&bjAK=0oWcy#`CB2-me+KQN zMi-{A(hr})@jj%Eo2qJ4-n@p3xA>CmymiEId@`&Te$biYJ)_OxKGEi3s(4btpRTin z`vlo^=J1okDfmz7%+FOJR6<{{S;DLEouVJU<3r54t+*Bmr0)T#P>N3x@mZQK<~8)$pN5B0dYDqEB-Pj|oAMbX);PhB1ZE;+Dj@>!5i8WlrB8Ue zoW8U383m%U@fBQG2s`_<6yH7ueQ#4L08Pk9@3u{ue^|!UzdQ0mtzyYdG#lyD&m-63 z;e;>`gH24)I6k4NPQRl!}prJiZIaUn4&!f z`Z9AIs?U_wr-8g!UO3J^XCzYxiM~*@od`|Y%GqDbpwinIt=&r9kG^^(Q6sB7AqWfv z-#`~}10f9wAXD65DCTNGaA4GltpsONKF^{}u;>!b=wq;+h~!~KERSUtYXtSsO)7dA7f|v7%(}Jh9Y7)@jpr8IB`Ff{IHWD z=4Pyi-rEyf3Erl3V9~_-%F9G2WBrPQBI{XIVF!R}##p}|Fe2-B!LpCY`jrnv)@Kvz z1rLH;?Yo9@UMEStVF>EBj(h1o}aHE{^wTq<|DavaEfxW|9EVV8q*V>X| z2$JxV^<-OS&;VUvDLNwLmSnzsKT%xA^EsH0A@k?J29o)rHOQ(WmY9oZ7Svnhambry zF8T~1bmT$^DQ{t5Y3481Q6&0@8p9a`*bhMmVs zC!t6f79hsQVo?LaVNV%W0kkFV2m!+we3`$5P$khHm<%xGnN5qV4yYB4N*L}A^OhAv zuuZvxVK=VL*ZX4ik|zO(sjQ*C5dre094u}oi&GS4U|iC}9#V>ohDVhrDxoE!QBj7T z0f$S#0|BgM`kWi4j+au~Ar(d=DvGL*Uxm3H(;fcu<}Xen0!>s6$NCf&FW?wrtI=X$ zCqW}*Wg)Rla8-qZp-}*aHGioj<|zu5fYVFh4QhW_O1=Y{kxVS1aRVw!z?%J1VgUx; z28kV6;>SpA5UFC3D<6$SYXD6FLt_l0=|%%KX(|z|-AaGiqysD@ejvH7QPNo!(`1{n zxs{|0L*fK!7Ah(QjpCrlycI>e5h2b+V*M+MkekDQLTOXps$gBZeFI{}8L0*dHRzO| zU!WrX*fOh-&M-Vn7+}Rzek7Ud%sLBts9q6?Swy)Tk>V(sbC-Ilg*p|DlPRE5Z^0CW zUZ)eD_S6ZvK04t!=nObVrjz8&fc7G&1Q(zHoQDQ*7!!%`_0b~;9kiFznN+Lr7iMel z7e-X^m(A8tUf@wOUB+1zs7jh53oVYn3@nR*Wic=sFtA*Kt=dB0JQSoCmas(wtxRe- zTQ!JyV=st!4zdUTYvj)o{V$V0MfAT&{?Vd;Dfx$q{>9{fO7t%vKdE*Gp84eOD*ES= zzqRO}O@4*wpH6;9(Vs*9yEMC7XC411%wqc7 zRd?kxF$mFqL{YdD9_tJE^CbyeVX4svgX;^0j zZ*2P}LZ*v?oLt9pv-}mfq2(qo$Wt-ATWOzTbw>ZOLYoYsZUqz*kpk2yZw6lgVJw?!pyjl{25cL=R8<`NAVz_JG&%ci`09+{Y-S(BU>PS=Eh zfCETjN-fS(BD&r$i2Rx&&GHjVQ8w9U-Ycc;N92A)29>gUeF`ryg`PIL@hDBMSzJ-$ z5Lo(%E(wt~`}*KlZHbyF$ErpJRV%QECt^0qEqu#E-R?yAMno8)M+6!-VE+th5ts)P zV8avYnTG*4ONaY%q~sa|XIGcmWoT#(=tPPszgAudr-niB<6^&>BYF zNNhyc3Ku63RiiH$G|YlFH+8*>RXAeuD&+kTL1JKXVdMLuZWtOgXiV#~=wjs83x^{F z)Dz8Hah9ypNNpgt9_R}p`I^3z$0M+QMEQwV2c zNc|`T=?AJ{Z^8Z}*~iky=nE5_*rJWBGuka&rcD?OpusAX)gLQbVTcnXdkM7VL(q6c}nbVN@I599bzio5|MF2I4Q) z7$thM0cy)5tJcX(-C+J_17OtFBVcaOQN1Nvy>&DnCJu@sQuqs~5I~Xjs-m}uQ~)ap*fJcXC>b=FR5MNCh3HCY zvA49EGP3BAHf1qF=Ez!L`U%W$fWXnF{05>TLH4Mt_4jBysn&DrYCVzRjI>_#U{qD; z7CK{)Fg0S6EVxXRE*%F-&Cgd;8L&w|as#s<2z&6X6j;RmUQx7)T0$KSghaQr*1gjsx!CoMNM{(W%6!pO%lUug1K6( zn|Zty`%qxFe$F;l<$6n^Z#LElAy^}5utwmqMzC9G9bYfnb`ghLujU?hQ;y6g(U1N#FZh*Ayr2!lo?~6=f(|9OPJpA>T*`WrPN*XHB6iuZ23V?C zf$<7KA8I%nzo~z*{e?um7oUXACyi!{tYA;_zG>Nr>uUoBtw@d2;pF?3j^y%xztdoeW*;s~v#5juoMXdb*!X`d9Y>ShNrC3j5{M zKoF{0;a4l3$6(#z%uI^bcXt*R~jbuce3Xyu*pn)fiw`=f{s$Qwn*b)V{%2AbuTUN z1o^URRe~$*mO-4!ux8TCpG?Kp+YMO@kMg zfpoO{E_5<+ZHVOyjlajfCt<@Rx(gng@*oRVZiBu6>RW8tR*E=#XBJog1(!p%k^(N9 zay`;8__?rw4;H5_9s?FYVz5mFRvf)y!PNoH$Bawz1*PbU@{IrhE8HmBEPt+uLg;kx z&qGRud?aG1_zL-O_^4egFZE~<~kgB=oJ626s5ULg`Sv})WymcC7 zK%^h2?@#r@uF+4b=RTrOkawhLvwV3W)om93ign9{PpsSXlFv>q)y;%BEOOL544WYM z42{^4Bzzdx*xFA;&97lg(Cep&hA;0CDOiuhVuHf7Xj7h{c(hF**`W$S8Vvo?1}hfk zNvIjs5EY6{qJST5(-Y~8_%nts5eE?vJ6b+LUSPY@URnW5{+vn+BJFZiWD+}ynK3#$jx9cAjK&nZ@HF zg#2O~8qY0q8PaGcExM2`>jhd07r6Rbe-S+nta;gb)%reU5slZuzza^D*`5Q&ylSvn zsWf_v+ZT^YAU9pbeuac$gRJH*jq8MWvY6M!x)VcJO-t+3K#yr8gw0Aur@+!07wcUu zy@z30DGw&YL8t~M!xbVvJFd^sF$zmfl&Pt;NhAxnST`cJ=5mAlRu?2$UqiGMN%_UO zcZ#&Yl?JT)G`M5Tx@g^v*#jeO^Mc=GCRo(MaLWsaunn>){aJe^_>RHu3G&QFG8p8g z;Y>q3pK#F%eWa|e^B5A?C$@N0QcnXfF3t-BEgeG{cDeQ800O0cBi8fX2r+!I4X72z zLTG#}K<1)-Y;UZHC|@McMc6tQYZ9xOEfN?9ysh`b83!7HUuB4|xwIH)F-(g61SP2Q z28AFiC!8`(!dsyk&su*(BcLVyu&(n3l{6k$JAgt|ov7|kVJ{Xo`h&|sI2(}{(J)+4 zX}hVIC%;bPh*|zC914<)pd+&Fl^;St#yAcV*t-ksuMkEi2UsqYl|+xjDMEkw$m1|j zH#y}OCFpzXQf$$B^Og}lykix-&b-AvPj7L@ z-r3t%7kb3#BMf!dhn8W780P9&rVlO6apenk`PH08`(eT+N%1b2?aMt+J~zmvGhEtM z7wq;a7x}&Tl(hn^4LSO z=!R}oZAldx$||5$DQHu^3SeG%-O9$*6(^C1d6JE+3n>Mp544YTmTH#Dfa*s(basoC z5@>uSoaBM2M4K{(p)RVVL^>B$1`!!CXf!U7 zpeBy!49v(Km_S?*+@n#nb)zr?C|lwrlHod)TCKIU)mq=TzTLE~F9KSduq6lxhzqV@ zwLecBm$*RyWq#l1+?ixR`hMR3|MSb|Gr8xUd(LzA=RDhaj*bkh)Cvy0U$(UJMSV@P zP0P*!aZ@kSWL!Lpgz_@l%}#s?FrkcO2LhgKsL4>b@gO|{Pc4OZ?XXMpa}O^;re6_V z?@pQp>P1c0JX*Gm&O%8deFh<~*RNi+W^%+YFJ*X=;~wOZItuDj+TSl-()=1Z;%nUq zQD%49D^=E~Jr3EtciMSxIV`XFZ}Kv%y?;^%kLHgBSnmae?V+l-gVv@%Q#T%`ZVNPh zfn&K?(0aw{uG$z#PRI)l>@=PTG<}Ijv2em!78>{xaGcgY20&k47~>|b;+to~ppWjp zFl~%$>&I1<<>TrRVnt6FH%lJJx}MqhEwrY0UYW_Y-u7;78ozCyGbCuamSX`EKvo<~ zxLOFN`V+U1fW9U%m^2Ugx)+`#mnl+u&=P8{_u@pkC~;0c(4n+~jdW914j#_)(+Sqikji$SzdKeO zFZS`(pBQl#4;kTdVG^RyCh>J1O&?UX)ws$a|Jge1aW}a)Ju_&%@(fKsbk$m*f3BYF ziC7=2uPMCNy4hW8og22UtFEp1dQXUJ+N^n|(ee)U5-^qeQfFV45l0Q=unw99X)AcZ z8S!-+_xzqTytw5h{wrSM3lDgZbUt7Qi}Iq!@VQ77brpoBVDp-8$kP!I=!LmY(X|{UF(4yww8E<*1&MZw*lW5 zM*L4iAtSV24?vq0ykZ4+*iDqR&xhy&-6|Q`vhirx}{&5kUaF+EtZd>(-1gpjQyU z%rJc~MTfC93^0>{0cORvW$yEesFIm(Yu*_9lFMd?x3PjUcaPFlPi>{gsnXG=#RP<2 zflD^wfvDT3*-cjO`A^fGy)?-=_Dg@Yg}|XS$yI`1MIIn~Pw)Q!CX1n$ND+9{rcj+r zZncbmy`OAaihERYqBe6ud-Ud3lpka?n`-ndx-@=QsWZmyt=`kUuAdHz_vFv>b0c)6 zwB=lr%UKa<3^oh&^}^-qRE9L=O&!0-;h}f9xY@Ma0=3w*U{w4>ZVmUgWKI}jz!{RT|(AZ5K@j8N)8eLm&toJn+^F0-{N;W9Ses>8YVf-hD+RG zt7t%^!C8nzIb6W4_c^%Ts)3DYy6}<-6Lv?BYPzs?D$l~E3#Dn%QK_TVK4^pZ{7T1S zxB84=+JvoyA_EI(Ot0P~(oD?vn$}frwOPN+C|6SysC8!Q~o18oGk;xw>( zCjYu)uZH8hmt-$GJvwtwslFGo-6L0=e$96y+K+o+gL&OFj*Bn-4x=K^sNl}X4Eze< zSJ?0?fL{Up3U1EiX42SeTt6x$q#l*4A+y&+ayc+6+f6{p-ZV--4Gf-YXG2ze-$uK!S$cJIni*iHrA>{vh8x z@|&5cbGOzNX20uqi8qOtDS62F2uTA|e_2^+5PateceKkANVzlVmS;0$>#ygu4S3)qn0hg@n< z4d7&n_pKGH_%}af1KgKJl?wtHq)tWlZx!E1B(k-tSJVx>^Cb$Y>pDk6+8IT!u}QU8 zYhX4_ujlX4R6rK9OP8TqPqF188U6;@q9%0WbIV<|Qd|7_nB5`n}iy z#a~Ke!9a^|w&7XbscWp_y+42jc!lqjF=cfld6ScxQ~}0I{ zwH*`YL$@K2dJhY%RIye3G-+vBtCU1Tdd>v^I^`Ku!xUA?Arc{Qfb=DP&;g{gO}E@`Jqt7!ukU2ao4b)YD&-Zb7xBdmYoLvIpQEEh z<(Lo zmP={YDbj(6vSEU!YGb4TMkQ!XE(%(n5G(qaaI&t5tt-sx@QB87&5(d^zaN!{g^R=dg#@O1Tx8cWy}z zSMpxRyPx-Z-s=ocYa@_}swaxI*(f-Z*ICBkgnTg*O%g*vw+t70c_2mKZ(5p0(uBF$5!xy{ z751p>WhK-aVgz&TzgTxo>vmHv{BN(E+)NBsUx~iVH)F<}fl6uBnK^?+jnH(mTfHF> zi5X1BUSW`4uQvZ{?Ncjs=J_*XXRANx%ylzl@J}|aDH-^KzMhqT z3j)+xwq!YMT>;G~vO3xJMOyb|k8Wf=O2caPFZ<;d^^OFr>D6fCMUzozOP~wP`l>cG zX^8*ncbSV?^Z`4i8iq=D>y<0Z7DiHPVGEjVqTvwwV*2Hfhv@5sLVWVt@6(6eYaU6K z%w3aCLlqjv(jwTlTzYAub>BU8P)W4~FF_xzT^_OWtx4{n@!X`Mpt0hTL6L-m?SY#= z%%Wgx-JrF6=Nk!DOe*BtOg;u%O^BiA3TGzVYnNMvz&+p`WKgpMcn*&m^ zk#u}Md4sL~{J!#6AVbqdle;KeYNkN_x?+->*H+V=0d1#Vn<+n2w0>PN-l?HG8OTDD zN*XH)Titw;`qC@jdB=QZ+*$b5c;n8?^1E5}0|+kR{`R$|Z~L4ORP0l1~* z@o`}+#0rdi#8MKf+TnFL<_?tr4tD1i?{WPMDL}r_awB`^`h$1oulvp7J8#S1EeYs( zOc40cQ34Tk03^-%y1)0(6qi=y($nO#_|E~1&?}qyI z-z^!y?_w|iuJH5k`g-D*H3Ep|1CQI!U$oiJCw9y8yCoWj@chZ+_WL7k_VYKpc|v|m zb8$jO|%~U#d1B3-U_5YkVfBE=pY%PCHD=; zgoyL@#$`gp`FrCsA>!(L<1(Q;BZZ9uEHXeIyghEe$%D5x`%OP?%iF#F;1b9Bo_T9$ zE~f{+-(MfmUmr<-eI)($(f_8uKFajhN16WmDAQjb{r~T;XOGxli$3~(e@UFz?vdX9 zlDNbBOX3dcuSFl__Lsc%^_RTm_807w?lQp+ee+GGBDCyWX7&qr=$X9?xiRY{U)IZQ z`Q0RFWX2fx{+R#v%r=aFRNj8fQVZWrSlXUfxZdQg(WS&Y9%v1ZJYc%IU<|tKpzuTUpPMM=S zd;46f&zzE_P9NEC%k4LL$PBaJ{k$J$>0 z6lx%1y<%2uhbM*h7~E}2U&WPYPrDI425fCoVZ*~W=U`*78x=)u#o8<@ey4FtUTVl8 z;{&JdF-{qvK|#6<-fcE@Nhc6L%Qz(r?GPLcEfE>)w|VqBBB3#YgCRH~gS{lHB*rT- z9>Y^KIQD0OD15b1-kxRNkL{Zent$&isdKRK@ZFk+cLxklVZazXkgvkXvqNR*@gvcf zUTa?@Iqrk!WlMTFT`Q!sLfjd)n$w+s$#X0Z`!*QOn9Xa^RLh41YPspGc{>3s2BGul z-e)A4&;tX*zW0olNBEMuwbpuJ?+k8j6QhK!+0#YqUy$8Yn+IJEKlD6|SI_j=>A~cL z{-lD7yKx(gR=Ps7mP4Vyb?$UYvM=B9VQT~Rt&;jKZPsO*@%7HI?~|^ZS(DH-d@afS z3T2BG6ea`LQ654<9c~~jJS}3Z&hXOvbW>A>wM(DXArTpCBj4MEWEWF+D37eoknHNc zv=-8h&aJ3IHe!AvG0zd)=u!^#oUzG010Q#W@lmq8_fx~VnaB6KhQv}XFG441N&fFY0M`<{@ zbl_W5>NIjf&?HMv6sU;v1gat6elI*E%)?fx8HV1c0xN}v<(1XcO>=5gP!m9zxtEnY z1mMY%#i^G8ObM>H2nfxTEZIrOw#}U)RmCbq!ybs)Bywr0C*ZH@1<$bbnoMfCvbMoI zu2y)-ynyA*i}xIi4TQ`ITQ8}5s5kx9D{s+f&0t5>sxPDwZnI$mHiw))vx07??pVEM z8ZJ2wV8MhlA9+&N<~JZz=1vVk-F;Yg-$J)@G3V6)lG6iJqKgV-bhp9h3Qc*XR+H5*}gd&t*0TM6TNwAkdr$88#K z1wEPC!tJHLQIBEc(zn(#He{uNX&KhIvex=S^Wa#MVND*WneU7mwGH!~>XNTnOvxer zQ~hAKgO+f5M)N%8VhCn=u&UxS%Y-H45 zqr<~itq9JH=E2z{Zk=RM>OvsqMJI)=X(XVY-^k!*S>M`*i-w52;6bV!h%X-EHd_85 z-)k-~nxBw>H7K0C+^t%>Xi}nIm+X>(n#HA2BiwLpsk-JXqLW1h7%o^_tzA?gHa1+d zsN86NFk6a6fV5a6Uk=AnTdu|lC~KRRRlKZ*Q*g>Bd7?j{R?!;~KWo)0^r-4t`S-N^`@Q`8 z5B@DzkIDaDT;DtX+J+G3NNfn-4%Cz`qH$lQ`iC24F-~LD78V;u+tKsU#>Q?olhbL| zlyWswVVG@o*IZYMh%EFa^>4V2%I^$l991jwIkDSPKRb$rS=pQI%-n2)`m*(9xPlDJ z?e(FTFzxfpn#1N#y*osI)Arb6XE#ktcCr(=KEjc%Hu@j69Kw)D()7UT)mrPrVlcN^ zr9PD^-&e*S0_oE&cXoD4YI<5s-xws}=-w5~Oe(|y4!B0EMqMh)xM50Bt@W}T=6Hq= zC?uBMR|XfsjCrjUl{fI^0!~wOyMB;CH7}JBgip5OOf3-6u$4JC7GGE2yIEYTO-#8D zHK!?vDt02OD3=*+xn{A9Y*7Wx`MU@aq;b}to+4C>>Jfev!zAuWoz{p6l&;9_KSvD2 zt)cboXJ4qjgT!#Z8%d~f-z9KGzKKpF879!=6Rn8Rg51#&Ur_2aTK+-En$WBGF_!+B z2s6H*64Oy(b=31Ro8$An=p`Qq-K#(7**(Lnp3s5G)lzC+#F~$|Vqw`WYI=7%Z5IGL1QI)zPgMsK&sVb0t}ztCF`plYAtO+S5^W!pnhja*l50r z++Ab%7h$f@6&JaYowQ-dyfWKBxw6M@;1h&$YG5q=2@#>HPlGjg78&>cH$85(JT7fc zPFtxN{bcRVi1o6zW)E2#)SOSaP}LLKDI%xNnZ}%*?wQ8oHXo)5aSW)aLG8|I3PN!)(P?>oG);jP;W7rC~BT45n2rwsDBI58QYM&7RSgt=FfRMU>lpgcY<5;14 zgD8UU7yK5n7Cj|G)dEmT`{!OpdO4WaXcnDOz*;Ym0k{kxJyvj;ClPY1zkMe2Z88#< zHWX$TbC_X`2rp)-Ut~WJX%%^>=Z!5J7oV^0(eaqGMXU*xX2SrncTd*#$f8JuT9uFK zf{DdwRPr~PaR50nrzMP)Eh<$6c%Mgaz{-1h*KH74%UXU;&FHoW$r%tNxzievpUhas z-ST{9V{YO*o$Z;ni12gW~4@?Hs8y%z`X3d)oQ@u1Nc7iprtX<_lm1i`|;?9VU zL(DK{k+zKPP0&(Iw`&lC)nx*LZfKYY6fN}>5>eF@>Dkwk1QaI>TE+a(dR_2L9Ei)3 zb{BMJx?qPO2?W=IA56guetN!|UnM!DfvlI?Xyv6T_J?utB}I<8=QjRbV6gIj#ech} zq)$m#U2Ann7lo}?g8(d-F4mp2kd*2e?C)5k)eg|LRf{p;W_8(iqMFtTM(M*m+;hu? zxin=Dr!Zu&)qs97^Ghzj5R4=5TUc$I)uqSF|8@~3CRph!CBQ)3g65%IoufYn4NY3) z+CYo0A{Rd=Z)q3%B`Vld!&+r6Ab!Vi;*01V>Lx(4Z0H$frCXpc zMhY4(m1Txy5Vh&+k89QnxD5Vy8vOA)83*Iub7U@PILT?h>q7>t=-OTC1~xPh!Y)Aw z&*dP5y>lK08SDyLi~fP&b|O`Lt``?zr+Q>u@J0p~bOe0sjQgLYcI4}@alvlm{+~)j z!4x+#%dKX6%4>@D=s2`T$2}5X;B-V!k2KusR8M{k$}CtP9Uf}=lojw0uV%w!NV9gK z92)7}Sgka4Pk}!c%yeA@!Lv5QpEeZ)KEok5PUlGnnyf;4l=&)e5Y#u8RXT*)&2;LW z2uuZ7Al*T8CkB0s_8RxdF~hEhSeojz;@o-RMkb3~b{2Bk`I)l|Rd1t%F|Cgw@oTPd z8qM9(#D*U^)q8pe=!_J+Y&5@3B(#!YHq3LX^?acJ`UwrT$@;4L3J6Vze#f5vjnTqA zA&18%=}13VJtAp=-!PHpcME0uRVEHH8u&x#l`2iEleXVu_28G8}L7L<-u> z{q=#*Gz zbWotVA0~kPqb_Zn$tePtHa^KvJiTV2Y-MJK>=Bxs{X?HYJFNe}q_7*1?VWZvsykWA zYUn?u*^ibr(k-h_VXdt`iGT6=en+f2V+m1NW5hi1C8b7eklD1PUTeAYYmUJhvS75v zq@%#3vkUi3D9bZij~fV}{(;mY2~@G#%+}I%8nIOA_@FhlI)L8QxbHkk=bKE9N0Q^Th#NM3+&oU`{E0V9I^4)S_}T4JtV%s>tN>p5`$!BzQ+CDYzk?YaSxm} zWI%Q|c$zo6Ut)LACk3-_X^u7jSvXv5e15t04#RHFX?4O{EFEF7eZHi8#D*-@^q=v2( z{6&Uk&AJHoP1YQF)z)t^21_z$>P_o5*<+(}+<2irT~Q~b@?QtY%IlRw;MB$BTp;wF z9Nte1G?SOQIlt&O2Ing|kAq%n8z0RCYb2xfG2_>JB)(I}2T1PvlMb?HwCBn=Y}nTy z5%vzTKg~j99w%Kj85r?)cUOp#ncaPVeS9=24M_>>pUA_&n&fII$@i?yix#gg)jhOT zJ^M>ZqV>KVvRbkll`KTD_0O@{ql@;1s?tH(D}yJzph@}239|X}(g{rU_IonOx9Hv2 zueR`JT7IvYXfA8hMb4`;6VH~d)ZvMy-eOGaI=`M2f-WQ0Jn}|`#NiCoyDY|pz(5bB z8g&3$qInuB=SQteFj(Nkg1Y`gDRs9-v5}f3Gor_J*n9>v-=QYz^yVxxai6Xv(lX-1 zvJfI=rkd}PKG-RlnO`sHil*kH`1k7E1IQh(_mjk~H;a~S6&t}5Wb=Pn?R!(&*sLML zGGWH&FSY8NmOE7(NlxMQqEhQ}r!^>DGV&=|xpbZMczJ|*=g*cGVgA)cEWPn=Rxh(S zAy8vunl8{5Wv9j$L;0716?!eoke$LrenHu6$%S@J?&M8fNuSB~pe`m5N!&t{WEmj4 z30wW-e9S@6lRcwm^=;i^)L_r`h&2EqMj-BWtdg<=D+RR$tUO2!zRFNrnc zw)1JGoC!!pf-H2bHE<>%2*7>l46F=U2vEB@&m2LKx(`Ghv6LP<&4KO^zGf-}Cl)q! z6qNQ?#-T5?d^O^q@=UkIzLa7gOB9I z#TVv5A{?h$a^lQ}tMi;{j>HtK)lFbDt_Wr**{y0AHBp!4WXJ;X=fv4S&gcUYSSekp zxwcOAR=xzcsB`$90=++(!EVo%HPRH}*x)#z(PUmx=dvz+{P?oBnY6Z%UYH<8dNexf zq6?N_e{2)lTh-k#W32j&=p%rw#8t>Y*iM$KzsdhzSx2v28@m~1FGdnQY?+8T)Dl2w z>l2%NXKR7pTq9FYmm*t@f+Y9tTzsPT3&VngZ%_`hVX)no=z!4ep1$|L@hMHm8aE{>vpA0vR%vMXe!j?frvtZK#>Kfa$A_4aO>Y;6l$p~^@@ zeNi~R&KW8Ar&`w+%~HVzWo#kl9*ZwC8#trnY?KK$Lp?^Kuysu(2ZqB9&N8#0N6pvy zuo;q_wV~EY;^o2@6?CdFZfoH91_jbDODTX`^7m9h;JLi?24Bo!-1j5B4v~TfmN6bV z%I_ROxH=6=UvP16dm;Zg9Ih=1sy-E>@07)LReMzTE}GRa6^d+wJ;jErcl)C38Mav> zGSifu3qIwdS`5u@dR60)6VmI=_=X|AGZ+39LoU0+dxEN7;@})I$=h17npW&J4^1yf$}i{& ze+unV>F-{b+9K$t<|jz*OwBXZeosE@YG=mtP#Iz)eHXF5#EvW)5$j)YNzjaMO`GYC z#`EDu_nI7fS0N8wt93ll<07E+DmHfuxzQ+6QN}F=`ihkp2dPqKy$7nWi?fg@ICBQyujpn-DYBrJ3iluYG55@KzJgF`pI&FDR(3hb`#;DJ7Zrp zRU`Uovle)wcQ;l4m|*nQrfQIMTlDItYSQDpuc?}2FKy8anyMd==b25_u<&hBZ&UTN z`8)?VRsT0n>y4OyHLx71b5;Y#kvemAi#*C#FO$a!tDEIh8)1 zQ)S0j`#;iiB#fyD$FU1l-3Tpo->QzLsO8o`^%xO3Q$w#k*2$ZVw-3BB7>@ zmb!jR3@gcUqbh;has8HKnWk0pphWw&MMqhk@%Jw@tJau1I;mm#oAX0wi{NrG^%WivX z8eK0be)0nU+dp>iS(6uYvWePfAIoj~yGn5cCzi))@Cm{p6Im#15Z%A=ucG!&mkgGk z5=MpI;y}HQPnQ%(ZHPIFQD1Q7wZB}N_k>iX=+~LVqW_9n1QszL02{F0}}?`iB$8OXb38XPMq=kUF_Ymu_2TH14}P`gP;JQL)PAHL+4D zwXk|BsEyC50eG=!-a{icY*#5yx>HK;x1r!_T0n)F)}Ki6(ThkFb5s7f^9Mw0=#0~K zTXjp9^*3JZRCkT-Yi7D5w5i_MOoj|}4v!1;u2Dou<&mSEFkk1KaU3ZHPlxw*M|eBaY=6I?)_A&;9f3paVPE^) zI-5bdSqprNR|C8XTK&Nn7O>0G>YjH25X_{@!NYjvTO|Ex2mt0)b`#ND^-_JndebE z&@d|W4sTsQq<5wyojsy$74YZ>ve4CDkX|5uFfTUL3i%oF#E25AlyLRuf7Qm+4?ri- zBa7H5H>uy1(YkIHBxks4t5qYgiI%S_O4vG>x{xZ7bgM{vYR<#hB)^uu+nQ+Y_GmndTkoHB=O0pF^g_Y~IK0YFD5FI#M z7*2Fx!l@EMLnz_&`t!#{&$ekrc+8*c%sqk;M{7X< zDyB{~3dv5@M%p+roVY+1y;72{;G5JRrD+rNOi-^9mr7FPCJjV$oBr>wtxBYVf}y?I zYnqr&sbz(N_yyG-LL0I(LL2T&VVl&c<_gf{5~wb)9#}#OzCjn%*Q55{A#}5^_+yhH zbgADlFBxjF-S?|&=jDERc~QSu#Sh5K#gfx1UQ8I^ z)m;JJH+Mb9%K0DYm*uw6z*gxyPA);fyhqGob3$la;m0%lY4)^;W&*yD1Du?Ja5D?K z-jr!)`68BU=U8gNA;EA}Pq=C)CtZb-*|F8!fl_87jqmx+SSpkdUl7joF@oicl``Wg zeZ2fSs26PKqMwJW+9HW6n*i#i!wVi3;fO1$%jlN@ZXsZR8yfGft?DFKc*jO^)%v!A z;eG3)|EW(SV_mM8hN+*MA~9hIx2nIv7XYi=h6M#3Nif71(-1(cn+`^z9R5oxN z(=g9sPP&i@%89|6ycm44>!($+XyNauNFqQLm@qkiu;RmN^s+t=lIZ{C~9YoD*H+Sqj$J{WpS%)%>Pz?7x!-9|f>XL+%a%;H;Q7AHsSlahR!u%a2r zv;;}l5uZK$Wv`v$X<%&i3G_GFE|zU2K03g|p?2DFoo#p)&9?OrcahHM$VGq+xMxv^faGdD%+AAU(*MTB8hxLtyDOYnCO)QUg`(*jsy^} zc!j=L&%1T+QwZdq%Et{tXMRGzY?xS{Z|{x}V(53juS3Z_zl0T3x)qFI4sT-v)@#4(PNo6lAR}DLC*!%inbha#nZA4!neld9 z4z0-tPG0O^eB!uDMZ31m%n>`9)>+UR>~Jl6V!m|AnERO?jGMoDfgI*fEO2j_;K`Gb zTL5Z&!ydei5MB1*b)z9mm%K(MJ#zZIUay#eId1PG+3^x77NU|bs;~6K@awbz?w(Og zT<4Y7sic!ruNf(EW)uXLUGHh)M=trtGS2}pT+=7r8Yj7CoB<+`BWx|Z zVfzB?Y+rIBlq0Jy_4ie_8yL)aY~L#?-kxy`gWm`37-lvR;#_5xP@?c_*oh=V6bB+t zm)uT8Bi4VrS=}P_@?#nRt(q>mhIm=2=VU#_M8p7*1X(XJq5u_*mS{f8HBbUxVFOQ< ztX7L#>Yiw^A=_`bg+WF}JzCH&^`!|lq7I0rOTuKV+GzFQFBch!KsVDR9};8=cu+DG z#Sf%4L){6tU|P2CnySs}`ag?g|Isq(Eu1=(Yc!l29%l`)*RuqRxJ4Ujrr^CTlat6-*C?f|y)Rd2?3U%0F@ykk?X$UgfD;vhll z0nydfXY?X;jW?sL7euP@KQsHrvOWS7nstHjH<~=qdrwBZIr=`$WA@iBptfYio5$cq z5AkN6`X`J{x}+OZTae&ti3UVD7WDrj%3g!Ha;$v_u`Ed40Ze`k`;!>VCn-89Fyne7ZnP6rW)~ z&a@wuJcMqM_3X{6qk-&4oP^=w3AdHa53I-wwz39LG)7N|?{h{Y@qPWG$He!!qvPZI z21dul_Zd-lGLV9Hn71h2m2WgZO|9ZxSj>pQalC7QZGs%{>Sr{INDyIJG+LGJ0>e1e zc#yTI83kzyC&N6Gt%7>vh%s=DD9&g*Jy86|$BAx#Uo2x%18QbCkuM=y8NAe=&E$M{Xv`knfYd0QDxHgrFExz7NfH3Bj5}Qd{RtF__%HrsStG?e zLI@}Z>O%wS6UZut=Uj;rvT)%ka#~*xQ@6R}WfLwo$Hw{?7HHrF&j6ZvVBoxQ0pqvt z%)#eQ2okbcgO=r38IIrNCV@M4soDsF#1{#%^Z9wTjU8h#K2>JHrJb z>4?ASSKSfzBKNgNN0{+?(IvEbQYmumESJH?DBiA$RK3t{&0<<3Ro1=}eoQAxElyMu zs09lqBd4|MJTTZCRU-;S=B$Y38%KnST~leIxY_Cqixk z`z!Sc6BL9nKVf=O_e(9@$teYDKVxp!<{fmEkWk-W7a95PEK<_<{k3SlNhU^j+5wj& zzMpbegx=e7*HtEerJdgpF~$J(*LXr!oy@4AH|VDxl{rUE#RqO~J2@+^B|>#(qE@gx^jb(E zRq<+WUF5JUU35|+ zJgEq8iRH3Q*zv7W>*S>I`Yk!Fd}gBc1!Clt;2OOh*FyOLBmTJ9Upv%EOfM?8je1du zX_Q)9s(xZ)Rb;}9d+wjm$j6HJ2rm?n8l+)wQV&QUL-}2`HrsYT?NPL!pMWiS6*=so zt(Gs@*6vP(KGc-l8VGP?27+%8BPr7g&r)aiz06SMeJ}NDc;Cw{YEa*csq*?>>N39K zG_?8ZBRyF1l4z9XztQ*JMt`Ud-es&Z-9fk09WTi?maN@PWDkAxp^lWjO)#MDBR#jX zB(~NHCZOrrOgBbPQ@=e&nsdL7@D+<9j-!7`SL3PnOd1%!3zJHhS~!+uiO1}GZ`;mS z-l>wTeUg%lT)z10GTXbzsp^3;Ng*Px=m@L$9pVwpwCKo9>UJ4NxXmw^aP|N(n7Xc_ zR4O_;5Jll%R+8Bs$UW5On-hfwQ$OTe3Nza`)Q3dKZeSIkYS(xN5y%=+5WHK} zYW8Q6`verU#_y;f4$#FBmZor##`Urqs_+Bhce4HR&WU!v3|Dt+fVy80JK)4+e(H9s z_yxY}>Nn^LL>qEyewkBh{tHQAiwvdaKGIQ@XZV&n2O+6CRcDl%&mtu$hUtPfNzFGY zhh6g~lG`>$tO<29rYlG4_q}YiR`DqUrnleMj*^_;@Arai#eYwNBlY{w3Iy0Y*#GDB z`yvuk`!D%s_xo*jZfgHmQd0Y?ba|P69}gW#^0fkrem^rVb@^VGA2xri;+q8&&HqxS z&T!?>AaxV$p1(_ae!SF7CQjE0nl`&Jr{~4S$13(7TK9I+QJHh3?&r#!c}8cHp5JqV zuKQy|^v;=k_v^a%C%O59_hder1-tBtayVg@gV2OP1DB4b;i?*N3D+-x&2-UzJ4=)) z3yAI(2d8k3j#O7h_>h4i`K{ual1Kac`PK_XP&|$ItVAfMFoM0ZhGfb0h|@%XZ@ce# z$@T&CD*0K-gVV0rSdFLG#t#gdy#t+@lwV2tn5JkvYc_DYqNoupa;!+@l0}_RuS=UW zZ<||UW?hUZ1Df_3l!67x^%>Mao^-K`&xiU9>V>~r1Z+0^Gi~_UHb9P6qUCdruBT$A zWqNC_a4*CWl}IbD^RnJaL8 zFBuPuc-6FyY)j5QFj77*g6N>);V01G>3bV=6JgX zVnpGmF{8mmdC(A2~PkC?3P-8sB%@3#2vBICg|$QU1NZ=GCj zCN3={+|tHW5v@FaNGemm5%wWw~ zxHvw%^Veq5+Z+0plaJ8YBXs`uOy>t%>3ivK?C>{Yfs6SPP1gf~7pX0JBvh%iUXKAT zhdy7R$Do`gq@L!V<>+eG!0ci0=L|!6rfMRP9DAgS9bpuF*-_Ay%1nkzYl{pD%R)1B ztzenVXxw)AXw=DQAf}RI`skT&VTHie(b|>Z=l!uE3sqLdu%O%bmN(U7mC+olXQN68t( zP7Mnys~~mHAs~K{0U@%c91w9Gy=Zqhz8Ru2rZCWrSbI23SQn|-E7vu(A1nx`H<-SI z(NPf#43D?Y_E7ctZc$gR0WEVJc)RJ_5c{X(vDMUap+udwAr(q3Him63B&AAg4&v1i z_Ui23TD%wgJYan-1mX$ASsS{N)M!$&2qE@fGf|5T>id!_Vpep(T$r_1zc9MpaJplH zHXq!>5lCLb6|dH!g)W#@Cui;cz^+Qc*TNH-upBd{B+4pl3qBHMX~efD_B>b4CayvM z0d{k~pd`x!P#&Hq)o(dXwW38j2|oll2yvtcW9XESuNX zCgy4!8RPKF3jUnAoa|sDo&c-($jkwoLOm&-yiJsK$ zk&1K8tbxW7nLK#)op(HGCnryX)P9Ylc8#NNcaWh`62>!p$i387lcISUSzJf8>q@9>fUAMm4=<)3!u`KQk-G>sSAaEiTlHj;>~ z>Ob|uWqdkf?**6T2O4|Mi{V*$B07t0(UMwAOpjz^-m1Q)z^>l{J;&R{k57e{bF zQDcAy;*5jw!pv-UxMFOX$%K*i#h!s1U(S;oyka2LSSP=2!PeMLB_1YN*FW!V^-${m zl$gAQmghA39JQZWMx%#)?{lWjL%#Q+x5->`MfpXlfq53OVhm?Xn;e8RLN7JeoDmzj zERe@>bEA6N&ggl9>~6*?K2>@C88KJ&#raW1!&!d9-Fyj3!<8b{h*pPiy()>%Be5KA zsAf-K(!r9Aoh#O?dt{NOp-99JWEg?F8+X!X`~Cu`7DNjbuS2DX6O;?cA4*>2R{t$q zrR4uGCKOjr*S^77Fw9IU8W-(!*oCeqMyFj#+D7&}M|K$gz!y0#ZY5nA4wCf=vu)Fa1Tgy`~ib_V)MiLO(hzEoSlZJp-eFyVn zHf&=eK%TxjTa!k`KvfU&5Arm^v3$tW?Q@I#)wh;MlTt`(X_m&(z=5e@SG^)?Gyx3N zsh$+TLUBfacL=qaOW#1+^!n7Hxm;+dFOR4fHn{1N$2A^tjXWxg50^83C=Thcz)F{_ zw&b)%>_O+~<;3k%C3f|v;HLvGz)wmquoK=wI_d?R7Le-asXA; zCl81AVcuZg0uS~Cu4uF}O?JdPWi+m_Pe+7sX_~BCj0;iRYT4dH#cim#4-Me3OWcQw z+fcvXb{i`0L&a^Vzg~QXa(M|!@D&>F#!~|h+45p5mN{DUQYXvB(0Q~*wf~kmRQCmk z&urz(pExsUmn_8|T3^>NX+Je)kcxXo+cn04`^MAXw@i>M&e+O7L>uq}qq7vq=V+1RA(TZ6jQ7yF>x;SlwN zp@s>b0X@_SJuj19^XVwnn2$_Puc`(k$43vK^(c7`#*XT6?MGJ08$9&7S;&G);s?Hq z4e4-w&>Q_9L}NR3O#HyNF|-i$Ux#ab=BqWMh*^kt5@sRbShK822D`O+;22rmP5Y&V3qP zOF^x9c=1iQ`_|6>2HzeSDO^@|xh#Kg3NO7Y#^)I;I#UCEo8}a42o}xqrrQI)&bi%U zqp>WXZS@^%qEpuJERaeiM@u)!jJ0hdlP$Aoj?j97`c|FF7~Tl#kfVCD$R%g-tcRuN zFoi`Yi@IW#$g}jEC*W1%A$JeS!Yq{zIy6_yEE;iW#sOr6?wJ*-=!hhf@;y=kUG$LD z2ikAgo_sEFY;=19%OZ}CQiJ#8=gX^?*SwFgjo-*lM;>V-7NZ#Z<;Aw7E|k%qI!t#N zrt6P!?!+7K88*+|H~^>0-V{Ox`wcc=w;0&BHVF{8F8X%I?9E^_Jv?BvWl0;O$@m?L z(RyVIvyjl({=#j;k*CD3Izuik2rk1&1mRtnZP>@VwMnfL(U+BcoC3ukht>cFe150a zULGWNRXGV&i?IB*VBLW3z7`IMHQn{DG}f{&FK2N4s}Y?ulnBeTIZ`k=X`x`7|F7PZ z%~{+fESM34Zo!J$re{S4k{rnvB#U2WGZFAY?T96d|Nlif+6?U9lw)l@0Tsp$HG%Gc zF}1qk3yxKPhXJ7m8KgH6WvkWE)j=nqijBF{&$Q`t=5lvU1z83_mzhZ$4lAKjR!A8UPszB*($J*6*4@eK zL;wczBEF1Yx$1$G5|uHWx}C#<(<<(V(;%HAjn zWTO~Lm%KHAE&<_^Las*mNW7U~v6@(FXQjn#2O8Vikn_ch2XGiEP=uVggX8dvR21X7 zUv=85aCU$DPh`W5tKA{e{sZL3ER@MVldBsqFWaUP58tFW~$d23!xwb3k=pTUA} zpj0X``FG7uHKmnC;-~Y$c#dlm+J9i9@Z*SBDF~j6a?UUlT*I9BwII^OwWTu?*B)mk z7qd&PUBzzTkSz%|=6TBR`haXQfxL1BLox$v9f{23l+s=z<+xvh6*$SApIDrQYqVg= zNvcTMN!3@EofJK-`ii{h$;OHRo5Nt=ws}J;)+F!FOAaY-jGSBQEcni>crEs|nDSK* zi=oZF=-5>U$=~Qk`n2Ax*b@83n&NEgdXniMJp%)ns5jolGVpETItxsYM$vy0o0w>v zV{n}#0f-6LXbHfQxP}lg6ZcAcQ~6dZafiEWAR|{Z;sd(HXxT$ag4B8rZ&&aa!a8lJ zr4=6hhqf>y0&*K6V{x26-2Q>Hwg8{|+XL0Zq8C(OnHQ~L_sf?JFz9QWR~AavB$s2JP4GM$Qwd_$KyeI*}SSeXbjOO8j#t z_Zfd)94+>J7o(jcu4}a0Ibw;AD4a!1&pks&uB<<8q5#nJKnyb``~@ zPRbO?Qx{p9DROA0$U%0I(SFA2FZ(lx-;n+^L(Q*D`IqFPRQ zo9LZ{)$xbeE5VAM>!|K7dWLT6r5g_OE#yy^tS1E*yx6Mi$ls*(Q zvnCqiPz?7gU^o?DpFguE=7@bDGP-KBA|=mCi8i-?M;(HQ?W4BX9&r+T%vR=JL@N>o zYN5qdl$iRN2v153DN5APT6_B$NHq111`7}kR^ZvtgiZwOs3xG19@ioND0n5{;0GwlJUqo5$T`C|)z6#ZX z(Yp2=I>Yqh>33OPLOJ|K$BM21^LgKaIfKoLUC4T>X$cyET_TOKD_1FE;M}cC#)l6K8x$waXJ2pw$Ly?(gXYQBeFxiC$o}LyBcBZgiiP_(#QQOMr}>kE?)4JY&5STVEb?0;{}Rrse`x`(P-JvhpwgktF@lpQ;*z7Un`yU z;9Srk?G|N#(eez5(k0uk2dCo2Ih3674C5-4d$QzF8->PrS)E|V zX1zPVoGy9#hAhljJwgW>I_-L7XG@#yn#HwuXQO2b*nq|cK(jP-m7mgSXGJGxayX(h zdFczfI|RWUI{1(Vy|R8t2K+;x{Y`xidFEW3qp^f`sXk01u^6F<`w|0=8`* z`_?gSf0vVe9qp(m#9B%FiT;TGSUH-5<2pk73;^VxPz!4}(~lDX-fwuQP7%TYuLO9R z^Z7!5u=t-4AZ$oX4AFt7M|aU8G(MyEWuEUJ^0en(Rvu{kXr4|9+DG$6Qlf|lOU5E| zTqR~@N_;d+$;VRiTd$0%yWsJAaMVm&gnk%J;i-X{m#69_x0dtungty`*I9#g7(UB^ z^e<`azfgQ*3QgzjkP!B1JXU$6+}EMOB6phKyK05WHsx= zA`oPU6Y*zrStB2HwsgsV)!WUiKSaYLJ|CoSH3<#lpDaE>x9v?Db&NEMBP=pW3J9zH zNHnBRkD*~>c3yU3zjr?t{Lj>U72qir@FunT1iKiJ?h%f12$~dv=(vN@ZR55K$#bka zo)W$?{~)?i&*A5zY}*HJ6g-oYO6jA8fJKkFkzHKuW$?FMaV>!GvZ0k}NmmqrCO2;G z)LX4Kzc^%r7J55f@}Jj{siloA`uq~euD&apu>l`lP82)!k5_9O z9BDdg@Mzf$MzgK=3?cv&N?DA1&4yAlIm`JEhnH}{dl1TzYPN=RMnV0K+g3{1Q^#%O zfpJ=W!1~OL`_c#}<0v7u@4oZ*^mF5Kz#5n?IY3eL{Hdn3p3!(nn#qJ{-`5Wm!)1h> z+!fU(LaIb?d-<|{t-TLmB)MXWBmoVKXq2TryzEkgx#E#l_*Ye?kZ>dWe0QpS`=>d)d2Q|SfYyU8*6OqsEwn9upM=b!d+qB`FaHi!*<9;3-FU!>jw$Efu{|687& z#Pv3wS3!uj;ZJtY)N2a^x8%5YVV6y-$R|^{Q0*E?tK(%pd-66Na6r({=5Pfyv!`!n zZ3?lGJ%1%Ut~gVUF<A(Lr@M zJ2F8?CGAfk2jmMfumaMzEB4MIEfad8_%+sA&OJ(JFl7YD@zl$^eR_(u%R4Cv07q6LCad*%A9 zv3=0r+$E&`!1yl5I0!shAVO&-!If5ch}GV%ip%n%Q>^x~g7foZu08FLkjCow2hMh; z^Tl>?f~OI`Ay}qMHr()US0o=Ps1QT-Z$MWlL4|yp;KrE)?AL_SxSRGGDnyC)@MVy3 zU){v_0NAizSFfAeb-iKt;PtY^8(f$0h$P4A-Jx3U&1lvF4~=3_dRRkcpiZ#N-s+d+ zR)3JLzB>&@SnEzbZ(5gw^~G^)Kdm#-j-O;-GG)(0b%O^m;Ul~fhsrA>M=F=Ze&PBn z&7{2*s$a9*QvXHU9-+QDCzk3tSat2%)<~HrMSGt*gVk{r}P`yeijf*@EE95moSlP#O?X+`fV4=0$8z1I8wC9kW7y7Xe;f5#hoE!ST}Nci?Yki!Tv?ysfmDVMLqh;*0@ zo!ZDT;ku7R>iO3}+;C{Fd*yAk%$u4KcN31CX(t+F1uhvoZsb6Msojc@<_7aEoAioN%>G4*QeX)TSOtpqMu=1 zf;J|Unz$=AeO1zJ|Nl(cN;G7fGQxdcm;}WA<*|mF$jejW2JLtuZ;`9ExK7_ung%k6tyCt_q3r?PQMER3V^{KHj7X zW!se*!`}BgTh(Xe7j#Djp_C|&bFP~=>n1F94F*Cl&6z#(34nip=JQg)6?UDw)3~y* zns$?1mQ;b(MKpCE`l=3n70q66;$)yYu#!bVyGkCGcGc8%i-nU%Br<>lLCsfM=UfMU zmyK$}8!g&*c-e#(XkB;l?|bqgdvD}IIG~Eg>n|JF13J8B{L{SH zUzk%i6?dpmuIoxtLPwJowTbJQJ17w)l}Ar3bbL)$Q>S#>+Lg?gCEEWD!+{ALp}XRGG*kZ8GdQMtf7l!!~&)@jJvbs09*$r!xA-uoKF{3 zIFM3;Mg9m)5MRL2zIW;TpMEKWyE7^bk$Qk+sg5jVkRk0iTdtRw(!Hoe(@?gI7NJF* zpd`!n$c>CQLJ67seqHd=(86^2q8;ZC8g2ED0-~UtF@?rFS zGrm9HxaT3_%*3N*ra(>nvCOZZ&z4o|=L=<(JR^x0%F5-LygQAPw1bg?eL{qxnaQ~? zQe?MSRId;3MSwp8=Kp}(So%vkx7PRF+}Pyg*fYWw!s~-!-`;tr8YAWSa^k&fA9^nK zQxQvATTwu!OQuf|lEXBn?g|%>GClWA+(2M>9ER&H3s>yQDNk#~ijHn|9^J~_x|if> zH1;vJ%9Jy!^5^Ya$=H|Ug$*~o=)#2)u zExJ0LwNjnMXX@*kHbi`%Maz2ca|{?O93ld**de#d<_B8wqt~X*t^U;qORAoMwa|ve z)o9Y7T5h=+U2AQrt=MXeY^0ais`CL^G&s=e9Guf4bqFwo|GHvl+Q6!^y|XvUp^}#Y z*608C-@>!r4O{7%%T3=`b1T9H?;%x~b7Eg1_P(bJc`2e>|90vW@%KwoMq%EFBPM+; zlhm|64_6$-^nUK)>Fn{Hr_(LjiN<2KeZ|2W(c>d{zor}hP5VtB`j)k(5kl3{-F1xR)K-M8tZFJIG>Z2@>GnE72k977%0BbJ4mN#Il&xhFppZ(uIbp7Q!3p6nN}a z@x!rI^1FhqgH!DDZ`+~g%?cz`+iR`0B4R*X8ym8WWrtPL8oc!UMX$WiQfClnT6?q6 zzmVu?Kh6653;GxRyBr;c&}@qp1Y3_1lDK6y!>Iudx~TQCVOS{F)pcD<9ObL0v9QpN zle)$eBYP5p1=pWGk}lB}4StRgS}!4}geGpEGjBuC>BUPV%tw!RVfV89)JS6mrqCZ3 zAasoH?r*G!4DlL&z`0O+E1|$LvBrKkHIhZwt_{TqY>dC(pDS_-cKoyXGN(5-k`*0V z<_;mW){ci!ddg^AatosoUpIr^XiIgeH*V8hZq9g!FYx5q2bimY_&<^L>3=BKWZM?vZmc{o#(<>5*N z<u95c??Rq`$H3)^aOTGcO6d+l5-v<#DB*z;9+;|SPHQ zOMG!^xP%8ucu>lau>5<|NEH~V72z%H}jR5M(QQmyI8r^g4d0a&D5OHf3a<*aR(p5TzwP5$lTK4uat2kT2&(CinzbX8t@w=K|1HT{g z`w_qS{Fd;$mtT_KgZw_@_Z`2Y)egsSey8#~hu?U9Q}|uUZzjLl{1)?D#_wnRp5XUq zey6dVs`#D9FCa^c-&B5=^Sg%Mjr?xqH=EyG{2KW!<@aNL5AhRg5b2Lz+S0Gp_4*LN z5OwUjUXp6wb7JF?suwEnBT>PBDda#BaznYd3Z55Bv*e{|xd#AVG0h^{Kd+E&ZcorX z%o+O(^3P;tSv%tI=iv;E1V=Pm0ZaE$vnpO123D8?44s+D603z=qbOGJw@Qvn)oksKTq|2K2IoP7+A$$xN zpbQQ{t?UMO^9Q=y-K}mJOQH&D!~P@PHnO8&^Jc^aN2}S$7;I&Kw6MeR?z^NDwFetN z$?A|yh=z2N>K_wBn627Yg9^(S*}hbuxaYhhn;WxmEv~iYLV#qW)XCZru(n`lHUWgy z!$%I#|6lgr1um+p{U6=j6crr2pyYMDqljXfqR5^Z0UZ>QQWMh>L=Y5k;-Y9;=wN~q zNo8f1x2UY_D&5g8f>*RsOe@Sv)ZQATw@?eg8|VA1_3W8}>g{(v=bZmJ=X1`6+0Xv2 zXI-Clf7aS-S70Xvtl$N8bHloc6d2}eHBh|duMH6AU`HwZMgb``WA)~-U)b-$h6sEt zEdj4Prs5Szi@7-2@v#u0@BDM#KpB3Q5qV=q-oznqF4iJCB5&{+;M@d^*zxuwL~qCJ z4PV&6*JLs9JY>)A4K?xsY!_bRZfl6N<>ujg8I)Zfkv0pmixL%yv7T+8$#&!~@+%z+ zS20Oqhu$_`5-sx`JRZ%)1;}6E1Qs7?zN0RV$(3M6Y_L0cRoG zKe!n-gU*Bf6#6_;pp30h`pAyf zcEo-FD{$2ByaY>B+Kf~L=^+#Ypo8=ff;|vl;e(8{^*RvAzyN>>rF|s5BTCzRbOot! zyo=%~kT9$WG}BsLOK{^dhnAw1&!`LuM%GDCJrIMGmw# ziDFCT-97>_h4V2|T&NfWu&M$fLjcgrqEw*28VR#NV(9H3V#Q3n<2%e;Ji}Su?Yq%@ zH6pX>gM_M26GS~0haJ{x?Uk^G&n}#TRm2fh#=-NlzXYBQ!}3^fsv%(uMGY~chL}-9 z;!s2UwB0bMAui)14CO3PkU~=`Hu+)6hF z)Dakz!2l?P_C<((mr{V|QfaQYFULR|t!XO1ViCu$SkN9si(EC&#;W-_@q!btP>{~U zJ^^2V8G8taq;ILjLEu(WCbBLBpKI#(siW<`_idyV$`fHlJ5ym7sWjjQn)YD^Xwh2h z3&i>=#?~ECyX;nMTCVR8iRQMqFNC-CT`*s+T@h^G^$CUHgLJgg3E0o$6&2+9X=(%? zKP#RwjQ5>}AZhw!J4i25?o<&0*g8f>jk3I*d%WbXV2qWq^XyR0^s%R~`*iN)mX_V; z5L&Tke^WWM`O1^nI2E+#Sd%#k&C*w9+aG2ROf0s1Q%z^#HuMnlrQxf*iq}HnT6~vV z*y;QB$(SPF9V%N)DJbVLt?+1!PAa%u1IHUG^1crL5g}ybjcX{F3l3UF;*bu}hC?Il zh$rN&-({vKO>eS)h6xK5Qc+*#n~S$A*u+976k~^V9(vcA)s7{bZD{w*)|oh;sCZ`Q z{3X6kD49hQ4o>q{l#U$_O!MhzEF2@=Fu^A~vMxa*XT~cS)$l8~90#gn2s(1CNjgN0X{oWHN-dzks`e_7LMXj)X)R%9oteo zv%S4>Clpk+(>AZDMubhxL|DU`@yOv@plyntIQevrd;LIr$qh`miagOi@`nv>Xxw&LZ+LRuJz`mlN%1@Wrg*906M$urRb)yDz_Sb(Lx)J zyRZBeJ5(lDAk4;(d)&uGls&FJ{dp|r{G3-ILmc@Yb3X(QL(sDNmwFTDZ8(4q12zkS!FmUs3D-&$`Z@Gz&}aV=iu|@-xdBCU zdxw+ybci56%)jyyL=cB|4pDjkH$(;7GN5e`+l+iz1XU0LMjUA4v11^Pb`8+$Cg?P1 z{{WhEz1Uqq9f8??9g)Bo@mXlUSwL4`7i&Jq7@P)j=KkdT+<8yJ!DAgU+6oEK9R^qDazP^w$EG?`!gMdvP{t!|+k# z;=b;PFy3Rwu3-@tUPzr%xTaZ!wkSI6YrJ+?w01W(Yamae4%!Zytk*nsTPj*AB5k!8 zz0)ZL$og2=n1gQ{$6D<^=nOG;#)9Sw#Kji%1dhi^sxo_AsPljF&Q^>sWN8NIY`}MUIke z1sBt;`rb6zv3(7Tg1<)NCsF#4+2}z+=@x?1%>%ut;1@^fMB<`spW_uoD_$y(ea`*{ z=B1HhkXtZZNM?-?_C}IM@f;mS+fY}3-r^rOj{13B0aoZ0+t)#JKJ=L7jh9R#C+2R{ ztw)8X!vKsObB*G3t`x`+(;UsKV&m^riH*Xe_UzHSaARG&(M`AhZ9KJLcT2*HZb?F#;y2v1XuqK1^$NotM`Ov`pvkY|B z^;B{+IgHVL2yOEL_Y`YWM|_Zanz#E-9CM(g;0wjpz3wSu5g)HA6%1BcD466f#*9)3 z1usDhshE#l6{Xh$YXbCwL5qLnn7_V)iKn%1%rACfA!AVT(-L7IFH zS3BgB0FEdl*ss1P3L|&Xhx#VirF>#N8t~zJaeTK{Ocu*x;t?Bm0G*7^dPIl_hblye z-N+8seW=606e4QwnCr7kiU*p>3Wx`tI(GdQKRCDvOW)8HWZ|e}nvRRf89j!eGor&8 zf-0$znFGgUTftz_jOhX`v*BCZ;(!LcfHLJXj2rAIQqgooNzPbJ)LVoDIwc0ZTqM?9 zJn;VT=aE4NTKrFq7J?4&z|b3I?+f$|K?hL{5%D@YkPh#Ezy?>w2M3dAs3h)-lj0^J zq9mdeE<@Yne7y`~1N8nFZ06ylxdeR?KGipezsUl7C!=h&bVgTFv$f?$ZXeSDQ>dHC znmA6uQ;3`CplKux4eNH}hD|$#4}gY+pFtJP#yJW2PJK&s0+`5*ZS4V>H*MxqG8 zPNFM~ku=D;1FETV?36ZHk6>!z^%5+C*(B71!bYw?ZJs$8AwoJ35$a}58V8jargc%a zeRxR(?h_C!+HE{NPCnTV)Q_Vo7Wg;OmBPlU7ML8-C(i8Go|nTV3~N(qL<>E^V)30w zqinlq4CQj}k@g|0)Ui;uHN_<1H6RR;E<&xQEBKNI1kid<5Md(#Q_-q%cxjWwHTDn* z40A*%QL#RZ2#RjOzE%cbx0QNSWJj_8bN?3NB9#ULy~X zXtLYN4kp{d%jns29-Yz{Sr@9O^Ab%cEyYvZ?a9a&Jfm~3wMStPg>eh~@R|ybDVo?e zj-WiNr-ZJthw6&4ny%op41u(X31{FwLv6UQ-vjo|w6AGmc_{4U>NH0abp^+f5Nyb= zM%UOd-4;_9*y6uZlu=#5n_%JdX1aoP^l;+{KEvjw+ZYBPW)=@t5vl4}8y2E}*IE0* z%$9q?r2DYF?!&4SY*w|ugDI)*!`OxtoP|Cf4OUU^2~i4Mg0kF~O zq1MLYtIgIg5Mce=c%N=0>Oxd?-6z1LI-@BcR)aA-s5aS7*w3S!T*O=Kwmk2;=U|1C zI}8(WN)p6U7rOQ~+Y$P*+j!d0-xs^VbQ=oTH~0e2$fGe=F!p|*LU5W*yb81DTbp?` z!>m|{3Bxz65JaKF7=Y0gozy5!v94+HZwjK^K}Y4^PDbChwg$0`s=lb}LI=``EU})W z=mJ*S^{{*w9?^n~q%UpuY*SN;BF5Fdh;DQVwW|lv|}5Zb{47$s^pUh95rW{h(b@JZA{tKM^#GcUpbwxOLfZV+*M2kX$+=ej6in(yh57!1nvy*f z^IBBj+A|bS=e*Vjvm(zux)0FcS!<2;UG0|;J)Ww?Ju5=8k}gf`?KIN%a0~5NqG5;_ zq}rEIZ(?ooT-nP8T_#?ESCn-qc&I1Sj*;HcBc%u@0PLgFQAECo@s;bRuA#J z#2~i?FEZAk@>iliNT{^qfR!&&Or0t)5Gij#2}&7MMdOlkaso()_z-0WzBSka9bOaeP?l_`069FN{FL2EdfW2_G5(_suC(_{?olK4STqi* z@(sbr1}86Myh_D-h8`wErOAA05(tw%(j;1%gutY$G?^exLSf=9O@>GljDme{92Guy zlP1u-`Tj0V+@wh=Ox&fb-?od;Eih@4cD|P;c`*4^ntUcqY*bWWct#q2APozp;R$K@ zyfiG4h6knLgJdXbl#oSEic=2sVY@EUR*G;s!y6{;rO9k*g5?I^>lnB}R8yo$JWR$) zJ0a2}0VczxNk1}?^%t#@eE$#HGU$V_Dex$C_BcB83cjL;qJ^+d(K?1 zos(3t&>BDyD^zlykQNYL*uJvAL>iRh8KHQ53XjFPUNldgVX;3b7&gci(?t7y5``e& zi|1Sw=lq&&HY1?@UJBl3Gt={O`5Z^jN%UN7OTaTc7y7AU+g6E2v*(1qwhv&>QB8rm zNmK$vL3t9i*-8QJe$xBx^z130%jmfYdKlQDdT#$kJPTDXy*36(+>HW`!5ZF6-h*Au zEfx8by_MXHG%muHni%YTK~Lm?7lqJ|cZ|hx2cX?zi|4x+n|9;8oyg*79JjMOQa9}o zQ1NsVv8ona550BNKu!mMzK2_`Uy9v5MYejFor*_(#j4`5z{0BjBRk|yzF zPsM&?lKD6GQ5{JxtGr;n9^=dzw1pP$2HwAoaxw|4>WJ90Xk;-;ld>jFK9u;5cLUqL z|6>jR^amvAFJJz7>5%KoKlHNC6X??EB_3N7eKabzg4euSe>CcWhyUU=o_mDhU%$o! z3Cas&gZLEP{WxpE8a{)frQ1-4x|E^|tii7q8%&OmOEK&2!|su*eUGja9D9mxcJQO( zd33N1R9CP;x~aaT`{`1OZtj0lbffp{9vCf5Px*fcI~R2eh8Wh9`_Z=?1l1MPxDlE? zrW$o7(b50W*2`C;a9!yRdQ48 zoAam$;_yY2Zk`YJKLsAc51)be4TJ9(jP{LyeNf*{g3kN)qHf+{{IDA+u=b)ZzHc{S z9`Ab!{=pn(!cGA?zx@X$DBI`UhzT~$5%!xx3R@MHKr-t-L+Tmu%H{?d>P^on9udDYTLcU$7_w5UcLi{)e zBKt2bIJ~bi8>Jft$>j3VyvqGo{raYn-K&1VV?g(vz3^&ZGd~2MMyL>2tuP%Ztjz;B zwzF#A*)uyohX=kfBKm&81)%%R9(vtbhPz@!1jpU??5~gi3ap)8al&r=p8d^^d@e!2 z{_(7OHeH5ft@o94@P>R^0@>LqNzif z&n8zx;i}$~_VKxdggpxb(c9pAmDU01fvZ}$Ya5|p@L`-Km&%-ngx&cum2POs&~4MM zQ8XWet6WTDV`=-$8 ztS;hceLopJgizM7O>iV#!y+?&UUO522yynVIMvVpY=^l*caJ-D0tI|lPRX6 zDW19y_7ZJpD(YJ2fm9c-R{~pF!dno#6_GeZQ|Y2)PbLRYo9gr7G3a0g`9^9~1+%2E z*#!Yvy@XIqMLjNm8;$M5w5#q5`#4Ck9-TS%g&nq!29TQyq4hixMuQ4`eSlBVx=WlJ zs?D#xX6_R&kXoB={^dRxo9`cc$xie2l{m3u9XX=`o@0z9&wZk;3`Vmxp$599rdV5*?V>ov zu}-W}`@Rbwa2{|kNVo>0>ZL9I%{P%!q+RYp=sq5E@qd8upES>i_JjAr(qc%=BtE0q zCI!Ftrl>3)tj7T1C`QFd%?$X2b7IW4ukhU{gg|fZQ6x|r#T!CG@nZ=&8$X*E*CCwl@*z2k6)YNz8N{2zQBG=Nt{Q)CI zv5P~SeDRPvJ|?LW>A}gJH*y2$^D~k7{)~(Bx?=bNElfgWm{;-%sA1+t+o0_9pNA-* zjMCOy=i2%np;XQpN6Q)s;y4=0={)i3Jw1tI9rMK4ooYN@aKkyM_A6AN=~D1x z4^afjeISPCln6Rv9ZG)t4x1d6(~tzEgQty-b1jmZv9$}D>T0A`Okh06o~pkSTCCSG zTNNL1v9`G7b}$x{SHaU<->~LfePez;p51bK+Mj}}ooEZteAU>C$&0YPIiJG2b@WWL z9Ul}N6r0yt{P*-EL4)_|_r#<5%`}AZ=lRV6oS}k`8s+F{_tBpw(HR*ki7NwrJv9quH5#B&PVM;_ZcbnnZ!BI|8>6GLHd)6 z)^q)B&5f1bIo+YNU8NI3vd@5r)^nHZ=Ze?Yj0Me@?dx{^)FHd0oxY`LJ$JQ!va#`? zTTTx$?wGBU+QaUGccWepo;%(M1g}$eHvm6;chV2(vR_1`tmm!^qwd7_$@bl;Cr*vv z{6_UBbZmwE>~sj0)lrwEJ^_0%W{D#yic=5qHVoZx3*_Q;;e8}uY`^#oXD`_<2$Ai~ z?~jTkPS&u~JKeU}g;XxhRIZGWF3AN-K{KR?NBfx@Lb08@64EnT*WT}*+r<{O+gf`L z{M;SZDvzS5-B?IoC|C915t=->#(Ma2%$rI0aZ=^zgF%}8BW$w5M)+~q*@~_Yp+f5| z!?Ya*y=%E`&JxitiqHdb$6(z42L_<^eNgZeFDj@pgUuo(b`zi$AML1>ATPR18)wI8j}pegohgpDYinL~<$7m7Tw<~?GV6wi z5QUv+Ez#hk1F$2?XgINrhT3rJFR92XyDsp8$#x85dlS9FGXm|BTW(rpk+DWM7gK^R zan(4W9Mo8K&{#15?hhKpfgAO~=+Q{sL*KL!0t+>X3Xf-WLDbz1B-;qqlR|Y?l$^nN zF_3tuR>)z@!*ycGHHof6m{2fD zOlB8dh9v-Nih45^6z9mVd?GV&aOH)uU+(r*l!~I;Jh6)5R}|+hiI1tV!jE2Ujq=;Q z8GNhR(+x)MY=l`U8M(vAjg20bMxHS8WFurA+46>wHyf2mBR@>}@$i0lXXccoT^7vzmQ88~6A*fP3Q}y#u(}Ja#Gx@M%ybPVWs@^ce9D z1vP>~D_5C|`Ux+*J?e*v2mK9+-RpZw3kJi`IAmf}2u}2Y3BH0$#A;5!!AVhm1L6{h*g6-P zM2~+twxUre^ra=|fD)%CjaEx6(WDfDgUAccN!}Wsl5#Y;M+C zkDL&yzy0afnd)$$d0i)xF?JrqjxTOOouR41b7y+?h3M7|YIzR5BS{5e#uc?AahlL% zVq1;l1Z3^l67k*x8V&4;bj$HCHr+rVD+BC}CA9cMRlUBGy-omXB~X2!3gk5^d-yd- zuEu)KV2`CI8g9xn-l*M2%|biNFIv;aX?tg~)PPDEYWwW+v2};;&u?iNR5j>or=zI% zA^G7MI}t+8kj6@5j^2KO)+YxX@QNoxG|wT{GrR5I!I1i_dT&BU2({M3q4yGPGBWX` z{aNr4xlVgAl%$x|{R;+S3~a|o;#+8`Una-VI>jWJCSwp6<57v%|DYZ=c8}717>^w( z6#>y66?nNUr=Km(CnoI6td2Ibx5wd_uwz-BO?aWyS`io(_F2|hXqO$H;X#b6SOi!q z3xA_C4RVg!$01G7z7C}GK5fUzWIYNYE!pU7#=({L{`5&aH?gEC1~-Vk*uPvJdnYDi z6WwyL+151K>`}QcG%gAQur510p#rB&Gza9i6DwlSgi}OV7R6ro7E*CMiBBgq?!m!t zy8;ja`vbVqt|*ynR0d*grciA8#L>leG=DA!LJGg4Yys3dTF*7b*bbQ*6;F((bGn&u zc1Ubeoag157<>;bsVZjBvFN0|({0^Rb0Yj8CJ^t&NH-5%PA9A`x_RIwB5ZVw8H->J z&96H&3VBj@4xu>&*Q$TO;q~1I$I#e5mUg?(9vl!8R2{3Eay&W4)@O(x+{K0+&+d_8 zT6b1yh%J5^U#o609q=I2+=dt%+(LrhNTVMF(q@iBZNE5n3k%%yknoFm``&D;L-NHt z+Bics!c(kE*rGfKr!>m%I!5)bFkP;R^r(p35aF5A1ydDTvd3bI?F)NJH07^jO#(&L zNLw{XLo7BQ*G1gxe9u&r3$gF#r2WOPmX^i{&y-x>bzw=_8JIDn8q&cZ)pvYo$KAAz_3~BjEY+;gSAz0o)VsPpOnyx?^#6eBhZzOG>jk4J0q@w9huzx#N`V>MV zMT{@V#n?mZLNQ#EFQAiTn)(#BrOE=ONfifx!y>%0z$>u5&8Ro_4~SJ1pNe42 z_}WJ(&W)fkn3(9BZ5v93lSqijR=gF@F}4jD@1W&*42=><2Yc{cIJnV5zE~41%E2fx z{uJ#PB4id3a%2R37?4hhvprT0*Y+NZTyjgqI#38YHq<^hiaJ5Pk~_(4`xjZ}sug6t z7Xln&i_iy?^l*&jO0;V;0_|C-gs26b-oa4eGke1%_-y}H70Irkm_mZ~!vdr96qy4Z z($XSlFMtn!Dm(Va;l`R5qTJ_gx8bqHEUL6D6tXyyG$Bk;BI63RLWV?AsX&6fH!}N3c^X%!m)334M%(}K}O(cYX+v7 z)a|3Le9(%4H8){e+RBv50J2oF=Dh0~1d0M>zZQOK8+TFh8t2wl95E}F{@v1w;VQHU zoRZ?KnW%hl5OP=DNQW`WQp}sBPxTMD+DD|RIe>!~-_gv4@NI6gw&wV(3+;ytS!}L7UD@+5Qk?y)bA?Z-2OOiP*WVtRz>d8Xd9%>Y*)rXfsYn5HmY!}JlRZ!q1%^lPSerj1NHKPmn1 z$8LrXfrxGc_}v%`}y19@9-sw=gYb z`UcZanVw*Jo~h^4oIa)_n1(W)$~2B?0@Ey}`Ai>S`Yh8on10N(n(0qWo0$4=IqAhz zWF~Id1CdWL(`f1M5?8dLn*Fx4Y>%|>_nM^odP&>bQ+7!6>VHcb^7`M{(;U)xvl=&d zV^`cNy3vzA-THwMHvl)wVBBaAhteQ!*j=tDx8N5-Hw)eJ@4&5^ZpvNwm89cVMz>nJ zHPB7Tz|TmxIJ%Y4t#%b|Wmqs$8tA6KAGZ>^(aNpT@HlQ2PvKVkJZ^q3;FdtQe7cov z!>yWb5DDV)I&Mfj{M&;Yk`Mn#=Uqa#iVL`jYgW{3eA+iIdeq`{5vy6|sF;|sH)~MI zsv93WGLjcZwz4L^`E zEj7>;%DAJ1in$GrXY9lL)Hd#O7?b9XuKYIqk~X-Mu@=AWjJ5E~8F!N2DOGLUf618H z$_d7vY*5Qs^WV-`E0(`7c4zwyZSe1H>^HUHUu%Pv&QkuGf8LC>{OI0>AHZ0PPoFk? zeH%Qujs3tjcw`$K!k9*wbd77n4{d`dx4}lnT6!Ye@Xc-bF>Uy9ZTPd>@Z;Oy1#NIb z8~2Ob;M6uay$!as!8vVkUK_lIu_mwlHn^aT{lYf*!8W*ru~xrI+u$^u^*TVXVo23uDZ{#Z}tIei>seekT}f?N2RZP5upxJF|aE z7a3nIJ~(??^edYDjEsk{{Zz&@QlP7(4ZoamkYp=%#+v`$U8Vne=7%sI$T*&HAmc*D zBN&%6rllmhY8Y$nv7IrL+4AZp!_(T&w(X@K^ELZ{j5YtwjJ5KWz*v({K4Yx(i>s~w z0bD+{_N{IE)V6-NZ4W}*xNlqjOF4WD>BO~g;UWx67Hi^D7A{O$oSc*nYDvsaN)%%v zvbb0=fgYlzh{HRU_t0NQ| z8&Ua&SIT7g0qcqiIubN+IL`Y5y&JzZ_zi`lHFy{Seh_}a_>IPI2!1LYO@;?U@w^7V zyYWj{k&=_RC_Q-?+|re>Vo@7R5*?-_6hdtW-S)H z1QJ^25~sZtl;4mF%yTJ&n3Ev0ONt8)+S*81oSc%Fo1UXjTAryn)hDGd&(6&faZRAI zkeK!N?%97==V>`o<`e~I_zvG)*$%#p{@?jR{-kB))}OD^p}sLX=raxPA8Q zXb}tWWxhH}MQi-m9#i?(%C*dpWDP0_%QG_5^i+nj)2LJ=IE5r@ifg7){Mz#8-_^=S zLPla{;*#XW|0LA5_Wn|+6rNMAf0CS2`mO9~@^2$&SKF|^cvWU%2C7C{W?D`fT8`Dp zS^AXaS$eHpX#B*SPR)s92OV z^e7ohHts9&oC%twELXCWWIUz7WD%aS1V3FF12g%PG5;s`*~%*Tn53k_opgOOoy5vM^>mEjj0o#PnQg=3;M_t2vEp z$bPh=8b)NHLZU&Mh1^eDo*9)V{8a9h@`ENx=I>pS&dFSsxqM}&J~=Nb*+TZEZ6)t- z>@PyKaJB0N<&QSDBd2|VwYe{0=$KmJVqkA#@I_l&T=C{MxTFn!xD9@!4WuKvA; zuN=DK_uF6}j)ykh@4%SW!n8PQlw=+EfSaOc1EgIcdt{ytbM2z1JYcPqkUc7&TB%Hi zmTNJ}3k~%mmS;tweT>Umo|K%O{n!3!{_6RG{QtXfLpHi8fiCu-Dr{Y!Gg@P){sbS| zKS68RzDA)l6y{JH3JL|GpwJNt%0ZFlQDA5r1%<*o;dnzU+RB_{)dN4OBlM$uCQ4;A06)qbU;JpkMtR>JKPnScW{8p* zQPKwykIK5nBY(*)QEDH_UsCRo8O>*C&O$#jAAld3lQx&IiE#wuI8btPJDbm9Iv14u zpNAiXO=X7s$-qyKp9Q}@_+{Zoam~k%;<}FMdQdXA;z#}z;79IJkEpFzo@M+zD8*|V zeiY6t_>sL2@uPSh!H@Xg;79y<5Fm}|10WQVPe3o(z63H2Wol-+R7pg;IbET2(zRN& zs1eZo%4-=~>|kYvNWYUmTcPx~x_ZlSj7;;HRx=Ih!|fbXt-q*Xtn_8~O!JvmGY#p- z?wM9I)%qJdW2L{eXJ)F!v30xu=l*CheY`BMWKIRF<@Iuf^z}-LFE(qCw2K^ zva4NX17$n-9@BEBA2Y3Bx{qlU)6bcnU|P%6&a{cC=OF37AJaZe^-Kpd4P-i!X$aGC zOhcJYW@=;_$<)j=j%hs8#Z1$gYX0XiUc&NkQw!5Vrlm~Fm{u{ZW?I9vmZ_a-1Jfp^$`Bb(Z>D}s z1DNWW1~Lt08pkw&sph|haX!-$rlm~FnN~5aW%@hQYfQa|%6RFSnwcgrot2xZ#A4Pn z8}qEW(0$HM%~hsmr75!#a}@9vC$7SF(dCj-mcU*%8NzO+e3BUjcUgh~2=~Grao|sG z7NhBjnab4UMM`X97V23R=qhD8!k?a-u9$L{D6^6+%1kI^Z(Y7ZiA+us9>WcEkE_UC zmUw_E+$W`i!jCi>55kXRvJ8I^TbSoA$<1bk1pLfN&RCS3MGRcow9;c86bIj|`f z^Bnx>ij5t;c=6~}!e7FRagEf+;?mQ@DqLi&n9Y+iGA7XsMqaZ-`v&)D^Jq>v8<-+F z7k6qvW}_$11)VDT5Xeci!bGWEq*0ReSGsp%=})%9hV+l--%c}l%l^}>-|3djWq3|A z87}o?PBXGM9WkJIPleBmKx2VPh939+OrjwwCI;%_u$x)}FWF6ki?`WuPa3)!JR zy~BiTO50||tV}}a8MxEPPrH(lE5XVf$oK!hNCNsl{RLwFFaQ3``?HA|GuHq1SAZE~ z+rQTK*I}-y{kbXsvp-iK)+^3Gcg!*WCx4oI{r~3+LH@e+)`ATWY%DC=WP7mqq0J=^ zKl137#~y!T>yuAC{mj3feXjKR7hc@<(#x-GfAzK3-+1$_9c6F7^X_}^fAC@X&RrjU z{K==gD=PQw-M9b1!Ky=tKl}WPBS))`eR=%ES6_cqbMo6$-+lkX>Dn`A&;9uCpMJL2 zov;7pKfnIgaN**m-~VX5+|+#K>NT9ut+=^+czU&K@7P(KFzt zUcLMD?blyFVBny^Lxv6u96lmwWbmlbA!BYHJMNb86GA6W3WJW^U`$+u1=5ryscB1> zrDtR=x7?GJos+v_W!|dQ^B3H8_rip~=ij?#?S1$E-<|&dyTkuKEPvCKh{&j^)6CJ+ zXT-$bIy3IJ+h@(jQ4#U;?v(QXd;b3;B{KxW=y&=y8IZEj*YGW#+@Zp z^o(h)MpqzXKM9o(#x!T6E0l3p2^AybZj8;0yEBestnFKfXG}Umx)K5cIN_q8W?8mqdV?E=(j6)drV{ByHpK%;xJ>vw%0~lKv4`iIrco5?f#)BD`G9JRX zjPX#$6^w^5u4X)(aV_H!j2jpSF;)yR{UaIsF%D)N$apm4P{ubiHZvZ_IG*t>j8hqp zXPn1)0^>r)^zB)?wlbc`cst`sjLR8^F|J~)GOl54U~Fe>WZcBq#Ms*?(;vY&fN><_ z5XMoAjf|%)_G6sPSkHJF;}FK_jE#)fF^*&G#siQ9#-5C|b+`76EzIx0IG=Gx z#wCn>7?(2kV_e3#8{-Pb-5FOi?!ma0aWBRVjQcY7p2FpyaRB2m#vzPV#zw{wjN=$D zW1Py^jR#bDj6E3_GH%a!E8`A~w=?d@xSTP4cZRMi#=RKVFb-qfz<3#BB|@f8d%MGr zu_q7M^o-jx4q@DZv5|2{#&L{&7$-39#n{3)jB!3=Hy+@XF!p3z%D6q_GR7SkS1|6# zxSFvK<66eO7&kBuW9%Kt>E{7s0OJmf0~vQ@9Lm^-v6*o%#_^297+V;-@qjX)aRqw&4`AGZaUkQ4j6)fR zF*a-N1Eu?T%{}8(%{}8h&HhMfzfiN!c&lcg@pjFAh_qj>*=Jm(*=Jm%*&iqE+co=) zn>723y{B^eL#6!y#vK_4YW&HPAFA;gn>D^s^5Zo=<5UetN`9V(%@P-CI7Z^F8jh2A zJ7do^5|=aX#kh*G)aokGa+1~--zkRvl`d_6f|)gqNvwk;EiqlRjz<@*=1EH0V!~2a zN$d1B?n)b2xkp`?G|@p;cVE zNV84XGNHYveKML{BtL`AmvH(>-Xt+vzb2cs)+{APzR-Fut&z_X`*jFYISE##DB3)T z%1f{k<X#fxV)%b5r38g)9QLO)LK52JDx3{Aeqr}9SW z)xxK87YwA?r}9Tw3!lm%#YeMG<&os4r9WG_2iWBBKO52}z9tVUr@@FVS963;{>^sy zN9C5{r};_)gqofO5F}QT=c&H&jo8;Yv#v*$YM~GI74; z3As4&YT@3AseZZIr+OBQQmUm(?pB%V$TzBYlnyQ486usrj&#xv57%-b_t(Zc zrPVjtJ~;WABK@xI1KE#rDJLYqSukl`zGc03wqvgKAVRdYvfq^D%vt|sIh*2;w`;j^ zb*ITK3UQ}P$|cHC{$&4ccGM>+CzB)IQcfmEyCvo1Y{#XXA{_NETa*)-PS<$I_&d|7 z#Vd{_EaMg9s5dfRvmEi1@rrSY2bDKxxH4YT9P!rTrRA4wZ>PA#GXrA@XU&!2M>@)n z3_sS<4rt*!+d~T9S>9y$5svmwhVP6o?eU}1Yr@^NealAAOYzWhR>mXN5l<=qXh%7e z;lw%kGMre>U23Cwg!(tndn`bj!- z9Q8u-r?hb&*M@(aqkKsJwN_J(yQ#Ek{kG(rUCIH4cdJXkPdLtze&={!_ViRP=+gAY zT6-VBdf}0>7$|;o4MG@y%h<^HZN_noOBp9H-pts-_$cFi z#$PcmVf-cIQpT?`E@QltaRuXjjH?-c$GDcUwocH%_;cnfcgXbbX6(nfO5?MY@ml8RGyagVwhpMR6K-X`R&TU*Ku@;6 zo%!0hrkwFc=GSt5doiwJzScf#>xSC8Vh!_OVEcB)4>N9JthLA9bESOWWqttT*BNW; z9NIWBkoh*|YwMKl8HX}oTc^?1ExnmDVqD1h5yo2??_#{2 z@qWhTjNfBi#rSK+HH@nn+Zk))XWpkw}4=0C=GJLB6J zhq8ZujLVs?&A0U|pJ3)!F<QyD+UIFIpmjnDq~U|h)jc*Yjy_hh`4`BIyJb==7^y_1>0o%tIW zmot8aaTVhgjB6M_!#JPAyNR)#`SUgR%pb_OiTR5ed*3DF^C{y1#(NnDGA?5r%D9}d znek_g;~5`hT*%?~Vw}qS1&lSl{yD~Z%-_Sfgz+bgw=;g3aRuWiHTxW1Z^kvue~Pi4 z@m-ALCd>4XW!%L4)r_@$Dn`cMcT4-580WG3P{skw&u5&^{E3VMnZJ(lR*r8U#-Yr= zo3WX3CgW1J-{CZcUQa!+Fe(?m{&Wrd3G|dPH8v|T69_A-w~=k0~#yk6HvD=oiFYIa6GMA!SWY za@}SzwA}KNdNT6Q6+7R{kbJtk^64$x)>!KYH9c?!uQthbpe3wz(DXZr&<@K>>Teb~ z;w!PXTB+Gja>QTqo#~M{%@IF|mpbxU;>n z9r-8u+Nz`Gzr{tKlpgtRkTZOXqufgOq!oAdKer8kg^PcrXHRy)wC+K^$V;w6r8(p& z*A2B*XIgirbvkX8RjxymytVQv^{dYEqhYNbl9=vVc%*+#ca%rT&k*l2kqx;H;*6hM zr*YOxTK}ecs^za-e{$uM|5R_~CH30csy3|?Q~I^lbHY?#6CLGOt^;bT*>ZiI!qem> z*EyW^Rbp-RU9P_eLu0MA+j9MEi9=p;y=$4He#mttXZ*EwAI*etFj@~yesXx$ZHI<2eIx}&zrPV3gRKIg2Da(z%+O(#CBJ87X2rgo6-@{;RXu6$YFoP5F=j`Anh z|L86+xh|~jK9KtnT+5GKC(?E)5TC+#wNL&#+XK0d=ZueBXV!K#&^Z=V-WNIKC9$^K zK|y~fSAw(_L&I8)*RWP2`afubl2R|`lq(r zLR(j+lL0ioTtCo!AbmddNtzGZ`iLu^)=OOZBvCC!+IoT$(>gl66|aS-)gL zrgeX9mkRCUpwv6dkK6~K?Oq{1l^1RIi`>5wj54CNA9A07#nFGu(j)J#@^LkHO|M+n zUx8T2ORm2=<0ID#s6CM_j1$Wmq+!jy#Ho(>NK7XJ$V=jENBbagj-&sS>;2B~|L%OE z)M7V9n@f4I-*{W0#qlT}8t!k6o7(Wb9qblu;LIj&4vu_~he7LFn>Mt;PVbb?{~qdp z?`NkBt`72C@GXzs6FA}3?Wzkuu3O}Bhy5Yyq8|*udARb)s?o(h9vd9?rFpFrw@rYs zkl^GC?6f~eTwk@}S3B@iPttYBfu;TZ4lKND+FT(hZr9suPp-7AwSDE+Huypt?B{UI zhjHJp8T>SxV(`Ah{)q#-5WgY^e(tsl?FU6Ka^QxRyQ;?JPj+DPhy2w1BXPOorxk+; ztSHi2>l~~kYC0;mTk`BD^T{8}i5<&`COoz?hiK`FE%yoI+(J~D)&6m!)qCH3f@s6G+n*$=Z~o&cqL#f+ zJVUg!UFfq!%l3c$oWO3oN{Q;vYtu z#>+&F*6yzmt=RgVpw(Ai+%E8IOJ5~g{c6Z-L=g``m8W*SPPnA_zBh>aePw!+Xz4wk zZxOXDJ|Jk>TN`%}t_U0~^1oqdfuJQ{o)a`=WW?LVH;#Kj(1d#B9m0OMFBG&k^%Ftk zLVCYT=C#}N1PwXetl^8nSYlC?5#}UKG@re?!tO^FAQ^4R^gK zXxaCjKP0SlPZKo3ZLgpqFZU@I<{m2q4GB0ZsPU&^JITBxBVW+cw`v51e0Gs}>B2%m zE!C$5ExR`UBQmf4u~^WMq8|k{o}ctFnOnRc7S!*@p9C#kJNXkb&;ROSL6u*A61223 z>{Bu?yQ4(V(${|!w4x$(x3K^CgMx+x*9uy?=cWqb|I7$M6Fx~7G~efOL6z{`g8I$0 z3)(QHOC{Ma4VWZoe)S?jOP1OM_51l zXqo9}vCpw)e5NnE;0 z(hjA9D)9#d4SDbvL5(kW741{V%b|kCJ$SdECGiD<8aurvsAbzxi3k57X!WLE2PnL_ z8iSzKKFNaS2X2xyXosM+{wE~==O#fd&-Xnj2^#Xm4nh4+9v3vu zqe)PGk3Lo8t^}VPC0g-ElAyJFHVUe&dRC%&3|Wspvsy1 z1&wQdK~T%p{eqVLY!|fQlTL>zoc#Mn37RncHbF~2%@)+6J}zj(_Hse>H@+1#M7=I) zTK~_;z43mNpqBh3K^xLH2wE2Tip1>?30m>Nc|k*N^ZA_I#eEYjY4l7%jjvb)4Y}~J z#KYbdG(Y-;pnmax2&&BL@dde0m^4As&hdgONAm==ta(z<(jL16t^U`yf;M!yDriV{ z?<3?M^+?dT>+=Oo7`jH#+R4vIYW!Hx{F}cMw7SbRY5qg+qvXE&$uL1nr_2{L|Kw^x zEq6a9Xvwjif;QZIQqa=%O@hYNcB&TPujnmkh-Z+Xenpc6HF`}Kv^H;^pbh6z1+`4g z6Ewecqo95l9~U$v>}5e4?tNd-xcBx6n(*Us$@e)YXhqOPLH#1!j#2o=+x;Y+*I&?( z_)&r?Gs6X~4v!JEbntvZEgnk+jr%rFP~)o`1x>I#E@)~H~@@=_oW?i^{$dun|3KjLuRY5Oz z^Ym8t9sjkW=M!G4d-RDnS5AL3ymQEe2E&TZ>fXk7)4o}BCcOK{vHd@HYp3r1cjIlf zm(GXRU+UGb)9McDLH7zv(Tf$~w;jaF%_u*$bLJPnD)ar-9q)TshCgsRJmbaj@9v!I zr_MazZhF;>Zt9yKKmY8v5k1rq=P$qaN=je#w`-R}Yu{-MFKnzk8`)h#RU9%|6XFW>Fv-4dR@Jl z(o=2n9y57VhtcY!GePI}ckih_H8cDAD<2J2tp&C3Z~pyy_+y=74?aD5klM{>mD!%% z9G)3}!29FR`>9)}-`-s3KR_)XGiIK#=VZw0^H{I!{`hPWN=#{fQ)!je7wfmQOzUqsH)Ne=I z`l=HqOuGJk`C!$v>syKUt?#Bjr>l#;_rZ~B-+52?H=G}?j=y(I=F6|$q|RxV)!~k; zQR?hBR$m+2XQ+Dn3yDXAQiiJ`!}fKliU?50e|Bf5j~46IZEnK~e8zQDzh1TZ{W%Zy zQlI+Q-OmgNt_xp!{eIt{k_>A1q}xt*etMMZbIYQeN(K#A*WH~LTDbgXb;rxE$1I&6 zruJF!(05A*HiZAD*Q7DuOrNawEY4nIU+k|2q-?tI%jU6a%8HeLJo`wvT3%Y*5N4jJ z4vl~9*UsOEskRRKT!DCuyO6x zh8gENDR=sCJ`Pix9{kUHLzE$E>6aIe*p4B6xgC8*1rAUTzcO#^C5x{*#JIiNefI~c z{fY+c%?uf<#^u_m`&1YA{pP`2%2YLH%Z7nfD<-Mu9{Az*1D(Uw9q|v;e!gwA`fSIF zFJCAKQ;*HxbFu6tz4~P1{DBEyhpQ7Bp5CB)+gm;JS;_l@3RSi7@qU-ObQ`ZerF&t) z#{Q$!mv&B!&U$R5dNy8N{g_v%I?m&{^FuF;Ry*AB(7ZopO;pYM?r2(GIbQue@uF_v zXM@!jW`%#HKXZ$E$Hk(=`){A5>Yh2#a_x>#_1kX(*N^fEQEN{}ZmI5eA^hce=Q}6) z4N<#%bF(gQ)KK+klV7is>VLvFd@B)oC0dM9<=gs$07iYeGB6#!w)#s0iC*RyR zLA{oL&!U2!fofv(nS+aNJ0C8~-x#%U#q&Y2lP9R_!s}{ZvX4-A4Cv8tplpPiGXMJ= zTfY!>%j}PSS+2*I2|k~hWqYq7eE8YKPW`P0b;rl;_dU_8Dg3n$KkJhosH%^492ngx zDNtQ7@`XzOPA%b6JM{TEYW6MahKzPiAwPww6Vp#NoPKhMx~^>V+Z#R~uU2jEr({eU zr}jVI+1ed+Djrk%VhP&?HR@2g^y8t)(tHB>Q|K0@0S%kGH?vP$-!1J%TzKh1__)a<3mc|PRqvkG_{!72j!=J0m~Op)#!z+Cs)(~we21%UGrM`~ zhsCJ#kAB@_RNi#eobv6e1#yw;fG=XtpLjJ!{rIaji^A6qSG&FTar1(=_3HO4jwb2n zk5SFTUc2;@b-22vuI2Z|F$Pt4r+v+?Pp7KoFW-7S?U^w3^=%8SFD(sHkNrotYVYR} zYVAzpzL<*P>c47oSI@{cs$F~3?(>Z7t!^Y+xBsfDpCtdNbXXR}?L(}ZaCX82FJzk4 zh8KJ;>`0!f=KNybalOlE_298>5A^;tMonI^{l@&VA!^_0y*lnWVp5-b+Sqa77bevb zH?-=G@lk5Rz`$E-?wg=~xziZ(anHf(#ot!l``bfCwQlZr6PF*1RpXI$hm0cfYUY>kzf$C-$!Chay$~LFXR5X=J!MW&Of#0}c*U*XDd1t3EYM4SL0t z_FU7o@G;&O++T?vpq|QIw`Rthx2hd~PP!42cB?x2*OQ;NJ7ZEG>2qOyP-?KcX4|R4 z3+*SVjaik!AbC zCAAptnKJ#^3emuW6`k-(qloD5~jT-3>#c$g#YV? zUh}WLIC9u^Lrz8J=J$QB8xHJ_T#$M3nxWIv9z}Q5Tr>RqncL$TpI$ROeCBSiceh8OeC54~ontyt&LzVkJMt@wdo zt$$oKTyD3eDCqmEhI`e*yc8mf0yc5i;;s$oz|Kv2KOt{QA(zYR}cd)46e>!;_= zCSNto>}`K<-K|#*N7~g)H&3`~IGLYr9IwA>*f#Lwm>C_e8UlAb|M3GCuNdww*>b=A z)D=T-%l=-L{Z|av>o<<>yW@)C)1;*dziqu@n0F@Rwx8BtF=X_re%USKiedVu)b^&i zR}8(jneczbP}*nz#t|d07~E&9Za&@v=A~}0erYxYr!MRH+&9ez_ve4S z_1K%P8LW^Ok8 zH1g^D4~%Oz3_n{PeO2FV_^QOM>rCHf!J= z7)b4IGQ4(anQrGBO@?Rio$`t&n+(&-UT^sK15JjVk5qg-ET_p3@IWH|n+)CCm*eFXU8YBID7NXU0<*JLo>?{?km_sfR9Gw-}@$l1$=-@VqF z7FJ(2tZ^Il$h)6jHthYS)_dTamkqXwA1r(A>C1+yZF%crHeELO9(byY_o~Z=w_hFi z<9ACg8!~d;_8h+BvSDh^yK;_2T{aZoFzs&`ciHgq4^!_C9DLc(Va+ocdEG7>EbBTg zsCB<==$UY_N5aKM!@&s=%Pya3G$dB78~XCGMnli^${y*JjsF{a-vQUuvh}+YLI>#z zh!PP58-$`FmVj8m8UYbIB|$(+44_~^;BXK-cI=@SX?Bhk6%{*nZ0De&Vh>HRzyBmV z3Leir_rCY;{l52p-`2%{&DvAev^BG5Pxf#J8E2}w?|REk6?tA6nEsYqUX|iBVm0mu zCA1Gpc*`}O$>_Ud_FL|Iv+*G>{oZmjM-KRPoX1-(zl-{$${}yLzn3p9^R|D>b@pe> z{m}I-cU`e<&>sD_T&4Ctz6pwNxu1G>ci_Km<~}v3Dx31OnVbFa<;j`V&D@FFYkQ`g zZRW22Fyi##gU#IX$&6Wwh0WZ83tj8|wl#BK`6O_zu59M|D*7nv&2HvC z^jL6kV?Z-kwjSEd{gQa-iN%O!?hCCZH`V^l+);{o-d}q(a}8P^K3DG4%so}1^T13C zZXN^wo4H#?-}hhD*u?FxXp>v>xQRQu*S({z)lJ;+5{t!U=bE?=R^{9ue58px<(P-~ zW=RuwBx6p|qKqc)%&Ogw`mb-|#=9}h6jPeG^Yx2IJeb$S4fLJla4D>byZLhSne)C) z+;elzJiP7M#Qk{NU(hnVi96=?PL8Eh6L+!c4U53uP2BO5rgz)NZsO`oFN7DgyOO93 z|9Yid#>Ymka!K}(Rj(Vl%>3HJy&g7lPe&K5zI~&S`*i30to3Ibxf8l~z3qFXk*hyo zm)?L~ja;+Jd5-4Ujog4mhP_NSHF8&~1YR>+f%}dLdX5Q=+}Fd-UYQcz$aPcxy>Ufw zBe$i;jv1GxHFA%~^!#Yz(a0UHS5p`}ypg;6@Wd&#PL13TokCtswQ1xY)9yL)QP)Q9 z{uj@D<{CC~Q?GCsMw*RWUuH#*N=73$$R_~74v#C z)NTNKC5-uawt=hDZ}Q6bM;o}#JsCx;y$xK&g-8QyREqE~PtuSlGbrW0_FKi)`RpGHx!|6V$-1UlT7ho8G|PJ$H57HqQp`8gqC2 zZ{SYx?XdB{;0Ep=hd0M~J2Y^6=!(r4JsP+%AG;(RFm2#Incx;5YtX>GT|QG|gn9#4 zm(9Tc2CmTb_(ImZdhY15MFX2&)^nrExC7oitmhsXX+5*Kx}Mu9A)`p?ay@rajmamo zQ}x{LuZBC1JXFtpbg*IOf?f684fBote#@=rZr7=f*}3S)vqP&t=$s;;rD$w9i zscdJ+htjDZn=b8xy|C`lLHd&zL(Y_n-U2(S35{8AxFaX{iP)5?luY6OO?@| zyI#V5D2U#(bGd|@q&-BXOA_4A%ewEFFX5hkH$tsjq=frXH+Jj35DE8Fe*M)k{t|9- zv`GAVl7!20xb<%1cnS9uzx@SwcL`UU<%0k4HzM)CX-5gyc)IDn)f@@;S@*f0_*N3G z3X7TG%$9Kb%^Tm>+z|eymxLRS57V(!BxD@}mmKk)J7T6SM<$g1mA${_ZtDA2}0tKU!;r(?+lX0OC0p|-4}GtXHa6qgEL&@xBrAB&t8nP(9(w}D!*i(TJ)dV;iBIfD9Z-`8PkBn}o6`U!)BA|j)#g?xWm z%EtkO$VG63tJi395J+k*mYQ((T|K~KAK6$myVo>BIxHUea9pwi%+Ege+GPk*OdkxMwrr0Em)Q|a`h|s7ovLBiB=O*+| zT0U>yJib4QkBWz7k(Qq*D!|;`#RVcNrHyQ|RF~A&GB26Gv^FxCKQUzG?T}84Oh?~U z3@Qn6%G~CpQmItEKsq-nAtYt`nYX{K#}4Z#KiI;=EJ zN{qTuC27j4g4hNgiJ!Pp71DnH-7Vc1;fsD;gCA2^`_tbBFK1AdXhi5evPh7zR{51} zGnO!@Pl#uR$50npOJP#JBwkUZD8QT}{b`3U#grO$S|-gKy!`FOKzm{hK`%2vBWrUB`sX6FUYFCwfU)GboWjL>=3ybO!r9-KBX-ki9*F+xn#xta23OUYQ?l!^) z>@*$pDa{61SmC#@dI+makyGE}K@vJ_8@DtKnLlzcJgF?Xzt(Oejw)pu<;^hVPhglj zcri>F9t>?BMG;-&)sPoM)ai*l_@JOB@(O*0 z3~eP!o3Bi1!>_iThLEAeq?GKK9EPa|((6E(^4n9U4hEFzRYOVp8vQEWa%~=(A*ExW zLK#Gio+@ci&{Lk)jvhEy2|vaeiL?kZU<;Y3D+WHCrq2g?DO37LPv1^k$S_bu`5mMT z7-f_a{E&<2OD=*k{h*tJBBiAZ{Y*3{6TUWO;-y8IIJBcoMBnN~sH(}JG!wt;(H%X) z5#$d&2r`rvDCHpvJSImPJ;h`wP|Ws}5<^I*B~O!Jdo`-Pmj>0|L7i%Erz_E}X;-cx zRQEM9QJ~u416(9Or0rD^ztVhZB3~NFmpY{{(iW;3GAYBW`W$(g9D;Sa3u=Kb1f{wp z%5-5eS(F0wVEQtI(mYG^h6hyYb$qp@X(&LC zOOx;d6jr<2I~lHpdokPt;U?D!@RhCHM)=U3_EimuT8&DTQn{>NiM~wu&7Me6=Rix| z%8=5Gl_6K6 zjO+|*RHgpmhp!G9!g?4;^lEgf}_85WeLM7>6}O1WH7$nsU-FnOddwc&|jtcL!hPZ{rSFVU~jtsv+fiC;(5~?jQ+XdW(t-{0U~(!3 zqQ2TQs5riCKavqX1lk#8sk26nNA5{*kIW0%(h-cJasrS^T&l!|A-ufpO+Y^`2F0Qs zya(#EN=xp?2y!#f4EP}z10OxqLt1(s()J-#_Eq9BI%!ayqSUEQd^M_*mnzlCL51pM zXC&!RV^FSxzQ;m%qL*_A*jQy=# zj5lcGT1ZD~Q>C_2o6@|BGE`>32B8jgR45$>9ZE-}iC6!s<;p@OsjX%r?S*q-)90c- z>AE-${&p+(&&uQXD2i$V$;*SG#w24OhoQ=(RM)`H6BIFdKMVUFajRm6CJ*~FKe4#K z>TmoU$Nl=h@zWpoN3`<8=%7Y*z*yX2jkW}3TcuX6B5P~X_5<6&hh2ajl-dOmY>3o0 z!tTSan4o={c(k&OJyj`VMiAAuXUps~2L{rpr!r+6<%9k`5&pa=c|E40&}H=q`;D~p zVS|VbH?kmOfDXn0ZK}QK`xroMg(GPrq)myK9HSrVTiyo4Hc-@I&;z<285XLP1>X~P z!~=F@EQNj})JI#A>D2_i8ljiun_fcfla$qk8l@#t7s~sp>MA_zq*BxwdA!o})zRK$ z{uoLMl#&B<6GBJQ-lLdw{@Njb7^hHf`fJdyWPReBoyf?dsNX;z%aV`QpGX)=~lN;Rx1X1T1*XtOW}wo;?4ntr9&QOU4Piz#id6iUTm3B}t( zQ9D6v2O(m9n;Z_PfYQTkLzzMixn z=-Id1ttL^aQL18<%jXzqBJ?~+2X(AXX`AY@n zYEkI34(PM=d`%vY{uS(L1r^Kd!Jdq><`|<*sCE*K8nr5wawVanFAIfG)qv-c84T(! z@I=Un5GWvn5K-C%-)+`B3C?2$y+!^As{W);jzQY-Bv0>UD1C0=(CO&{p_lyf^_Fq*SX21%f{Iic zhR`V2 zG2;jKAO?=zjus9L#!>E3qKMd`f;qtf0{8I1h|$61tTlW2eOH_uA0_yHdm=^clHWrV zvKtfOWJMB7Dve@Jw2 zDF1u3vJx?q-*ts1t%KwAIlFK7ekd&xbXW}ZDwO;9R%SmWP6f+-w9R!mYK1?MS}VUh zPACYM7fN6RVpheO+hJjT;r!9T;W+%#MKp5`r2m~Sc|R1R50Tc7p;<~XaHoLMK+XoR zU3z%_fUO|wQ@bFm09^+?0+C+Jq!g&g)+^Zj&`?U&UZsVLqMH~T-j5m@9O>sDipGuF zhKw2(-p1)62*crl-|kWrSwECtN*iTq$=yF9Syv=i0gVS~JWXRYjn8TPK%>=GSvY-Z z{CLi%u`i8oH2TmONaH*jm(wVw@gR+tXnaniN}4Pk6B;=*j;1k?#sxI4p|Oz06ExnW zv5v;KG`8C$i@!6CeQ6v^V<3$yY1~X>9*z5HET{1*jSp#jOCxKuEFEJSIW!KW(Tzqg z8iQzzp>ZjV>u40ySV-eh8n4s%oJQ6bI)60wpwW@W;WYAS^rcZmV?2#(XxvU?A&rM= ztfKJ=jV&~4(Cx&SMtS>t#(TM)BQX3PJ@GHArcqC^$-bnl5!$VRVEa z41~W3XKxORalx|zV|aup%r9z;ATrW#rT`Wrii98S2YV$FMTl@h5W?V0-QgmEfEqQ- zW9+ce_IB2!hf(uc9)7`*f?+WMIE)jg=u*^2Y1*-oIGU4~aq69-cd#fb+AkCXLKJL2 z)+*$Z@zDYt|4AKTkcerGsO2R5m_uT|%K3Rx$~XY((h9(Nn%l zNyxD{dK$3@C?E=ljcvPBHWp2qh$d25 zs5RsznD0tdMGvJaNRM}M4tTEA{QNcrEqcAg= z7|a*A2KkAo?n-jjE8^RIY7~ieSooY^QABtcnyc)PZ8F~$^P{Fw9C}DIIDaCfJq^M}b z>`vk$A?ap94jaElPcCFNw33q6CT(s9%B(FNGt~T_>;H=qxHye{HSqe^uXY)K^xlN` zU-G%DJac4rK%P-;*tDe8Ge431=%4s)eUbZQPY!v1|L?tIJDGm=SC%gOFRy>EUjM1s z|N7unnFfD-FzA0u!~dFee%2ynGMPSq*(})qHTwRWQ~iIunUGzItxG5i?@D|VV-vg3 z!kj;3OnSBq6L!>mBAjzvycF}`|9Iw|Ot$RypZ+F%k%^pX-1krVccK07hyTkNwHe0L zs`@dHGdkxkYZR2E`2`y#&EoDz_ZPyv) zvOh)g6Mk3;{AZIU@%}h2|FdD`{=Q%wCwwJc588)i<@A4wi|FeD9j`te@4t%H z(2dal>lfp1F6IxH0)5HzP3nnUayMyD(P?rQ_AAb7+dr(iF5dor{22oIVuy2N6yMqT zr~ZEQLt_0|d{KCinGuY~Zp8CH*MIX8(8fC~5ZMz;kgP@LfhQ>6Gf9xbI&~%F1RX&& z;K|x`Jc#UpBsd@I*{lSH6!(KS0pEIGB-t#^3&5uVhjhfVI{2Z$KR`*~Yk)B(&>uWO9efx!6}&z$15^k;3)ru-ERW7W z-7Z*tgPa2#3#tLn1BwYDF9i3SVGl6mWx#=5@h(4+A;mCr=mTB|ECLmR-wUh*5#1!f z6BdXc@(SQB5Q(!I7}X7aASd_$bOC%VaDpXP?!fy1Syo6Fyd%&PM8bLjSI~Sau!80Z z-lq9`Kvs8|e}YCdZvtEZGW>-h#X?$6u$<-zs`ilSuL0ah^Jzd1o(Uz;hoBdTsE;uHeYKqS8e3u&I< zQJOCY-lq9`z?U>H0Zy|+J3yR#;93xgvkchTUZxM(ch&<$;&%q}KzZ=*3zRruZv=R% z5A+9>foB7|fDVJ_0Bu3#;Dx|dAQDe1@IHv7RRera%Rd1R^_8`a!$5_8biDw((YzIK zGR;o~t_6{J)&U=bSV;`34miM37SBLnAc*)00$!ly1lRXRn}BXgzQa0Q5jO$FYh`D$S2;W9ZJI2%Oti2~jLkv>%o{0btvk;AvTgGfA% zz(^4BzX14m-@~p}@%YgSm zM9&&v{0xdRhn(O|5GliIpph>{IY4d#3;+@RV}S4dWIB8TI{3ps{1E&KiU&`AY;_9c z1HKaY0YuXJ1nk9^#ZPbx$eH*7E)IlE1D^tX3?lyPfZ;)CPml|Nk3d9+T3}-^+C26* zHUayD$nr01 zW$g+$2t;(_0aIz7;0qA(Pky$h4ph!f zFJ}yBEBp{#3@Qbm3MBjBNc(34w}HwbzYZKc8+xap-2#_`h#!JaMX&|%BLOapM0W1GB{W|Oyh`)e zflVM1CqexL8BcHth{U-ZSP3Hess&E_Mb>V8fiIFUpF#P&1omExz5$*CJdgrA0KN>U zxEys4UImy5BKax=o?3x12=Yqcvy~VN!M_9^OvSjl6zv??e>LVAkPif22a&!*@X%V= zJ>m!0a|8MS_+G$TkOclEK;A}KUI>QLJi+dpkstW!1>6A=B5XPEF^H7I&@I>}3yO!l z3CP?g%OeZ82}EqyR^XH!GMhgYxEDmqieL+f#A7H%^97Oo5{ybmdqA87uYgEiYJdtE zXg`o^1IL3%JifpMAQI1VU~DFIhMxt%q%7zFJDCbR56XkQ3aFhei<4j$h@?yKIW2Dj z_Rf*T&jIp5wFp}QoSuumfW4BwK<7LVc!JwN`ryUD9{IAo_X2(fu^`tjz&iO(nLZrg zG7!-}3&<>#@ho5;ns)?-(Yz3tPV-s7+cbX<=v6FhV+7}cNI5J3J_iLMU8)4nz97r+OV9^!K55SiK%df+BARdCV zZ=erA9z_r&?F&G?n=&2rffGTbFZ%#L+`?E%(gG^oMmd950Y-sH-4Vw1&FlOD&YJ_cz%aG4LJKT>H>Te z@HB|%Rt0-1$}}ag4byIb>L$V$(M6I=BOZZvIi6R0z|@+y``!RGCvx?nIJd#i3eT+@xYTk zzT+BY{hjQi&jgV=DhDoZ!h8vS#J~)YGumo7@HEJcv|Hd)5F2Geko;zj@C3>4-Uv^S z{4OmEJVEl?Fl~e-NPe4@2A&}K-IF=ylLX0co=8}NVDKa2P5at)zJ1jUr6cHH_7}Yx9vCHxj*KFk$mrP2{w6kiGif-4j1&Y!WBXq$?S@1om>rE&1biO$-D3yU zOhM$go2_pTi$PGbUvRWwjv$mBO8ySA^ow*4pA#`lAhKjf2fG9WU{~cJ%Rs--NP#8W z=36`iZT>oyfi`V)8ffz^a|k}rMqX}k{HZp6CP1B|-eNN1Gg2~AGtx53GRiY5Givbm zT9Q$hMP;jGYiFls=Vg~>mt~h{S7ujbOR}jP?HqOvC&xL5m*blw%!$uQ%@OC6=9K4D z<<#Uza;RMGTy`!e*EyG$>zgagjn7TZ73Y@bmgiRG*5pcZsXXmGb{;3sIggj;nR8J}2KfpO^2OFU*h6Pt6zSm*$t}SLN5_OY*4#?E-cI zr@*;@SKwP9EQl{iEf5!!7L*rM71R_+3aFjhJJ~xqJDqp(cKYrV?u_4=x>LNfbZ7a_ zs+~1EB|E7??Lu}Tr_i~OSLjTFp~%8-{z* zv`vA&Y0x+iI+sD~O6Xk;&Fi4Mq@<+;39~V`CjCjQBGwigirHc-F-Pnub{4ydd14>2 zuQ*676vv3;#VO)cahg~x&J&l4%f#j4N^zCAT3jQp6HCM`Vk%uFT|3<{otN%-UQt<5Wl?oeT~SMsO0i+FRk35STd_}ZP;pFgN^x3oUU6A* zWpQQDwM$iEfx?uMKPA=fF$aUOD8iJaCUmnz7i6>{f>oCP6QDd+`x z=m(V<)#wE+87i5EnO2#OnQoarnL(K`nJJlRnR%IInU$HD^( zy!^8K%KYm5y8M=Wl>);8s{+RYw*nuu&=|DNG_*{#J!h<6p?v>8>C;8pMeHI@k#iBR z$hSyX6kn8DBrYl~Dle)kswt8bQN`ND>|#!_b1|>jw^&#lUz}PjE-o!DFRm)CDV7vd zCE6wI5>AP839rPrL|77El3F4zDJ>~4sVb={k&r&i!&)AILXTymw>qPz`l6S{qlb#o zJIm2CYtSnx^hh>(Bk7C2=!fy>gJSf*a`e3#^g9ZDj*b52jK1cJeijeAEY2u}U9N&% zmSj+w+L`Q3PNs7vFViHj$nkmjK%`DHX%B;zhWKvn$S?nxMmU9*l)=QWbpOu;= z&MM6+&#KC*$&$dPLq{RjU;xgj^;A;VVg@xDP>S|wh_>j1c9;iyUI~k?0&C7e`*KDL zRs)(<`_f*19Y!mx287y(4 zA`i0BAcIzNEw~jG7?e1X^(8%#^?{|GvoHC^-gNW8V#SOOcUJn|WF@j{Z4+5FoJ7{W z?*SE+A8Mzx7~<9z+V3G=x(6Slg0ZJbgR4Q^eLJtrg=2$9BqJ zxBSKFRYha?aVY^cmb;26bBf|FVO;k-bg#p2$EwWw*=4)GH!HSD-g%==^KiG#JuVz- zpzy52O7J>!_f~%JElV#urzx+z9Ykk}R4R^GYKC+l}Um zhKiLsdnR5|U-d=pR@t3xmu@Rr2A#%HOq9aV&tZzHk)Z~ke)|;KsYjzzY_F+7G0A@n zKX6bj%XMytcwt;{t);`~L8i-?A?_@~ zU&n8l+}Y+ECSCxk_{eSC{c*yK z%A%+aNvXAGp85MHaD?N>D-;YmG)dCvyIS3icWUS+%_)bbQG4;8YEi+gjH^#-_bg{x z=$&df;$VOGQ@Z=E_Bp*A0~$ZCTu)Wt-CmA;XMYFQr0s)7ooQJyW*5J(?y7U%Z4-q& z?~Q|_1?tpwyhBs$pwwM#v#i17i~YE@vvbcyUr5^MW|zabpS?9fle3(v#e3`*{vIYC zcOLgh=4St9^C@XuHn-pFw+C~Q1~x1QE00$=x7J>SNc>dc@fpY)i|>h7|{c!6Kl zC*6#X6Fh#qJ7UP})nE7Qh@W-&R6b?RpyrONjVy>;^IA0|_mumKyCKuAOdef%Afbe} zQ~T_kW9v2X<6-PMxp|=A^!t%HXI8HCe0no?LJvdrFRQ9EPo1jE;J^t4Vd)4*VFIIO z?Rwj>+b?yEcU@yOmq@5$tc$V~jE44EC#tne4hgC6CyvNWd>VQ(pZ%#Mx99WP4fRwh zgVMUtlUf;Pvf532xqped{fPr-ysgij__T44>y;D9krQ82Wn_~6k1vkx4;GZge(n&x zbNP)W%XW8LyP@BN(JPAF9@esZJ)hRFQfbMFvs=o=i5>rdS^UCDRCpp*U^19YZOtJg zYmPdleD!tia!H?(+$V9;_L_l@cyL6v>Ks)ig$XPrJ>~>YTLX?Baj5BO3=N(c>?#mN zvE9Q1Y>hY_h*(`ud+yx1_!!|#{3>1)WsS#ZTU(9|xuu|I^(};_n~NRWB{C8}^5e7J zhqB4f@bIu6h99iWWV^a>I(N{tvqko7Inrx#2Tgn8=u0~}Zf$kyz%h{N)J~p4FQSyS zEt}I>mUIXCjqgdZ?Knsykzv+4!x)AX5*b<;i}3NFLl*NTYGxW94F5!SFTO&yXti2 z1DlRc2mEeEz8o87zjVR0VZBG~;yrP3>gSz&Z*A8V?|2q<{3rLXt+JW8Z!!BtyOq6e zbiLCwCT92}ge=>zEkcT}V`eEd0#!{3KV_*I@c27?vL znaeSkyE%+xLpu40fJkARs5s-k`C=x!%9ZpZohh=7M;m#ICVh$SO725jcO+LA=pNO& zb)!3@bz=jS$-RBgHg`Sz)c7g8-O8E6KMxU&k5cP8B6H`Os}>c)Nk{d?k1Z3j$LJdv zDaT5#uudl59&qH#P-ftmSHW3YZ_9+2{p*^?kBo`!JJ0e!<5Q0vYja<@ZlC{j?|m1u zB_9`O3|AQYsp-oB{@j2WT|I(-N&H~!`6~6oiu4YVnoW&@{=!j>TW`*re@El+AiJuL z=bQaLMXOMA4kxs0DSt5aa)plQX-B?$wb|xqgCWhm?Hm*CH2G#6-0SgiYK@<{#kk#n zob8?$={o+-k${_~Q?|Ub?l?}b)4P$Hv*R`%XyPWlIQ}TpJ^jO_++*IC4^Eonw`4|K z?%tI%W8U{odvMU=ZLQ6} zZUN0XW@vw~fF0$P*4p@aL_|co$BvpHF1B}krnNfhuM$UURd>h;h3Pfr~AcCEB2(+ZafyyB{l7s-ra%eeB4W^v8$X54mPrtNS>{&IJ&9t?mFV7Z@RV(_#*=g#5@ek5t^-qs6 z@5-5W_~k&~kU|^f3!f#=N8C=~P5&Gi?(JvUeMu5dxp+F9_jz59Ub(f=gAb-@B`w-v ztADfTMg#x+{LYG1Nk*a$^&OPn47zx8%p0X4pEEprjQh1yMW=x!m+us6H{UNTeCGXi zt=v3bf_XgG$_V|`MzuC#!(L>RG_5(GXxn@IvfSJ*+Zw|vwK%%ON=ePB3P%Zcl!z_0=uJTYEW^%uO#?*W>Go;DV<|QFPW?JWQVmMA% zEbE)>AdgBE`h%I2xBfrJ6Z+?{KbYlz6qZL+V6)sgBRGyqYUGg;0}RDvae8p9*{Hq~Gz4g%4b`B&pnzPsIJ{!N-JNV+8`*#);1kBqLb6KgMbJCkpNypAzR$zWU zm3y)sdq}U6xs5$dHM&ghaX`_bQ%Lcq7R!vlW2yzdf6U2pTC3Hv`%3i!BmcVTi58tJ z4w}sG)#c&MDZTXjxUzzF-;HM0h|Z+l9LRrc>ZWZtV~L~R82{nsivyOg_UKj8eaYh( z)0^u0+3~k@M+o;s-&p(W+PM8|W`6YaTawp2YeRXHPq5~KO2)3=tfCKW3i#D*x!<|i zb#aqEE!TP!v5(Phe`xmW#pAow`uJ`+sL{J;SVl$Mp$dzhBbX`++>7=0KUgqih}CIB z@5?R@`va%#kDA}?ZEt2{)_3=)jFIcBn{!rrz-SJ@w6UwqXsY?K$KbIHmt|{niH9T3 zqPFHzkE273pNgK^M8C)&Oz@&2!fmys?V6>mr|co%hed?*Z98*Jh&MGogE8`#Fl^Td zkuXB!N8WYVlF5S9hO%4V=L^_)D-#@uuP6{-Y|=2v>=Bbr2ircj{T=$)+ryAzq9=9s zAx=(cTLu4hd)|}NU8akfyj8N@f@cQFj5_%~1$&scR25>@|IUP-zVJBm$;a!59{28T zm-ENt1n$|37Vi6J_c(cDox{uwHIvdA!8y*mDh}*kRh5>N?=s2h+P!r*2i}cx%o=%N zj78$Rk=dTQj;|Zt!)C9J3)sIiXU55_q?U31oK=N5)h`jc4)4(h!aua@TGVm4Obw>ZItmlHd6 z$?rxZC*H7rGR?%ZS6tdfW2N?4L6KDsEWN4ev#(8FFyZcW{)2vtIo_(u+xZ$zzO-a8|Gl=7Kgt5x%#AlL2hLwL|i$327@Ntxq~%Y-K*sBNP&0$l1$}PkI%x zbyn}=BL_!!8aCR6XL2O|`R&p*%anTPaXs5__wFCEG+=*>d2I8-vfoCet8F;(_WY5I zz#U2t3a`ehw);HrULjLMSiU(PwY$G49`^>Q_bH zvclEpD%PH#oxLqhSuJ zJ3H-9E1jmAHFr+jo7|PtlHMAcpT828b25GF3GL|kuiNZCKe_Dcl=$w_st2MTTl^2q zc(vZR|5TfGVQvQ)Gwf{+-Rm9cu!J>4&16~Z`^*m=-h4iVCv)+QKdv;6G~W0&Ct{vyX=?tSLPr19*ZdCf>jzJCR4BPL z@9MtP=Hh$(H{IAjaO10-^X<bfVRy*_uo$bNY#XzOC1XMVm%U$i@W!)W1- zXE8_G6?TbMUX>qSvpl57tgVlp3JxFcJuvJZ>)y;^eeFtE35WBjaKF*(PffkPa^()2 z<91O;j_3#!lrO$HFr7K^PV#kQD~FO4_uZGX?+{!dp05#_`6fKF z=vuzxf|mR4J_SA}olf3vzfK$GGaGZx74)1_Dd5JZT&onDzF#~3G3{5$!PZ8UJazv& zY!r+VX{v0`;F;mF2W_?$d3xwBwU~~!u#&P%YBAw-pl70-gk!eEjH}a&vjg$f@NbI& zt%OWH*0`YH2-{Aaj>JA{Y3P$z+4u||dz@${UZT&FnR43rE9xmb*xB{#Z)@My9_vWE zIR*?B*N9M!cka-^>#|4u9oGRy`dflS4_umY_u{B%i(is-yFC44U!sLCKYjjME&XZ6gX;3F z!rTnbpFb4p-g4%E^{sPvns+kar@Z!D6LNdlvBRl7#!bDcx6Q`b({|ocqila_G+%Ob z(uh#Ui60ujEHx1D+%^q)HTQMoN&}&niGS)U&1)>Rr|16oQ0FgrcYO4cp?f5qCw|oG zIDYd!*WAi=+*4aep48R3yLrdJ4-xC`yqaWkbLdL%S)X_pdUpK!Z1=inra@lEqWWI# zeBjchu}!6mVx2nHpIJ~z4R>d&*YDpR<~e1tf9cjiD~;x#xR?2U*3Ql`7e*b6S)RH7 z{?eD#>1!^g8GU=se}$5I*2ZqGs*96^*@aMwUC#(NhB&2uo%Q^BE4!Q8_|#i};ho>N zPBy(_^V~nlIYjXtrw1{sI;^goE@D&p1WZLij5eg9HNms@I5+okz3IAdJHf@80$E=A zVS=l`nNNhKtSF9%(@)$-Y@clV-A?~7Q5@z?8ads@N%QVK(^hJf{}k|t+38;f97Pgm zvxahpaQc0lBq}ifrP-yjlH$w+4Tb_IPF^0Es*5!;BAW{f|$mHqp7;t*UD7Z|jX#cpqBW?4q-L8h`x} zrBNe3cX&2`RcXT8N^7GNqH@XgwR4gtIeqonm9l?Z=`CwDfw^k*^a*F@DGgKWkRTd) zV4G%a;UmSBvzcQzU7EVoZq$j#H%xy~R==YZ=sj$n=|bzCB}Vyw9O-D;Wk>lGhl8&3 zqYEa4KXBtO_+?3W&x8E8k4|z-`fg>oMCUL$iAslItg~oCtx&)B!3?Lwdi@PCdu~;a znEIbE)PFshBvV^E%w=J$iK!-rx?d~je|XsVyQyui=LfCb`}FGhmy_Z@7036vvj5hZ z%wC1Yy$b7-3Z44sI;~lKaAIx6*+!c@w}A(4hTq$0c5r;GUx}-Y=7BAH-EE79`V7uW z-;ld_T-Ughp#t;N34_zmbzN|3$g_%h{=>E`{5>G*{PqBgmZZ&_i-wIWUT9jWcRqT{ z*#{4nwy0~QHJuw=HLPG>=WL&~eI1VWwC-u^6S&t-d&f43NBWz(!Pn+AI8VO!A!Ja; zQAKk~mrs4&_uwzjIeDXhuUGHkUgfLC z_kOs4+a=S6LtPp}?^f?In!l8{61Bmf*I!B`Wb(f%={)S<|HI>oW+C1 z1CyQPQT}NGRsKx$SMi1ZIffrB@!!Xg$f%GR_3L`D|JNnQYW^tYz9 z{nO%dWMpXX0AXZrkw6#`85|WMiiOuOM92RY8Ww@!-8qrrLAl^pd$AN7ht5S(0$>Cc zh`&)dC@2^bHsuPb2<~9WgYoM|Kk&p~0DOkS2f0IztRqt5-w$^JzzZSqgAc;Dj+MBE zBFs#XJZ_Q-kFt^e^_0dDj(55;Er@>)#1RTDgAw0MxOs?~=tT5(fkcG+0{l`@fcsI9 z&6R2&fWK^n3&tIyqm1{X{NW-}S$Lu&NsGit(jsY)yy3%t(!a9&5+6ahPy7%cY{WuR zC+<>J zyLMcP^U8}4n&axVPEYa{)x8LDIbw2S_>Beb=K>uKjvuR8Eu6df-q{tlXEnNK*uPrw zaPh?@E4UrM-l#F&xNO0lu#b7CcRK~nn$OsCu{y`$r7mOVdY|kH&xv`yPo6S$#lLms zXN)=+tLkwLv9?I^gR5Ipkx9l-~qh1boDHB{Oi`}lWK|Nvl(gCZ^Y+9b# zBRnl^y5H#y2bTWk*L!W*i*_v6Tg_i`B>vYog=8N;F5$0>$1aEIc+i;4WH2}h@&DNo zfvNqo=bBb~Jj4q)+TXWiFf?q*dn2Q7FC0}BY&BZlW?@a}`-Tv8+je;2^xbWM_loj2 zm8vXTGZs7W@Zs&LzE{d^?;lNY@aU0peSBegaSmhKKy)b$c}UEwanIzXh>qZF29xDaq+0*&yQNG)qR-b z>_C}SpV{6%Q)26O$bOL4kh`6)4l*y{+>Jgqs=w~OPcElD^|kZ$naTbW9_}d`YB>2- zSl_B!&PKD-Y6}X+h4p->UF=Tn3*DsC{uF<6lzlgc(_<~Yw*R7W^s-mzC;eG}yogF$ zcY4z+{Q(t^qByj_N^IXqeG)FtuRM{mw| z=^^s$y8XoV=j+Gohz{?bKdo_G_y&jRzmQd5PNHR>Z{gJDd#r`bC;m6ce8LwN`*hu-O`#dHR@b!tEb5P5h5l6Pc$3l4_dJs!~|CBRIoa>BM9h z{YCZsUEZYC^B|)k{J8S%z;j9KRmuYMis$c%Y5Ga^{O2<*PGknd;0rh-zhxe+n>CQ* z)VUiq9`jW{=^XI;oHkfGpJiiXuYj5Z3Z>h=8tEzI9}3dEO#T&itQX}<`(@b@ zUkU5lcuV+|laSci7~L=$lSVQXjl@5Ba_-bnI`H&9IDE>M>W6=QK^$rZTn->d@I>YS z>`V2LO8Zd#!S$2+u?1%bw;k^EfuBC`)nDqHgzk&*4n(b}Hd+O8_?@+>A<&6Te?yVs z@0yJCM;M}mKmOSxuHI4|?2tll#N_~4UvPeq_J@z&@M$OY=LjVF*(1t85K`^iCRHm= zx6ZoM1f&@(Eo2c&0u>=IYA?*EqoE4IIkgGbi_@cXdla<*Q}a;iRGm!ArE&Zu?7%i* ztvS7Lz8=gV?X&X?BrU7d8L*5V)EMN(0VU~;ydY(}HleZ2p|cu(wKb8H4`oZo(XO>Y z#D4HZbw14hd`wXTWJ@f1_K0d<7w);vBgKQT{gSv^kGfp$WgMLn6^mu_Fd;b_n(WiSzA-GmkR573+dg(6`W++0cC?d| zu=?Mk@BmJKnZkD8-adU(*bQsobNxgDskT^Y_YV%m@5|V{X#ddQ$RL3Tqvb!|>y}rs zVe()F{hTqe?K&BWTl7=UguFJwF<)2u-7-iq==Nzuw$1eeI&UA;mOkCVO6$Woe*W=- zMJH#P982uqEv{eRg$}=ID;zJ`5X9))<6`Lfwbt+5clPJ5%6ZfH<&67`k$cBY*!nP8 zGGcER*Bv!aC97`S>NsZZ^SCQI;p5M*+iD>0!q-!Hw0J6W(bM@016OW2vUWg-jbwh!0+p04Sk8q)sK+ViC*dmlgg{anHC`y_%69hPif-|42O(ech@ z!cEF8k0wUH>=bLTQ2)M~|Dxw}9!8z}DjIx^WwgwMZ(6{4rIj+xO>w+ud0gp)4-x&- zl-8en6z&zCQ6k^WaULaghSQTQFj%n2-W~Zkt)^hvidU4c5GW|LdXA-(M^ky8n?p8t z(ww@TaVFoJ%ZcPjHz??k4GLMqIIi+{+3I?&-eu!_8rhrfNYog1dquE4f&(Jt?=uwj zH0|u|9PRq_bm(hu+uyd$3ccU#+{GndgKu=1GtOhJ6?HULcV`kzm~_RQ>;?%I&lC%i z16nPC{OQeRgfLS2a`RiOvasXO9RZxQD*&aP0r;&u5Z(oUNLP@qv12y~z3&fGx@hEl zw%min=#?S)QHPZn4h7 zeZ9`Q@h#Rw{r2#w&wInn64SdowmE;zEVk}Zqx*4VR$q(cAsJa_gH8u)mL(|UJT_Ar zeq`y#LAo!Nj6AW-BXUFf1}_!2MImJ|dn%&J&iW=<`4z8>-8`triip*#cr6cS?RpjZ zYQ)jLwIe2u{WZv^JO8WgMXP077p;g38GiHRZJVjurVl)Nh%{h+A7J`?R%U+H{L~AM zORcWzpR8`mzr~1Q=~`yQx_@hK^lk~%zicmufb9|;6%;{!AWQ5mNBV|Ry6|mh@8D?f z_-*0a&b|+E{`VUo{?6EDZsjDY<_1__RvRICZvH-#!`7NU(Y9N{W?SXmyz7@{s;UUo zCTEW-zVS-i`f2v2eLI3|(w6x!3)y?SSIjXu$;y0h|G z$k?5^6JBO4>3qX}nCJQT+vgs+*z3Jw*`g>$r@D`Z4_vQkJ}bD_FXG6TGObsg#xRDD zRV)0ISohNV7rm8Ej92>nsOS9Mf!h0n!YYN-LY|xTAlKJ5=Ic*iTzTemS?&7E_v1dU zpPtUR=(=mSp1|;m6e{Wkf|RlN(!C7+WqT+42WO0|wdZLAA1|KQ(VyGol~F z^qDLTOgm&#Mpw$^Z+48dUQ)dA*8BXbx{Rs*L01N?OPzA-(q_X7jC4ljW8WmQlLjMDB)V9Q=_@V$OFP(Yk!i~?G-CX7#QyU($chB#-!9C}8Jt44R%($x1% znZ7_J-00rv6HSixL4FTCM*Q-CVt8LYcz!YV)vCEH8~b$8j2W+HT2Hf^E%fXFpXt#Om;Fhp6+}tA~t_4q|>$)!6O2carfY&$S!6%&56} zpc`Yz`o4vhpN}^EIyY#L;gP1JX(?tp$sX++hrLwTs4ZNaDRiAZ@b1}Ug$^3ckyB1D zd@z;&>eeZ@Td(XMUMaYCIrr_vv3~F8zL?pNvgdc|j>4*{r4IcDcdGU;ySl6Kfz2|} zfP@VTB0jwgoFnq>Xx<=R5;i4_vVLQAKY6j@iM_u)KFUAQqhX}M)U<~cmcM@Wc;p|a zwVSDaEAWik?rVeCEPbBcq+iq)Zh1A?QJ3Ly&}KSYv~pzq=cluo%LhDAK6*B?>Hdi7 zy>Iqr?cw~kWbmAGzfo!4aXX(cEz7QQ+!;JqPhH>US%Hh`yMsrZdO9q9t+q4ZNx-`l z^ObDPo&&Z$I^ph7xWlwv{{hpkhx0<7Ec9?HSYU6^U9;PChEML;{6X`QBBHpyJ8JF5 z^`%O}W?vheGh8qu;eWCBB>+)XUHtdWu*`r62nwzYE(xdz3MyLq22ep!5m3|eV^mGulf(l^L@>u_rX=^7Y(x(e{k@Pt78uK<=xY7 ze)s%*@5M1$XY6L4oj-^(R9HOO*Qz!pyZ_=BMh6et`Rd#=exH`iI@8JNdzZrUemf78 zg}mV2#dNG}<(;Kty4?4^a5<~=iEfIw#%(k?$1SPzOMOH6!J2Wl8^e2h-k9`hpXa z7|*&SEPm!&Wl!7XT{>=D(gxoiwYk%XY^9R(UDD3@s8e{)=ppJvZnBlF zr0!Ka(08I-2vqsOSV(wKyHxeE$u>&vA)2(Vhrhg`iwns}%E~OwuPu3Qvr3~~7F5o^ z|L%bMKL(e*uCR&?d2hp;Pr5qM_d$~87@r<_<93RVg-*fUD9rn2OWqluyo!H|bks-K zdEiT`?oHBBXhz~pm@?6rZ4$zgXTeKd0%19F-J1GZFjN$Vkob$gzV54G135 z$1gB2s82xu{{Fr_e7cE3SEr_G>1&ZbaA!7awk9$$gB+O_E@?dmJ98t%!>Xp;IXCpg zvAGe4Z~OHkIguM`PS~_NU-)l9gw!*oQ|E_594>Cf1<{a}TDH6jB%2BJ{*4Czsn05BmTkl_2w}0igAFTB8C9nFvp1vw{Zt=ONV!JpE=v9C1)DIuLx;3Niovi55&un~Y>giuLX7#vq{n*$Q z+`x18{oSU%`pubB3t#lV5K$ia`&5tfD}T)!{&kqg;;QElIXRd2Qm%+!I>qKr)P!gK z2F}@Kb;Pi{^TO>T99%9e2)bT)VXecj)2d|gvLi6%VkS(ko>KQl{oeApJ}FmseH7LD zqv!J*7Bt&TpNSv*>>6?SulZv}P``$tp`t_!_)QMGt zpJ%hXT>bU+%wX^K=bjie>#Oj{AAU)hrab8E<9xeh;oH%ZJ)hWieCvhN+zstZnI~s= zwLZXa(>?Lcl$fxsW45-rF!IjP6WdKsRPVIzQ54>*-s#ZCVMnIr`F=9}$!Rb1)7DoU zom1z!cg06(HpVWuqr8<7Z}twJ)cthtg_^zJWG{OmZnOSaXRCvKey_~N`^LZRdgpM! z$@ru#7U{iup6oVy(F=y6EAQ+rJYHY!@%vL(>__IcTDIxs867Wun`n3Fg~iAB-PHCO zJpa2nQ}=!6{zA9jaj&d;^4a*9_D9k^etfxTd%EKMTTfrc+h6HmfRp9Du{zI7*c&Ta zAj>E3ziV&o<1>)dk^Kkw3k=X-eDtHo|1f+0Ya8L>&UT;7bzCu6^UFP_wKwnNCa+1! zigGyfmJM6-+}&RnCXMbLefZF&*n@LE9y+bpGgsS2_O$SI({}9ea_19jrxzE-`b_@7 zC)oa4O#ZT{pjKzc>>i}r5%|OEtzT#QOnPF;=HYhbS8k5X`*MHxn0`;kjJW>v$mjbl z>yi~aT_2qn^}$!G!&V*m?Cq&1W1Uhxj&v|SuiN`m?7a~OdUozUy-h|3_Z9DtPFNlu z_rcVLxo%Hgos_+G--^I7{P>ghuUieBo_ljzsq&TJ#lM%2Zb-ef?$`%K*XMiL_+C9X z)=K|TzRJIPUZ?FfuYLLC&wYdUymRCGgne7M0~+<}d0*c9r1WU;)IYU*owFR*b&SlZ zKziKyKimjElr=rx2sdrAvwZRjY4@(7Z;!O#v&yA$?am*me05k}y7H+vHg9xK+fn=W7dL%B z{mSxu@%=lKX0`X(aq#f8qceWG`SaCz`#vvvv&RMFC%>jdO;Yt5za+TLT|?wwzPJ(7 zX2qN@cwW2y`)6HD&l>;a;{R&*iKDZA4|FJde}C?g-0K4uO&Qkd`fE9(^bzd+LG~^; z`{v{&ep|dN<>KcjZXKJ?(nk9PcklY-SHCLXdgZ0bo}YZwU{N)DL%&fy8z#Kp_x?wh zUhKa&E_&T}llYozCtjPj@-L*MUApyM2WfWoxRmhA?Hl_S$4~BI zKi@lG(wt4{B~~l!m$%y!`;O!Bb>FlexlVVu{L6RBe_yopYSoZ*^-oi?UhS%iJ-0o) zOWM9`RqUsqeXX~w9(+4-=Po7HdhPw@+GzP6%o$Isp0pSJZ|A!YYvy%gS#HFxgm)dpw#;2>Ry}U8>_4Nzz z^PAEbF-VHQxBgEx?LsX$-@af`Hc|zo+qA(S_}f~ez!z-R!oA~UF@wvjrzwMc5{yl( zg~Asn!HqA=(6&}lh!5nbn9>^dzT|uE`9gEvLf@U`<*~QMPw?os?-#pP8Clm#-?&kj z_WSW4&(!ecXJ0XW)^VtRKYW@D$?tf_D3t+sP;aGw}syUgpOu_^EUviSJ#dFBKOMFF%ltWB~b zcXF!2ZGVrs<=+;}iOhb#;o$lM8$Nz&ZwIHBeRh9v>c-3daaOxE&qnX6xN>%xTSvFb z(>p(%^rv$>EN8VlxMxfObDXlxVfF8CE`7@8CC8ZO?cWc(acgSN&*%Mia;NUes)(ze zyJu_bEIyAYoHXzHJAaya?C3jbm!tP(ZXMco^p(lESH8;Lx?#wGzA-P@?OPDyZ}-id zx9=C92-|R~;^!U3?|!lWg<*T@=bYh-ZjD@Q|Ce9lUZ0(&?PYr>^Zu>WB;OlLdj919 zPQn?RlzHcBSDjiO_T!gte7kV$tnyzk-*jI=d_Pk zeD(GqlWMkWmrI?$uT53iJb6LbO~C(>CNezu={{l5xuj+$_6beE%LO?4O3WCn85thd zr=Pcvoy`#Z$6@~LoD95~pQZ5#@}#q{L4An-;MCvjvo~)IJbB~57}+v45s8^l<|HO~ z+u7LJJR=#cooZ)8pG3uG!K{hqDNWbz@@o!bqq=vsvk96Ro0*CDv58sI!^jDt!H8Jk z7uaJ_TCSRHXRY*#zUe z!Nd%}PLDo)ef#&NPWSfl_Vx4jA>5#6V`5XYr^Uuok227`hc#;kjQ~b(&Dbby#Fzno zLIsg&*%|SP!O58^nUtD|N_@>qP(pBGW_*v{nyy`YYkIdVLtUXVQb#g#2!VhdprKe+ zGLVf=(u3iy9+`qg8IlqZLmB(o*)$?*c8*Y)ccfScl2FORQkyjgnD7sF7x44LXlOt! zCs8~i|a3W~}g`NXW35w8E z*k_tKv$ynYUW1f`8iJByGcX|dARpb2Cv^DC^9596gGNM842_7IpbfLL(IzBBBu;Ot zd2B+m5D5yxD-byEZERDDNlwf})tCkXj+&AiqX=zwHeR9A$kRR2{rjWM&=Jv5lf#Ec zhewPHZ))6YOkzTEtd!3;fM82V&ZNlfS%ep_@T8nf7!#Ojp3g_hW}cQkYeH(~?8JDQ zvk7)K!&779W+nzF#m`3jlpn-HF`D3zpl0Z9>XSeegz1TiI4;PTlo*HaPZE3M*VX)h zL87kI#H_xUr%hxbVclC&+!Boev(giQ5X<$}^pl57&e`}pd=7X{SlV6Ek~j zCd|gTM#QF0Pb6~fnhJ#bqbqVv&GLq&&25$!9{?kwS;wU=68e%eXU~ik7z{crldr^Y zh<-DYXA4##ni9=u8w^p@dCSxQJV@P+9z8r_?C`MY;E2$P!y}L}3vYo**@(8}Y{A3F zXvYReM}ntBfd(=%(lUb5QnNDBW&$1}d^zLrh=_3!(PPI&jG-AIwHpWykwN6%!?$0L zXQgol7cob=JVr9+duxQ4F|h6(J3Wymgv0=Z&jEqm9=^Uko()aIu#U-`?wv4m=8zV( z%b4%2p$=t6X3vI+4ICs$_0T8azP&XA1t21`GVq-^k(R^-2)6*ZtN4hjq?L zEpU;S+T9e1A20m<<3E*1Ztb?GHQTBF(tS4<_N6;sc*@kw&Pbb{5j#s0ADfyA3r%r} znvCpJO>(LxcwD4r7QQj|wzIS8E>(RhcklVNUCw+UU%@j^ZeNM#;F(`BPw^!h*y&kE0Y=_`rqXbExnaQHVWv1V@N(_-bctE?-tRkZGecS{BT zsT8lFm(9rluV7_ax>qnpNRlr~WQDkZtaEtb64tJ@BIXdM!6p2}N~F5XV}$#aXeXTR zbg=D9h>wAJUIc6`qTCU#gzp#xMS<|GMDdUKRZ==|87SnT@M8uEDs>ej3K&yfZR0;t?m{PKO)n135e}`|)s1 z>y4AYG-M=P5>AD7$)K{?k4HYU$Nw*HeSjSY1#fg?4z(L$mbX+H{k1tF8Wwz7+@NH7 z%7-`4g}lC2yz%NtuGrd!EY%J@ZJxj7y^?XqbD5&{kPVAE1h-G_OlOSNuxl5TtfZ@g zMa40P0UC|TE=IH8E{>@NXc+Dp-LnB*86SW!pe0P^Z3ODrHKiqsqA=cqMY(Hq#z4kh zJEUY$!&>pzLCR6N2xH*-Or!ZJtUE4uUT1WV(HUbj8^RKB1v168t|n7doWf^-1Kd4n zEck6*TWPm-cMjb4M5mx_?o6R?EMvj8#kJzLB{(a#P3xrG7LPP4A9o++p@5(^sNOi{ zF+fv@`$D?c4A9KR{cO5-rTbR6Z}rdio$vtI3f!-5s3)8%alZsO-2|MvJFt`SxHOs= z;8tU|3AlAvAr6;@A&zN89N&u`6nD>U(4%lnc)cLum+^I246i;n1GP*wF&sL2d)ACNy0JV8n`@3cqjbJWeLZGYr=U9 za4+L}6L3xKP@U8!)xQZich{khxHN@`E2KEh0Cyg7xHPj7H=E*I2e@kxhYN8S3)bo% z#n~0mv8EL_mg`hFR)I^2D_X~{ZM9-i9GmuLKDezqIc|_jtuA1k+t*(0+H`8)*3quJ zdzX%`&JL|SpJ?$jCvM#H=^I+{?)&X{cV6PK zsBNOp12;^Ke9IKXP7mv6S z-+YEEz8$Y9Hl`?x?{;Ixh6Q_sKHgPwB_=%I#vHh7QWW2|2Mo%NC2%2D0XJq9a4lBh zgg!arsxfVdxC{P2!;&}7Kp4`VH)L8dM>kHLQbhF7nK`PnyR`A+oNX!ZD2_K?xo_H) zf-nW40QVcbv9b-*6YSLr-jLmaxvD#{$eJJ)uz!g1Opyii6Y>p5zVjCPm)pa267rTR znSb?ryzy3?*5JbNhH5uvr2M;FnX5_BE@cyV=1qIvD3s;2#;R_dzVM)>(9YiZTElTG zHa^>hxz={0vFJEj?a0P^ake(moO4Q7Yrsx8b<`RwQI6WX>%tpvqrRFTKA;--SPvIo zZC_vp75Y+UlKP_Oja5oD-d|_>%*FgEDpzSG!k_@YRBb7}V+qAy3FP#li=xXsIoE>F zMXItQlga-|oZ=yIa=s2t`3}l)?VIAx%W+PPaSaQO6y5E{8;vPJ80$ccxwg12@~}V` z)R!csvuz#vrAEJN=5vPRexTulyfGhrt3HDGjhvq5OpU@y-=AXwF6xdj5vRZn_2YIo z-f&ClRN`sJTu-B~epYNJWb3Qd81K~`ctfs2m9iYP5Ma&pM_*)yCs1Ft8*j7#KDyzm zQaX86DxFJI+93sPwM_41#|$Aoc;k6xTLDj4<9D4${T%XEBJMTJ+3JqKsU_xKN5E^V z=vRh%)u`8vwXH2gTbP$6l$Xk#X)vvwg0@|Q`WGa15$Vy3>uCE7=VBwn8EVa3*MN7d zxntU81nqx~`;UyKT~~k`q8q|hh%e|5^rPnZ4?}$=9DuF}2X(+%es?^e zn>VN(&>yW=fg9JVwj0(%l?QL|)Or;3LR^U_ZwNs?HD_Ot%c*OLrm>P6b(kyHfbWn) zBL7v{f=;BdD^q~)02b;qjW6}@6zJ+!w6?h3$!t6&c&n7ICG&2YcC9$bBtFo#aW1b= z$h^TU?!27Or77PgIj&t}T*HFT9?1uY9v(ja;(8#^SR!w{2|A!Y2+ucpBV>MoX7_H= z8Y<(NemCetNCQ8->%0q?$p`Kq=%x~}MJ-bnn~Madfi?2fGi zVF|q3r0AFu*Iwei#P5|Igf>fEn14HK-avDQc>4jFKcg*>bp^HMjPyFdNO^84oxL_F zTvGBNuYoUI^)Rc8S1^AWPPwd#IURT7+SkU>m?OVxpE#COr+Ix;S7C!Gstah^q;O8j z!+wO)J%9_9Ei;(ISS8n$&Ks=(qZKYoJZcO2nl|w#|?EXMBVazA@C%? zV-Vr_iST33qEf8c_%emdXyV($99jCI)-1{H+()wJT85s z3VC$8h_AP=uh@#YRK&R+$J(jywTNGZzE!NioTC2aV_p$&BmTYs^9QnzF-EB>5XK`% z?2i%S6@c}k8gtV;>0~ zoVm$WNN&PVj=e z-~kN_J{Hy)8owP_H*P}yoYa%KeheP6^G_H0Vce47in>zi z;?)DLoReOGEY^+f%I|=+_Z4Pb-i9{}JHXO6g3h@rpj$;*vgG zaej|LJS&O|v39?UwR3AP-heg7ndAqdtsk1&%GWTzeAMX{%hKI7EUE-`hH<=MYcFOP zZOI$nMW1*z)(OnPq9A4nb>t1jJ()42EuMQZW4@Ljul8l*ljAr;l!m$PL?1o!`0;0R z3nSb(mz1*3EXu=@jfea^BNq33XSTB!F7SPnlnAl+xEh#yzOeLoPR} zuvZ{DB3W1mnOP_&&DmtigLoUr`j|7uB+#)NXH(0Ao?@VP^g`KQs3RF+D8k0_yQFqx z9w0qaJ2Bu#F854pv7SeC16(^GcYK63B}Rt%fK*moPs>%=(lgPvS)WK&Z7eIN$z?y1 z@=9eXub)Dk!@1x!mCAM^zmVW61P^amaJ1+y_=*wpuWA4FS>wLk!IG8QK^$L)s~T5b zB=iiDZkG7r4e-J&(4LGxp|9va@nGV0Dm%7wgBAP0!ZGU3##YdY0uDK^jC(3W`3dj6 zfa@gS9Q>u0Lw+yZhY0vr*XG%>sL{YV;d=&+A>l>p3*lFbdOp2tS}VkZZp>*(S|iI< zpdX@}#^(vVQKlt|i#aFZ5c80D+QlHIm-ti#=BNN?Ay+@J5;C;V&aV%ypHxp{x-=Gz z^%})-O?rc32Dbo@Odk?W|Go4g@J7PZBWOg%A+1}~Uc-WeMRk#) zUWRC$Nx!c(B)Ct4A5-<1JGYdV@q)aR~_g`h{GIhNn`n~*iLi0qI9`j zOPX?TK~ozgnxg!VqAAbEG}Vj;{WY3ul!2tVNi=m6^QA8Gpuks%uIeK9iEhKyU_F-6eP(E18JOBB(RQ@S+wI%|!;OZ!;J!qBI*D~VAI z`x<=OS)diWrhT^XJWHH=)k~TF&x!*9HLIcP?8b^OqYbK;sLn;@l^BAqW@o z5qbYF)N>T{4BD6X@R>^HC&S2Lza`{J7TXf;+iF4o>}rg{qH%7NDVb~OChRf0LFRyr zOYp}+4^)X~p$u?G`HA+0XSJxusfI3VLl;rjB%M@#KPmquvAs&9lTMlT)3>p{i2Afc zjr`nE(yuhlXHjn~!%TZ<8iU4m9|C(J;3k;!F-}~2-nanzu0MhqX;+=lA0f{JW7bv} zGtvc{(+KH$>moVO2=u%0O6Y(sitqLUttgqk#~tXnmvSeKfU|!zZ>$H*#tU3AbSD9B zmMq<({iONsjx5S$GuAYR=6s`4;&U}vo0i+Nok@_xA=}kzuxB+{Gvfv5zfDRBr(+t| zkP4>KMGy}oynN)qT%mLFy7S_T5qnN-%h*8seT+N!UpaI_<j>;fE+NE@AkJ`~;Z*PR`Ah(^V zW;=JP>IVwAJxu#OacxUd@J7;sl|fgPg}op2hyq*$Uwd58Ta~}8h_0^&9`-9#wRt#W z(=CavuLA9aye868sfF1(_cGu)B-UN6Lkvc#uDmP&Jy>ZE-V@+Hj`<2+30kJP5Ho=Wl57wRAB?AAb+ z^b_FUYy@5|Xp3dM2ymSt9Cm2LyATIDlxYCZSDmsSMwjiuOI!uI1fA*iM$_86kmZdj z!Jx$;&}1NJQwurhNaMW1>j0(9>_>ZNLFWS3qa^b^pwF~-tC!DgNRFcXxeAHyivL(f z5&H2Exg$o}w^MsGpX#i&^>Vun3-&%RpJ*;cpl_xiOYmJ*veQMHD<{D>K)<^{zuZt= z1@zortW=~!!kH>|WB|e3za4l0#tiRG3*-G=y=iTh6%%Q~y^)8~KCdD!km*4a6~wcM zo|9~_w|8PYoi-vr&Z3eAu$&sx%7_~4!i)|$yGriD3<2oJ{5TenGmQnH-R-Jxd1F_^ z5pB`_fa($AmjhptoY43z=@1?$C*eqlx732iigJ$%d&WCTi-2ltMNXB{LT|LT2q0KZ zP8I=17mEOsy&~tPEoU&}w6RQGJ{ zS$&t&(>dRA@Fn4_Z;=9Le#&CEJeE@g_^RLKa`atw6{Zy5dVM!3>@I~pr0@wT>?wsF zVrYtI##_1VJ&mVuuK0{HAgK#)sHsEyK^FSz+>V~WNx4d^(3`roF0LlnaA2xh2f)?J8dpj{;5wp3t_DCsg8W2K29g6}0={{1(rl+;C3u;Q1u& zgT%EH^tg)Vg8}&#>a zp^aTwgPY`o?J7w=5Yl2vb`a^h3iEg)<}s~3a{p=G2>d~k^NDS#d_Z5oo6R7c+d^Nv z1hToCAL+;;r{H{A<&1L*dPW}bZv({CaUAARw_JSxKeLB=J~9l_^``6I_s84o<0!);DanAZY5$#|>0XNvN` zT$jsNv!;7@(3A`2Ii1Z?-`XOi{*@q4F)r#GpSQdN^^5XRo;ox62DrM)4s@Zt)pn0- zP3Ll;$#Rw>=x4WkI5zU=awpD0%nLpy%gix&pFrSWh->6ul6-@A=e9e%hE+i{lD-cXslJo2IoI;E#Bs3FW9CWAPm@ znbIY<2s!vNn5;sJUj~Q5!s|4_eZnk8Uki)YZe>~!U$BdajVdD^jSMFV@#p z3I|JJh!jqf!X;9;N(xJ)aE}z8kiv6PcvT8*7KrsfA%#PwFhUBaN#T4cTqlLwrSLN; z{9X$0OQEwg-d&_{kQ7dl!Z}j-q7;58;bEQBZxJfSG#wbv;el@}hMIOl9P{Y_^TwUP z2znyX3mhM&k2Jj*tMtZmU%c9ltF#}U2Ou1X5SIq;#LGV&l+E;`y6LA07we~c`5k@* z?kD4N4aSAK=qIN^N6wx_;Vh(OA#IRck&x?O_{R(`^I8W5N_dgN5Ge$26o142T>!}q z{A=T!rOil%*Z*cn1SBB3< zB+g8X%}jhYHe=QRzh}ja@<%$e6XFQEkwH;m^bJBdUNn-wO9+S3CL9Mc!gKdosg$p9 z5QCEdISxd^i{ChSC(}0t-Tf&K@GX4K;O*lRKW#cb1;BF%m6<**IRQmh;u&%SlfP>q zn%Uj>+$6_bmKiw{9b{DgEpxi>9G>eMtY9R6sa{vN|(NGpl=pd;-E+V zj)A`Oc#`squnJWA1>&%WkmLH}mQ^BdkfJfpkK%|&9>gKLR2DM@KOJK+2!!t+y8GZY zRy^x~Gqfqi32pRa;VMiwR+V@0LJaUn@{x)0uJaksstPJpN1s1xQMSbEVh4_Qz1AWw zgbR0ll0!ayh4o1xpU1^8)B*6APglmma1jkW*#iPlH^ye-qGyKZg^X9tqxzP1y|{+) z4lfD!yl`O~3)gH$wyFx7qxnaz%9fQW3J@*8_bR}vVBxN1lrE<(zbK>?A?rwE%k4&*CEBz=I~Ol92AM`xwe%v6d6^22Fd8q0PErV(5aQ#d&I3!sn>bO-{4KxZvk1#)%m$HHgvR zUMer-am0f?E)^~{E||Swwz%wY!QBaKhl}GBOo6L4?yV5QNCG3f4jjidz+OTFweo)3LU|tC>;Y>jgZHLx{9zI#88j$Dng5qNC*FRMhJO; zbw^12oOmk91s4<+qPPz%v!f;rUe!W35nrH+V+~LXsDv5R%N0&0SKG+^~^d;7E2z zpm@j)kn=f`AC}>nWQZ?W97pm4#S^d7vQpvOu|lMiOcB5WMao3+$G?YTfX@6H&v!bC z=Q@3BNEdB=lHM(;c7iU_WLl}p>#7f>XIo|@9b1hH%UKRRN#zBFzGg4zD)dx7| zk~rbNlHio%OR>+T=M{M8FT@f3j%NY3sOLD^2nAfw?FJ-K z*bTZg&Nk)BTo&K~-LA<6y3pw?Kn?wd${Fb<>taf$wBE5pGv?=8em6)^H)7;ky951Fm$zS&n$_M)_~z9ix`@xl8p4 z}*fHqK9av*d@3g~p-#u?)Xyi*7L zF5%CE^Ux<{Fyk@YhjQN3->a%TAANEl}h(BrVun)t@NPxu*yB~-VEtCNKZogDtR5g z3ce-(R$}kvz=J1e$uamY82d6E;z(|ke-(&hy!_NWe7wIG@$q=WB8$4Rz=@O6Y(UT!}k zF+LIYc$x#t*z-cJ2k;}{U~F(`q<7@xNXFi>jgAhAmTaZOM<>OmCd^FCfHY+>I#JqW zho+*3@hRi86SET&B4;P3k})8BL$3%;3#6?%`JG9IO_qe2?5t2KHi1x*DA>Ya?4;!w zSYyFxq=dsLMkb52oiZ17W#qsvOjhEo(9~&Z8M6dqJd72y;q$cd>DjQJ6bWlJXf7)j z#*&85i=UaDi9TXi$PJ$-!6k!Ymbw>h0h@7&+SCMsQf$$nj!e|X!`ew^P*Ng5KY><) zp+Lw&4>JWTLt(J(!dMQV2kwKiGcsU%B`PT+F&3MWSi}w|gDFAqcY*<*N%WYO%0?(d z(~z62e2j~mkrhdw&IXRAGk?p`iIZZJvqq$4jDda1WYCV750t&gYFykj zqCgOBBK$dv36M2`Q<-uJpPQTTA%oi3TM_0+)D$!`Ei*Ah7&-Qx$k9WPu4A8=nNE6Mh+swhqq>OG``1 zo-IWM=14c8z~#24)?^T*`tAbmCfqj_X+`ZMW=XX4?t|qU@c{`PxQJ+u(!d-EFRN4$ zSu@dqJc$N2^-RXB(BrYdQxJ4Z8B&W|K&_LL6K5t2pO+Y)P4;RS^T5c{yrL1>0E!FE zM6H5(FwxLZ0<(t|8JSA61>zvfGh`cM74icNad0BiP-AK$ECXc_@^(}2B)&z{bW(Cw zQdI18b_wy+o_XBzX5%RWn+&)@GyC_8OU`N>k0P`OiloVqh?$BB7%A9A$_c`#f#zK; zLer+noGl_TJsT#l82cVA3U!KvNw#R%z$SW*1bxfY%+^0VTrMG z5?jpN3h;xV*x6*7EVB9Nq-LdMrevhf^iG@y+yt=Ei2|nzhqWYPQkbbhY1zaJSco=^ z?DsSmgC0NqO&H*y#33yW(>2cy-3lUn9ulv%5cRDwC=mvwS<7FT(hpG+adH=pzr$Gs z?n2o(n7fHaIF^mTJ^l7s{C=M_ZLNhT(fo^aD+CtIf>HHWW|3P-cZB-jb{4096^|WsSa`$PyR18C`C|`+fOXwP;*$KeYuW_LagkuliDZG zx#XIYegmMP=4w{^91yYv7thL#z}Axj;KE} zF6c=dg-7*E9J_FoPeYB_zia-#%3l#ipxq%^>u*r zcK9f=cCwC0Oqi9Hn&8_7?z<3U{R@*pIdA?H!tBH`?-DW=k~Fm+kcCn z+lBCRJ5O*h^8c!z+fRMq#!Gf-s`PDTwhh6XCT35zzO5Tw!7Lf5;MzXmkHioauY3Od zyQS+(2Yokc(WuJ1gI<5d;l=8XS@mCD{yp_{?*Ao6x65CDdu+{}5iv2bia$4u_B^%i z_K*Jh7Y><*_L<&(z|gGMyFD9t=lifRPu+X{MPqQ^zAno;)c@tFes4{wykxPra)bX8 ztLMvx^~jxYD(@M^3xW1KBeSaKZ5T0d_D+w&!ToM!Ofd?8*3@;y&u4yk+4G}eckdc^kNAFK)^q0v zZ%cV4i5qScxhcVU{Pzu?r4Ak&UutNx?8u(CZ^v)1;f8-^^@`20pXyJJ^s79-=+(sy zU%OZZZTRFmKcRTz2XETn-ZIW^+K|7DJ7PP}ensRPX>$o4Y^xf_PIPp85^-NP4mIsh+kA?sJ&$m7JKoH747R9CI36xko`jF6K_GXj z>PI=cJ-wxR`mB4$KN;n5$EEG(5g!ho+VElW>h@z*OQV%bPNyt~quZd@bLS+taCAG+ z+jkIr23qI@N4Mo$^1k%RJN9q!?<#quwRJM{NUNPi2VAjcj&A?J!L4=>c_r~1(I+4{ zSo^m-xCQqdr3d}2cK_n~>Eec*LkG22x!V!b+ibWxOc&;o zu(s^;d#g_`tm+u+e)dSGo2RC1tx@&-a?0`vj)&W>FYSDP-{|)|9f$m8X(#`!!9L~R`dd1_^5Tb{zu$djW1!o}72mIY%YVnAGZvS(dFAIl zy)u3r8g**Jm{GMy9y_+>IC(SN?wsMw+F?V&-MYWm$D#8J?;knh7;Y@K?{VF-j6e28 zV!_m`^!u}uN_5fn9a1hoWz2E5a=&A3y7uR}@4j^2K5a$i-q6&qZtpKHE^D9tTI;@h z4(_>B-tVpCP2(nf+^)xk4`cFI`7LdIu)w*U_0R6ly?%16Lz!*;Z@--{u)Z+;Ud7x2 zlfV6T()%y&n>FxK=9!xHv%*~tKRq{OR%quzv&NoV-0z85Spm0R@A`GtwR@F5$CepQ z6=N+Y`ni8MGS;=&AC5&|s{Q7RgJ*(nI6glnCGO0;noq`l=y~cBw{L$N(dT%*_vW|- z(Mh3sV+^O9hcDK5%2u8A4Bel!LS^w_cO->Q53Tm6pzHBR-| zv8}lCeeBrwU$FCi?AZ3PW842H9owFOV_R%gIiI}xjd}lb+ldGK+3E-Pzy0wq^A?T? z2wr*d_wOvDo-_XTU+d4d?K{rq{%n2v_xJ7J&&;2#6!B<(wx|H#D@p!rJMM4tXX|iQ z^k>_y9!_rmmHuoeC1rkW@oZ3ke{Jtf_iEMr%_q4#;p=zo+%@6k$N9FcqQ`u=#BXl) z3+0wtL-O?-8K-;rHH7O2{1EuX&tI|3HlG%B%2;w>#o<<~I`(XPPFs7SNXy^*xc=Q2 z7k&3>uk8E7w!O!mo4t9J%V+z8mo1#TCHd%qwL!xcZ#AS9XJ4LBQ?~y8;r~8=wh!65 z{xkk;S6p*D;e2=MotX4r4+f74NpI~pcv|1Xb6!tx{n_P16MF3VV}G_Ow|-mJHRqSa zygoaw#ozzXa{q|-&pi9ia~UrVOPcW1@PKarP5x|KA`bp+ubmunAeH~(Px@y=eAdo= z`_i{Z_WR_gRQKKV_FG%dA32-fLN5Si46}WsU%?5hPJU?>g$>I(7X2x;bN5}FKI`{o z!kcABlY{qIzIFQJ4;40Be7|~5_w(Ri``oImIq_Y%^^1F3-?T{@o9#3w?ulOdvNcu1 z51+P=4RxH{ZqO(0FKvx`BHe$@$nh)tTsf_OYwOL4`m?KxHSg?1}?X8mbZ_`p~9ZPgw>WLWg zmwuad^2pc22W6-irTP!u>15$@VZ@0a`43WD%KbKU-TL{~C3AOonDg`Ji@vK+yRO_k z@AKX6iyeBp{(17qgzPBgk=5-;QdGnSK&bax#ke+h$ zwh*S?G$FkARqBn62!r=bzu6#!X;QfIhWI>7cqTu6)Rx$Lyk<09^w0N8QA(^F2mgE; z_~+})enHH8Vr(V7MCBmd>(FiS9`E*9G5o<#4p}8q@ut+!C-IHw-{GGxCWB-7sHYmC z2US?g301K|)Mta|7daN*17SJG2H_>dm_NaA3*~#%&jR@bK;;Zz=K%GK*&K_Jqs&6# zyLUWC)#4BJlltD`jV}$FmA_*b?M{TeUg8^N=hn~@-2HHA20joBU}XsM;M0Nx=({l4zOS7A0rL5F74!KbFZthtoy;|$^wWeF z_L0yhW$@-|3F6syNmyhI%Rkt`wx8VKk$Fqf1v3Q zcanU})kP{WFO{%sYJoXsiMeJ~><*tn_%2%b-kNLy-&86}ZYh=URi!Mkz&EG#JuTTQ zCjVC#@cnWm=I;c|-$|IilQDmv!Tg6`8Qun#Phf!*3be3M%zJ>Sh7fi{Pu z?O?zV1Xu#W6ST#EP2eB$T%?d}LkqkJJm_Km^(gHh@vrwOV0oB-J%Rsi3N;{v9h9ow zWSkmBubqDc!x~I#*Ji z4q~0n>vOkhUY|m!Zyf3iZ&4q2$;4E*1Q>-@)^$wQm zZBf3S`a`fu<;zgM%&dF~rBQj9O>ia{=o{)CkAlai2_7@tc|EGHFyY|rz<9$4N?nB` z+kG9r5>xUZ_Jl9vjgI&(-gidFHufR;5%%xw$uMz_DI2kNIl!iW_%bq;qZ4fXX{1ob zj|1CYK{VpCNo{Zd4Tc8}I^)nf@D#!0V1@fXMV)f9Vb`t>+_26;5&I$KtK{6iE;qej z;V@-Xz+JPxQJv#quk&hV-`cDFSFR&pj`%)9SU(+3WUYdq+LOaTr^6KRZ3`QMgew^i zIlO{-@z{Uoe~2@lJKgzt9=LJ|PJ-J4@L2xm;dvAsnD-U`2)^SVg=Yz$j~uVR z72p5tcuC{%sDA&=^z~1UXEgb~g>BM69prs8+INR-*=W$bJM5I2$H(HC;$y`!*Ji-) zGRo>{Ud_6MdkyL_!(kQpJL0a*1i#e4uP*NO@rvei?@@E?QDY>}WeW38tlYWqn{}UN z&3ty$^XIpe(KgroC)*0GhP>lu!P0f0d7>$@2`cb!*!xb#`r(04!Scukx!?Vw~0_$EME6>OYB9`d@3{Sj<{2|o4|2w9;n z;v(#okWG`CT>eAV!rW72x1$U=fgK!!(5|Im3)PGAoQL0@3qed@1$j!SS1tOWp)_od zo#B($P${Rim+aM`3|quSHY_**b9_(DDu;H_4YZ&g;}X2;Tdk(LbTPEZa=m2mm z)DPRc1Uuoz2k@(47ri=H=~-O>8|OiSFJdF%2X>Wnf&d5EJ*WEMf7mF%1Nf>Si;_(g zs;3ghz*Mkx1L?>ByFmsIlp#D8BdoD!2B8e_sf6vG^WA_))Ge1~9P<;>fz$JdCpouJ z>U$w<-6zpFn!zjN#kk7lg>>LYw(A4`u3kK+Yf}U>eVluMeh&F(KE(7pl%tT(s;nWImQF=G#)(i(Kt)v(F|Up{pRB#&}+-_$j3O+ zc(kGMz#P2_+wFhPczloi(s&5=cFg9-_D9W+(c=6lH=7^Z|AzU|tbe|Z^Wz^M4}8!p z&JSrkXdFUlZfxc}$@WzZ=8b^I|InCh!=8-Bq~IZAauW3AyM%m6Fz1E^9|}CS(wZwi zZ?6FFRf6|g6qCKGN^9k|^Y#|oF1lE5yXSSX)4}4e) z|EM~Q{SoAI!X@~v1TUmAPMoJc3;P+NzV_sEN#wIJT~P*h)1WSPru`!2b?adXe@^f{ zBKa0DhlkRgVG}GLby0rUM!%+kP4qGg_(?_@%WJL2T37A?pWf<*pUO1inoxr8o_F;J zzXx2-l#k==#e4_Ee56~Typ{J&JB9REd%X@ew}iS{vQmsX85Y|csRZSMSMA;&LZ@~ z2KkhPyTQsc!(fXm1U!!Lk`I}pH~Kpo{s)s9L^x+QG{IR0zx3ufrgcMv#~{oH!htGQ z#0}|h6jrc<7R2JdGa|&;Xk-<=*-=AsDGv%pQGYz3v6PU{tM7-s5{epSn>uFe0FZeIKro( z0qM5%j8BB4wt@EdbFETFZDe{+T$>@AVvL3TgDw2^g!F*ze1xI!ky@(tEVzUDEcnZM zh66tk{og_6@gm$-MS>3iw-ulP!tZL}A|Ei3tv@JdoQsK`Cyz4Rg#6_JpGhQ(cucl2 zRJ*{wAAFV2IEZVdQ^`xHLl^_CI0hFm&(15`dCBS0*!fc#!Kd0YTF7joPc?D;Mc9No zFz=Hv??P!+-ajZ}J-`(&OId`u=X@=WVw@ zpCm7m@2)f8^T)jcJTl0o<@cnWHGMkMWvRj}>!4<4kS1!PB z4#|Ab0Y$?HjmczDYyI!t z)$sj(W7NEQPo?U3bzrZ8Aq_#RZ6`0NPj1LP1s|UgSB5b1ao|^N13s?pRZxXK5U)#m zinXZ@f)Acl)YK=+gG=C}D$umdKZW{@@Ph|DMWGFP z7U2_lIKdZ#dWerx9j7_A3pOY9uor1eX^G3tO}MPI6LDFIH8UPKxe_?2AOyTB3VaIm zB0)2v>x^{?=?)Sv%g41U^s( z8u37wKa@AvmgGmQIOtIjin%@vb}wTw&%saOH?TtrNB5A)H^5KWM(~4b%tfLpCyezD zY3)%RXbvB>b?OoLbdvDdpT6O6OuvRQRMp_SXoa+o4UqkESW`r=w9BGaFDx47@ zTto9Cnz3t@(1DG^v%G(9?(b3LQ{-##7We=|AAUq##DCx?#%~ht$-m-`AQs? z)d$BKJgi0jS$(jzVZM~V2=FE=*c!~&HA8R_eRFwgK|av9tM&ILS~;Utl~4 zZ!LXms<2jjpdBl~DdTV#U?>9q$WIFgeatMRUBuY=e?94xZn-u`9*m}q@A7Em*rzFWm=KFTBlkq_5|qncEQ&h zXo%LbO7J@3k=4*^B_qw#U7?3=$LN7Hns?QV<)jpv}k zb5+x`jPF|*v*i}L3Kh<5;RB590~>E5j-J(A?uVcuVLv91pIi=t!L41kU7r8z2c4YpX1inLM zs4W-hX9d|7agCE`7k82O2>SN0bgWa`LmU?b5Zt5#^5kIi+;K<%=>uLu z85w_657ER&sEgwAB|N{4JjRFM)qI~U&hv!<#J`i2%%AvC5%zWOqYbRlewkRO<$iTS z{phFoym5%$70;B1+J~sh-Q{FmfY z_{+1U=cRb2{V3+H3;I$q19Ob}YJgJyP? zb!gH#=`de%6>V#~;hc!tHeXw*{ic1oI7fwcI7hJ^cgKHOJA`|QpO>qWMVX9vlFVo8 zA|JN@71z7lc#o!f8edF4EpK)a{a4q5Mr^qD$BVGHrLk7$@T>f}_60Tlv;iTG@QXf* z_BAYh3YWi51s~BzTZwv`1JEhg7jVT?4*Q#wK={8*0`59)ROlf`Z2u_2ub|8d`{f{y z+ND;|wb_XGUFp6J_Cb)Z%CO%iIZOk*U+{vD-5`&GD)^M7HMh+z)7luz`hmn3W|1FB ztmB@o;qwpv3Q0yvdOMK(Bkqjp+E)LrH)w+9G2#dYk>`nfBX{6S_Nh$m(w@8y>#Ig+ z%c;*CnQ0U9g0)Ri}0YzKdV$dhCe*ZErHqkh$*EcJPf6?3G1F99A73u}%p zA`o+pkG3Mrq?!2HgkYq#NM@^N{WjV!jh@t+>`r!L==-S3wcVs2;@(M^P7z3C+2# zL7oK%fCuUy)kQdSm`O8ix$Qg@T3p}9&@BfH;$!311#d#RgVhy2n*~u(!BJTfa zT|Njt1{xIhl@D9L0Dr}5#V{$lh0`ekk4E{jxM${AM&x2x^cfL%q^08oAv?CEcz+VhK2l^r4i($@$;+f`0A=*_-_oawe z0|!F^ANeFES#KNCv0oIPA#0CU`-pysg|$bLx%(jqfi0Y>wF$T-6~PxA6knbxTH^ra)}<{Y4N#QiwTd%3UFce%_C&@1^Up>n05bI4(BlID|6z^Md#W9(HV_?ovP z=rO4c!b=kHBGg?a@*C1iP<`;hQbIU+VBI(KH;ejLGR{xjV{uL6Qv_VGwoDj9qBDAS z!uSjANaH2LU1$b3%>~+@SYk|Q9th(l@i_B&wFC9VNHkK3@fGTgZE9adcxcBARG)br z3_p3?0aH2Vh6wusv8gOzQolzJD)tk{Q-FTVd%Sxkh>dTO%_d1>Aq^xG*E>Ms`ByyQvp524f%Sq-x}AD`h>} z#%>(W7NIkMy!Qe2@lNO-#7VVoGbnsosmBt$p0loulR%net8)5 zC{88cp)K>Zs)>h6@+o9J`5fs2`bOtSb=X6;1$`0xR0qB1kZx8d{Fbkc0nfp^G;K)c zR|0mtTO#D6IpJqTeB|H*x|_PlPnw;>$nD-ndo*Wn0#}tvmy&wGa07B_6xvJzucq^@ zeCRkPKt3eeXb)di>wwE$pp6>fv1DuKz^gMCI)(%`L@>67IT1fGIm%$TceKN7^|d3{3&%nhxJ}|IzzO!Pc7zw zBsaB6A$gxb6PBqr9<$#mcLF#{Vy|?I>iRM@H{ktfK*C{Q8K4$|5 zw3jCw5Z(#@(8GB-0vG;j=JyiTl-9ueboAQ_VJ-A;&;1|j-aS65tKR?KdxpzQ0u*v5 zy(9tKP6k`s5(4qY1lpQFt2WSD^*j=amkCkN4@)n!wj^-Wq4ClI6b1be;Hb%{=V>KT zqV^EzQ3H5^VC(%ffwm@1)mA9Yw9@?EpSAZ+c0!;%ujhIGc;@w**WP>Wb@{IE`mXQ# z-qzaS;uFk?)(0l;h#t7I=_dQ4rUdJ{zV#(~N+28RUTfDtaB8$Km>F$?uDXCdPPq)q zEv4KiD5vqnkJ&Ap7##TOyb500O1%lAdxqzR900>}QPn<`6S=d93xhjrf|N zjlL_a)+(O+u8{xp@s_dniL-|G962xVG_f0t1IM75M=~m>B>%c1mcFSnkQ@kF=G*FV z^KJDhCNA2Njf#Awl|_n4NXKnp`gCl|)3bxE|4ximUvP0nKXDz3AFB>nw?Ff_39a8O ziKTau_rz=~eS$jszCE!OzPef8921>}(0jbK4LFjooN{cM;)k-!NhUJ%;9D;=4^~1$ zCHP-L$VBT|pErQNOWkLU4{dbEi76-`E&I))w`{Wxd2`!u=MdkU=9b+_UA>G^hWiW? zYs*H)QhEbz^ifYA>%8L2ZMSZ-Hn?)lx4^C7^?3InabOlaOnKBNFZ~}%FB?hsuS@FR zbG*;KMto;P=N}IbJ<3-$PQGFN-LaR5SM)+dTbLWN{mb5e%x&X1I_uCJ~r^{BpT>XPntGw>{*XgT$|<0tsEZbh!Ctf-!D9g{r!E^E{F;-J-b z5&FVa6VPK1yrroHyow>pf0C6mn(sON5wG*soR=8K0`VRAcMJ8M35KE{ zVXX;umN!YJ5Z&kTK9Om)AvctLh$pHoW$0&F>6ZZEgIKxpEF!1?4^1 z0b)Ft@kjoKA7cYlI{~Y_mv@bmt+uuNcjq-gOF60O{~+!`vRH|=W+i%k127P4)V7P5 zrC$?QraIVGo6}P0^!pV3zKXtuJLj5yt6%ct^ilUF?BOlwdl*{V_ENv<=m~~Wk5P}a z25ZgENu7FeXxnt^gofLe^G6;mliycu`0-W8SPS+d`?60hp^Z}hx6jyAxDUAq8%b3B zhA~7P%d5$m_&JRBKo_e5*!F88~#+}rN>OTU>7tnwoX z2d5`EdaPG|?U?ev8K=BgwwJO-=0Yy;;#xjR-wdA`*PiCkv-Y5B?LL8gnWoRLu&#?I zod_1Pp3elXg4D^A*jBO4p9)S&y&9aH`Vo9d^WZqVS9oyzVER^MUg~?`V{K-v<4){Q zyMgyF=pHACxp)@4=Sv^6TA!ibR%C{gS{wH)s_3Q6gwE0?wL@G;#XBgYJ+KV_apwXw>~I=9Yc2W4mzPrMrkzxt;M0O~+4EY_;9WeGd1_xJICXJnR*Hz}Enu z7i@LlLG6^za@wertm3xO3$OII5!Z7eyznxsZK2x0Hd4iWeLMGg+`pBp@LY6>BkL8? zH`#PT6Rde%w1M7ezS;Uq_Tub=w`@Z`+xuW_%XPreB~sJ zlC%)>-&^nP$FE3Q*reybI<&305IZUDApg$Tk9_seOxA9A?3sLEW(_>GC!@8m&??wO zo`rU(er4`RUv1sb8o_5Ghp~WuLes1=Xes(M<93oUI}w~2?cEbd_RJ2L`0zfi&+j3h zWU-Sr@{`rtw?%woO8u-U)^cL=(qE#VeG9Q6Z;W;P6`d-CY!qG}JN~zgb}|FIV&s8~ zc4WePQ^*|kc1Ba2x>l3kMO}wyAoHzKeKYH)Fvfi!bLiu{*izLe@%{cg;a%{GzJJAL z^%mqa;xoD)j2+Lo{GuHLCj-C~YmHOa66#z{eSczJX&l4YD>IlAONlETK=*snm!98M zw{Hr(Nb*8oQ7rumw7h{b`lj#2H^lZ;KK{0*KFVB*{9TJr=w)<{A^h^6Cr(drJjocS z-%l|PJ@Bmlx5U!uT={*Ui&fl6*}u^Dmx+OShWbu0J|2GiR`HG7jj;;xDSa1Kb5$|F zeUmM|Ws5C5Xmvab4XAF>fa>_`m9ezOpkT5!LwGIF`lfsF{ZiJC_!ZoT1ItP$S!K@mTXR#NFm!G$V-26@y$^@Dck9^b z*5T*1g8hTYNTU5Nd}t?E*-2*&693IliC?SF$Xr<`!RsJ>l+H5rKJl$=)&PAIS2|99 zDx3PJ(ofMMvUl0W}&0LkNIHcC;fs?B}-WMUZq^R z{(AQEE@r)21aDL9w6jm2^<(R^*sn^UC)M%2SBO<&Y!p9~z!qVb*hxL3+-{9gx9oWD z}6_@8hlqPW-F09$2YRFxK%3JoHugH58W>R3pjihp7jjt$J5MjtrsVP7ejl6;IbgqPF=;B*1SIE+Dm&Tv}#Rh%@j}Y z=Z7cE20xG6$@eg}eO#Y!a(Mz{Z+OD}4o^79ynW?uvGfNoaIpOkN%_qrJk`?=)GxZ_C{AmeXGhgwHCxfD6#Q^1} zj)NEB`f-&9Cx4+I&+v{|l8R@6PwVyR6;|sBctxGjvI(T(@W0c@ieEQ7tBm$HsO}tBmLh-m=SgdEafVJYPz;SJPYe0ikCVT_ zm%oAd@{&p`xtRG>$lOO> zyQsXF@>0;yiH67ib4OK=i=|Sd_>>{~EGRq+I3>hW$Dm;sC=WTr#^S_#Jq=#bo z*168%U(UY;x<~+%=8xu<@xgmC!#j{mB_muHOP5}xIHB->XiEP09@5nphY#n~%SI%A z+A_tOp8$8Bue1RlO&!nZY-PV=-o=BFa;Lq_dd0=<&hqvxWv8}+SK0cTG9CQOmJ{V3 zo*EVJl#C_5E5FrJ14l-^;M<7ovz~GvZmzyB#hTiH3@x9&pGObTfBYK`-=)ujeMxv^ z9XToZXE|dY(3o?tbtJ5H#MjR~%uDrC>q<2=roL>N@Fm3W^oUga;ri{WF1@Ua5lnlRJMRq^vq^fmb_|3aQ; z$@2{PN7jy!e2ZCcJeoS6T)Z0kDDv59#P>@f zH_Fb79`q{mlNWC<{jD9@Q1YB?@B4|*Zn24Zm|6 zv3EbdB9{I!a^K!r$hnfw(MQ^$>Grt}9`^y`GRkSqzJ;s&ZtvkL9l^$i*UWl#IW#6+ z>xaO#7kMgvHGWX^t_bk70LxyV1lq-r^9WCsoXd^B#!8+v07+VM7qxdI+jaD)+6MYr=-onmQ4Q)qkJ1PJ3#tl{y zxwE4Yp3uPDsS@4u&a)r>AUoEv7a3$9a$pB*&5v1A+vmp8KV+>D4+zq)yP3ar_<0kQ z7vGYt&&)gNQMzZ|ZNj(d`GNbG+u{jd2fhY)vP9o z4wVi%G(~(ju%v-u#5UmAxixP*Imu678R+}6gRKu(9aAP+1)2QIz+)BokS=im{y==Z zwke=YTRWz}D=torLO8*<)htlJOMryRyg^Izq~ zyP49CAIujwIRd}WZ!t{IKQ?})L1<|oG%EiKF~aG+yf^X2&*qK4?eJE zCuLi(afug~)!n^Man9R)X;C|`N&4kd=#XHB;k-(he)%QRBp>aCF4g}K>%C;n6Ucjx z&R&rEZ}O~T?)}@C@?O0Uj!9R&>qxVF^@7*#RmHN&i1!wxCNhV5kUQ@uUx;>p2wk^B z*L$HM;cuU48yauZJi-s~80*!CT>NuD$UR&>)_6icPLp2xo<+NuB!!Q{xoq5AK^t5B zKRbl|Gh%mE5UZ4;j&4CVhO|(|1aLjwwWOBe$w^+9J$peFBq8~nACPHWp1YYddfjZrkv*!P(Q0!6T-+s z&#^zP>HvOdn^+NOt)IB*jD}b`5=QSJZ6ka!f{jJ@tTD?+;vW6}_n#R4rL|D_D4%HE z-cBD|7^gTk@zcPf_9T04+OT49Zb>XXZ>HlLm){iG;FxUVCQqfKo63J88?N-z9%u&N zsHuN)CP@6O);U+3&c@%&|0>}$60 zA^xoE`q_#fb>#jb@K;N_o$Mo@j=w6QdRb4Z&?SV2TH2GG;FTMh`%Za{hwRhq133n_ zw^9BsVn5XvC-yVf?4@g_@5)m$CiZg(SRB8H1J{M>mtOAG|HL@;H!z>nhD{sZ`sDEh ze><#g1=0!Vx5*cT{_qJzMTdgLkEdZD47}FEnbsifeR`g=6xIJbp@Y?o|57`cTw!Mp z5TlstG&EJYk9~CYqB-`p4K(`T^2c9zuth4ZW1*p;wp?hD{grL|^Q;#esi$b7l`du+ z{sC_%-si=Y4%}IPL^K>CU#Q29v({ls->`=(Lht8I7a_=qftKIw4>}xGSe~CI-{}S#KT(|PaiJKF4bsf_lGL70c?YZ}+J@;Pi?V|mb84WdU{KJ&* z;`$r@S`%8RvyH#{TSeM({_!m297D^#J&CN<+T$WxyqotS@(Z_P^C724mFFJt`Ymvr zw5(3z0y@9Q|4aNI=D(Rg@_lDp!0K%0Z}HQ-a%8uWby0qtz0B=mE7nnnZZ92xcG%b>w;yx7H?$N+VJ@8`6d1ujx7k?T4(lV>E0=vaL@qq--R=;UTENHu$C=3aGVyL@+5E{?>b6zgGAuVj!~`mb_U??leK zF?6&>v}n>QCnYb!?vn7@WZtPByCfqC|2$f}E1Y}@TdYZ2>84d?Bwrvc0lgYHDIeqNCtM3C zW8O77?^M6useX@_wZg;XKh%8FexlH|jL&1(c1l9a(52^z??@&AmqyzP0pRb*^ zUa)&}k{TD$LFB*$aAdbiClk-y4USZgV6d=XnEaKw$!6pq1LuJe{Y`xJ6g=Lf8+(Qm zi>|pT+Ekfl?|tJ)ITIH!WR0r1PQsno9n0?$s}ozarM5eHK~| z%y{@?vCvsO{0#XcCc-3L)!hZ%|*zD)aXx0<7CInX|xK3cRXk4)ta57avYs*12E_`dQH$drdRzp86~u za|!$XS-Z;;lR7JurnOA)YVF+*K3)PJilth@c~?w^GU~YHPT;=B?VG>f#0Ac4pxt6* zQ+VUNq6TZtgo5h(nD5J%LSO1{E#;I)IQ*m!cWQGKuF2!!&) zKQteir0prjSUB9kTwp8F^&j4)G=C4*aFP0of}QKDd1$>AS&O z>8Arb`YtM#|6>53)biq~RugML_fA&^VP16~=3X+jJ72qvt;xq<96I)7d?$_$eI)e@((!y69oK`U|TX_F8JaA~pZaujQn+~ut|BU=!3Qs)qxyn}NB+q%N4xTlCh-+>C z7urs+UJDNOZ!^zZc)kW$HcZQFQk|;Li~G|#7|2I!O2o^sDWMa+u(mXiR@o4A^bpTg z)e|}{qr7DAA=X!|$6GUu92Y$${tjQcmv)q{_jhp*^UHaT^f2{Q*|GEh@?Apx`$BB6 z6#DNX=3Z^pQMbzQ?u87u-H%Y-Yv&H`6_+ADY}PqfUmZmkKHH*jo%j!@zH#L4jc(55%G_g*n2o})Aps{LpmWinYUW^@Q(=hw#{11 z^C@%;KkSz}uy;p+^9-e-A26dO7!)d%Y8$#*^LUn>~UblWnt~GL(OXt}uC!)hl-$xQGMH?XXOX(@$zpOlmANwR_ELrk7lo7Y)9kUfmiFWVjkOoxfnTo0XnZ>&S0KM zc5FZvmmPvMV`CX#9v{z^{R^)pk9%vIZ}lYCXD~=PLHHrGrzjN#){mBTj&_Td^7JWWVGd`GBaOSdW2P*3vL# zzXol`Ilp8sWxewSBo`ebpW2*5n`$f0HG}+d+P;~*`|bPzJul_H*1r9f9^`_TgHy3- zOr_mS@RXT40UqI_k2bItc=73~Q+*6m-*Ad8 z%n1_%5Mo|wos^6PO+69f!7&p@*b5= zbP`z1J-QHQ$!#kJX7$?-ms7jLM?HB_af)N}s(!zZO7LU@vuN~1Vx@Hbp|f{4wF};+ z=U(@@+UJ+^>k?kuy87XVuhm4wm(V3H1h3x*0W&<>khLh?-A6fMdUS7K#LiG;We?PX z=kG9owqyki?#A+{vHjaZeYJ%*q!s_TzHp*s-J`Xa-H_g{=gZvZ=%Vyj^c)R1@2_5z z-mK@VeQo$;o3Z_@(m2`qoHaNhsr~5V_1BM=b?BJ>eIG-NiAMwed^oPRjyHbZ_)16X zlYC42q1+p!*R*|@a|u*NwhBL7kv9OB;QkY^jcwEGub*!I0BqS)@E6ghtJ^=~?a2{u zK)1}X@q6i9kD;NRiN2wy65J=>K2iFeW&L2fbM^OEIC)b+?3Fskn7!TYJXf57CO!G^ z8{BsyANpyjAAY#RvW`^M^)HCzS_|UH2PG4%W7xmiZMTkRC|8Bv6GFE8Joho~|8Dqq zL(N}ZK3;0YDwxX^8SKN%fbKFa`$*(cE4u~!67#(vUbk|A-mM-QD(eeoEDb+Uw5<4n zB}@I|=BHyLx9pBpXiukj!$aa>t&2Ot>n_WxZVh%M4#p}}w`9L9SI0Wu%=(aBY;`0~ z_|nT`9akv*O^OwFe7PR22ycG-PnCGjE@vN~+Lqp%SVRmPKCUj;{=qt!PAsx2Bo9k& zZ^S+#AIJ>G+?4MP7~gmgexEJye9ylr9xztd9oxPmr@{MzR=XdT{|Ov_ho&wB$8l(_ z0X&X{q9qfx+;_%SKH2kqQ5(}PXrQg9^+yIUnH^}y$ zo%#sR@zi4MF`@Y(yXZQtJ@HhqW7BTgqDIRX*U=}%n>7xXl-meisSA?7c44ZU%#$zM>j`CELv`@X@+i#Zg-yH z<=u*d#iVnu?L%b@@kG~e7t=UCs}JxKzF^Xzzf$nc&Ovw z$2zS2Yl3me<%z@X*;cD%+pVML+jzcTN7sZNqs~55&dH+gz1YuWvqqj+j-I}JX9oS_ zerFbE#OPY+oNFNa^3JQRH8G{V(>dQeahr4gR2Q-lIE=D?-NY%(BJU(Lf&X+>^vU$FWf&1 z&zfIiC8giVF37%&?mOWnwLE_vc+SuUcza}~Z6)`TE?Yq+ba6NkI(!@SfFAQ=Eq=!* z*(0I8_R-hStj3zq<-aLR*jD*IXqG(&Y2>oBaCQKE>0Gpnr^h;ej4%6x+$XSmho<8X z*L%vVPvMU&O1IE<#*A>&Ql9JT7Nx7Wmt9!?u1@;50vT>erIpk^Cc)JQpYdSd2JC`O zb{cS5LwtR{>R(Cy@1#xXaylD_wuaZ5m060{z)!`Rqgaix?=@a}r+&uqd3yZ}ot;cS zo&N1YF1^C^&+3po>cMj-@Cc{btYhc$7wwB2vA4gIyb1cQIaqu{Ed8@**@wxz!_IO6 zySwzL@%4r%<>J&Q*{l}c(uvOYJ@|+26QZA~H>739vA_+_kA#pZ_1pmNh^gtQg6GdC z|N159;+p?&5RTL6FaCao)Gz2k{n8p%!ZpVKThjZfS8$HyDHgx6_!_Gh{R4cff2wyb z_>vwb9=RO6^#HrmC*rc$|5i(%^o~8FTN`L|>Nstxy|ec_wtC{XlReO!V#8Y)zrNYT ztCGeQ+%?p+^6%d>p_De5hmmCejYH^?sfsH#q5aoS9p_&R(Z-;m-ukwdk0g{sjEv>%7S_QK_!mx?KaQS0u6*j~J$aXz9haVw~KBNAI=&yX(4F>n&)URl#hgcAc_$#f8eY(F&dYXHWE&r$9 z{k&NEpm%>czIN}vG?xB}cmEDzrM&yfSo#OveQ#xYFZX7Rczk4y;Cm0-PTmR5PLj6K z!P#33&PMBJBXsBJP?Ad;m@|q2(j58|axmj%=8w`}Zw`Hw{9Bkq@B~AjSMxkp7SlkD zZ5KV}5%#y3GX!dxqhECFeyMC=P`YHWFOa4&{4)K}^)C87wr;0h&p+q%iSZeh^(0?G z6VWe|*6g;|!-FgRal>KuBX`PgJ>+Aj?&yp=(=Dn_$(jS$nqzte=973*?EJ+`Y zPw2G4r4yex*8g_;Wk>EA2cHi&xcw8)dn;{vb74L9gBa_X{JzSgK9=*o8C$*Aw-?;? zL+_8feY=W0f-@gDv14*JX?7|?n)>F})BPr%H8xJ)vQygkp)whiQTo{N`ma&&pEGU` zdosFj+#YUr`_;p^O`yGo^NgF#yV`NajSinPZfC+y-+J`U9k&-rQ{U8{#`do~`^WA7 zcKddmGUJV#@~XdM$IRk49-Tsyrf-VbP>gglM)VR%M9%$Su88_i`yn09`^zKuQi1z<$5|b5_I6meg3pu5KrH+K`;?`#{E2u9oewR%5ZAFL z##jkwe%(cVcKRzh&*9x#hvMkv;FhsMrk8Owc=7q-pC(52Bzp!<;D^!vPT7iUk?Vzn^Z7p2U#n!g z@!3hfQvVdIA{j^VDp5P*@G|tFz5g6)AFuvD4h(G@&)4!D<}~m>WLHoSzH(2=jqLUyPMW}?Uxna)SpmNK2GX;7@3Im75Sp$_w-fyR`~NZy7~CF zmhz0xM@G|W8GWlQw>s9IuqytGI%}`y{-A6Xqis=;xuJ7!yeoa}5`|&lgIluCdBkSz z%rj?B%=71W;k1BrY*l=SXEN z=3yuCa?;y>Et(=NWa05V&l+atg^$0VU>?kZz8SkmtTg(u;3{$8Vof$M!I#hB3m2RdAJlm5^qvmd>OWk`0iK* zFqUyPc3S;hzni$%2kqAN*tOnGy$O7$*!t$NCo~-ghyMJJyZK|_Q-Lc4T;Q$U@NVGx1N|T#;21H2@(-Ie^kwwHu`-l5E@xKg>>OuaTxd+N zLysDh8Z#!GC3~JR(Rmyi!(ztdx5$xibWEB@#-u`H!hT^~!wubI$40cPvH2T*lRR)x zKF-+ul6f9*$L30RY&Z+*oUvg&H?S&3Y!aT|UwQ6kO=fM4Vt*_< z#eQnV8f4K{FYxHSzJ+$2Jj9YmUxu%A5qr?bdZ%wFCj!|=!hFkQ9C6 z7_|s_{xB`KN%@uMW$l?}uK$s~?FMI6R%b;&@CP~!-e5a12C6sT&TneOekl0z7{`ak!1!4ojDl|i&(3`RuM7I8 zUH&#ImgBL0=pVWpyI0TS3r-Ax`juzCkK@_wM)xt##IW=Q%Thzw&=kkj0FTp}dOmrn zkC>GL@S^=sCG>N)=_4`sth3HJOwK+b$zCDw)Jbfx9n3l6>{riAHB&x7xU$@(8Eo4lQM~~z{n_=sP&D!7G z$C?(Pd?WmN8FM)1(n^H#g69gZiq}$|f?KkX&iM^Z$!Q`^q%}M}hp?%J?mE_>i`6DF zH0wuNvUIqFc9FSdPk(_jTV%l3w6^*IkFJ6W%kq~r(_=)8nPG{YmMUL zdS(-sMoh8n9z%~?73s-|?LUH7L=-c$(veAge7@$y>;OJ)M@G@!lp*x@UBEXMrdxoi z@9|iLWKduz(>F%{lh^@`$tQp%aYd|^Zz~*G!1z)hpTwHj+H+Dddy`s+7P7A~7^wIp zaP-Y4R{OD?H6izWKe&hp$Bhn-d%>%Ck953-mxty(G%MB`wlj{*0Y62-Of&AX zJp>P2k&I0(Xj*9(9MN1TLbfvXR66x|bqM#SE%07TThM)t;`RJ6e87X@0%dP8W0n_% zrjyq}+iAvQ^tl})I-nh5aumDzu20_8Tv&gHRnZ6!iM)w3V7_IwYOP2}?nM{v!X5>z zFL-)qUCL@jzRfv^|8ph0S9~H=g^bDk3|~)t6Z7|&&MkmvtXyOzU*TT-BZ6<;^Q*EC zwkCotel_ii7aM)WXEOm(pI+{)--1(iD$!%lDl7Rb(J=cj&!InX+akIGx8u>@Y2pin z|6%$Yo6k>wW8NA1TMYdHvvlHI@Lx+?;#-=-lEoE|_m*KDT}N=hjOm0UZK4~p1kGb z7e+P~zd(-DcO16LM#MVQ9?^LFWcGK_#;5sP$n3eQ7nxmaM;Ch$9a%dR_39rj&*eo4 z03Vj;N~t$Oz0Zv&&k<9rv%-da@|^5E;^X4opL$AXafOc=*m|8k4AO^|^R1ZO#mHsw z1kF#4>sa{n4eZ1^0lVnu)bW7ft*4F$TZix`4K21S-YR+mhI8m?qf1Yd$ET-S(Gz)w z>1k{}KRu0kH_XrWu%-jQ&Ts3v64`TOIQdTc>zutaf|HSX;l*u_>@9+ZQa^BG8nvH2 z!JL!tE{gB2awTUGFrRZPrze|9FJ>*@#eAOy-WL4#(6-~qzaenLvtqGpj?lIR&Xg`) z_S<9XyXhknlHXxnZla#&splbkLNamrffk(!x|4lHk^@y=ob>e%G`6gVRt}IhKs~B& zi|W(54t&M@7t$Uu&GV^L_fl79 zAlC6b{7JUHv3vfC9-g+wLjEjjp60|+{ASvkPU^1`zgt<7Ec)Fv){?~bFz_qQDk(8> z6Xw3R*u+hk`(4GuaTAXH$2*Ibz0U9m(b+dypEgm)!qN#H>ocrt5715o{6=^-bD?*l zIlo~2H0xTeoul3zaMldbwAN{@iRkv%GH%29Bm-x&_V(>@X%zbP^E|ERig)nG0vfq> z4F9ja!%B)4H1;=vzc{#`0`Au@vIZZ5cGhRv>3Z_4KWGh>l5ag{UvrMY0^;3oi4)V+ zN=)}d&_w-5nU}mD`|f=F-OO+CL+1B=(BaU=ON?w*%RcD{@&YndeJ6YNBccU+lCcd* z|1$HOb1Fl?+t1ulTp9Ypj3Rr&nmy22CVY_dk1DW{R`h=m+D8s}o_x`Xu?pFRly5h9 zY^sj9io-|Q1Z~_v3vWG z5$hJcxoK(Lzb=TZBnHmDz37gQ}L6?!PT@Ep}i{Fn@XAZ zL~Nqui}><6c`m-L&JNB{zN;yx_Bm&{CLWm5G>!J_X`A+wYX5ueLt>nhlNj$)(Chg2 zc*p)V^mzE#+50WS|1}3rpbM4}19{TO#6i~JKd>~BB4tAyb zm5ejvQNx_gv2H-W!3HE*VlV5h#>s*gam|9qYp==S?LXF_v(FFdEq;iVnQ-_JTn(XYY@-g74({>|W0P(-FIu_PuW+R?dS9d0=%j zI7lopu(oZvF_z8;r_I2$g0mj<+yb9gyhso9vy8PvIO_Z}?Rzq&c#bcg2D{)bGiw`tG8>Q>;5m6K@#{^U@bIrq0jN(?{&)Tj9rv@4r|3yXGah&t^^Z#vqRD zs{7IdQBMMKM>y<9xmzM-O%I^)fzZ zwPiO>Pkxtj$~#)ld2Co;6)&7mKZl6RHNGgu4mmMemSvR*raU{#_%t&47WA$7HCi6b z+!5vs6YMti*l!)bMy~7EfQRnnY{gp6uZk$9Vwba~G`!PVCLY?%K0fhTIMUSwetH_*x1Ro*(HHKl`o^aW!bYu$Pk zYg>uEq3_~Y1>XTqR%zcSJUPU*+KyE~L-lds&>1`IC73aXf8C-vP4JFe#6ukZcbVaF zH?iha!LLK`Yv~j6=hQ8VHmx`tT;t@s)Z|+XU$2E<2Y_9=b}juAe+qMtuF}!P92H(e zrPp_au%%jzaRu-F{rDXHAkWcB2WBL7zWWy``%|tzhZb7OC+>KDN~oy`_?zfQg8ryq z$UOC3^o27pW(-Z7u;vc#S1;mx=(>+CP+$Je-{GC1sQ9MJsZ1-jg(muS1U?PFO#X_# z{|%lgT3fr@DiyAOeRgP@)(iFJ%52Qr^e6%5A?AH`bvHiel~K(lWlCyWx;krSje+LP`D7Q#Eo0}k;G%uR z_=fI{l*P_c^CbOJy7%+XJ42@>j@%%6*Em}<18I|PkN6}^`lTa#3B0+fI$J*bRU?Na z=uhG^Uqwz1E!Do*1iY?)IkMd4U4@jNU(|w~0iG-wN_LLW@$IHuGvy*r>@fFr+{d4E z?y*5H54kchn#!3(Nq9>Ktv^|k;?EzE&=j5je)mM!J*K@#C1zd@M z1D9+I9z5c`9z4}PcuKkV;OPVg51v}Vb8kVG#!G#ZZK^N(bK7-&Hv6;N;g@Caf)@`V z*Z&1Ot$};x2;9STm+KHp@4nV8$g0g=0AVtH-cvp4a%Uqo& zTXpzs<(4S)T|bd?aQVicWVX1Ct(<)2ygP|L>*=GOzH5Anz%SY-ZgrddQ!mk`fn}Ei zi{^*u+_*fBlLVYi3Q8ji8S*@*~Jr zt~#FPdAv52*Q{k@;y1>_pJ3AZt8*;zE4V(^C-AZ64IE^=lC&ZJQHU{7n>3u$X4cD` z)EDuaO8(J!%Gc5UVBQY3tuvXv!8_Hxlke(&GV9h?c2n1G?DQJrih*9{VDkkbD8d*HDHtoN(&{nzom*5a!UZ&!R= zEoXW0Z{Hs%7_c~-7yO|cmLyBbU)nnf`NwXBDIERaE-Tr_xl6}Cuy#=OZK2PKVcJD` z_G#qgzklsuz-p{f+zWl`mOajL^s5vxHtliu+jK?$Vw;$1wPW{|Cr`L}H_7J=Z#U^B z^M}(nk{*Lr1y2NcLewwbG`5XpL94y<-DORrZ~n?3;hR@hwyIAnUm02sevW3sCo_mi zjlHL$sSX@0VI4%r*&G57Hs7w@L>Y~zc;kik)Ts^Gs2(6cbGZ$8ecx?&XxyZ6#zHT= z+l~D*v~OhNSBKgJPlj}AV98XRtTQG{<5wR7mW;z6rEblaKfC=t4juTgJ@*HsZ znDhKm z(J86xg=~1N_|QDgL0Nl8AX$rlwiB8+d=!4(LY$0v-uewigWmaA9^H>8Z>c@cuW9gK z;^1GnR(n=DApgto_Z){@`>n5kFBFl->xTpX*f_L(M#nexH@dONjHBA<`*v(y3wlN0 zThJ@{jzA9Q^MtK`tH~t>YiCAm&6e+PDg@WjHgGL?LiUo5HqO{XkF3u-u%(c}C__eh zVS!yBo^Uk3(!^bM&kSP=05`%*vFw2%>ru%@t$|oYD|>7qRId;GNjf6;-{?Ss;85|2M+<;T;e-{QQu8YvX|$@R>nMjLUyB> z*Tqj*72qJ}ey*pH+jeq3MlJ7a?S?GHw#B%L=N5B~xND(!P>cMV)D;F#Ave7loIrP3 zcjZ}W>2;06?G#?8I23%PYj#P#G;LpcT`y_6zmNPOyU@U*K8D#B%lNcK*4;f=IMZ^z z13S%HqyCkooo@qQXf7Li^6~#3@Uxh2s>)Yv@xL0nXh0Ub1AdBaaNAtcl;^|b5#5aC z_YQrItYd;PhjEasRJ(fZAoyxKi#=!k>axK*XUSeM{YB}RqHj}|&jv6u&gaI{ijPQ! z)1LkqI`Jviz4?ss-N-*@Xrs@3D>={bmQHwz=6HfJyAz#L-%VDGhOV`YiRQ6r7kG}v zq012b=`QMdVz?e>-4H&Ssf#*}#etnYZ3B(eqjWRwZXd=w)55`v=XY@UrTQQn#m+$X zKm+ZU6Ek-g?W0ICS#40sI=Yp#R zJ8p_AaJGd#xao5haF6YCFK{2SZlCXmTk~!Jev$$Yt@Npdy1ui@N`7ZH{P;@FV4Fkz zZ{Z%>gU+JXxomqja()E1++BMtbAH5E;RW)G(&vtqz^!jwjHN}3pXkIppQ3LWwywo| zYlQmxS)cl7v!8R`Lda`FLxIvT*Zw`hQnL?=cFaB~&U$;4w)z$`UYzwt?Bp8kUme9W z5{3S=E5@5&l9A!%9g4}8KI7r0kvLk>LyT*@Cf1s3=PX@|AD(?&Yw!_ud;y-IeHQ4Y zDP$wbim7YxTkyWV6MWjpGPN~Ua?|gOK9Lm`9ng;22yq5|g1J)(O(@UxP98IV&W9hz z|K;rMi!)~wH=uKl6vM7QjE!R_&m&$u!Pq$Vy^gM3vm2aIHaZ>HeQ z%J9Q{Rpzo5(v|0<^!p3&=N@bVS_6zP4tub}>qnolr|~T&&*z{6-RtL!gHJxG4-UtB zVxu@3pS46b?|Gq^v;RWtTpe{p9!(S~ekwjIqr-Y2w&f?#)V<w`TpeQqm6d>O%}3Mj_{S;cyW?-^5}flBJG>b9$Y9@nb%WI7E@j=7?TBygPE6v98IDU| zF8eBcY*bu2eYYsn>Bgm((x-&aK4bbePW;2@eFf}G0T%5mu7lrUD<7bp<)l3#zfibk zaR+IuwN~tB4gjCQncw$2Oy53v$<)anTdm_6>+TqI>drWC-O%s2bsN0;>NUO#!8>Eo z>%!ZPKj(i0FMgCa3h)0uPu;_G;Im75GMVgZr?5%g8n)WX`L_^DpJ2}8G-$&%w(dsF z$=xP~3mac{s`~Fk+g7uV%4Ql-ks3C)?DaqP4S}* zg>$K|>Km+8%=xwGI4uEU`)MaadMo#=eb~?Y@7)v}EMLqXr}doCc{BAt6$sX6as{Rr zpP9U~P_{_v!X?mFjr@9&5465h=Bb0F1CK)+Et5=N%9L)Aj&PI%&DT^D-yAr6eV>sQVYw@2`tOw1=1s|kN>Z;6<{g z>P^T_@>ys<9Xw|1-9wU-1IJ!ttm9vF?qB6zxJ>Ix|7RmZ3)Xz_dH3!KV{n>pcx3}P z^~3WVWjjgh<0>3?xj1fc`%^`KdU>bw2t9m%nRQpR_(#%(*YoOOqxh_I=6J*%AJKUa z;}ic-rI|+w+IbH+QN2HP>-Db*=cBv1@FCIO@16h$;4=o?Pw3vIy>8vRwD)V>gD>Uz z1^4kcKzj$tH(So#$vYnHIsF;Y-wTla#Mj+e1+!LaovZ~%TZmOahiq?P zew6d=JK#QQv%WSg3M8kvI;P&epLfUUn{cQ${qddG=o~bDW82mGbR1f4=AUEfJJHz# zTANje{Daw+6VssmHfuTOU4E5saG&736ywh_b0as2Z+-w?OSzoX9NsJb9bVZz77pRu zx_QZ{bBZtYZPLrY-_YY19n-fjwBG65;zDGaUgn7nop`*tg}#$7SxEj@=Zybtz#&&A zybbxLg8#yh_2MdEj&RNwu~F$);eQ2nc{^U39ZJ6RcdM1yr0)5|Lxh5b`0SqCE??e? z668_z!=hl}KtJE)JRT^-hF36f8apv_cDZ8@acs!aXM{)Dh_!~UJ-BuIar{%tFTd&l zde3w0k=QAJ5oJFgC^%AOcPb9)*rvMY79{GT3$WcSz%Slj5X?KGHWQpNGZ2iWA7N~& z&Q>1Jejsm$*XCMak^JVgTR7bA7pVJr+8O|!6~P?S)@uBa@!ZY@)wD<4!m%owHTA`! z1;i_~d-XYW70`##b$M{F2s&`z6g)rN=hl@o3NEj%H-dvx!10p7SN1?FeR6P_Gr$>d z7sNaJ#g;Qqay;Be-a!7hG449ovOSP@MEiD1DWkJ1|IYs55ghX!40mk#PWSTt%$*?X zgVU#6;;(WIu0I~gJ@2^6uC_I6%XZP(Vdl)M$k#gq`R4rOmuY*5wySA7B04nwCx83I z`qMu0dYOC>24+*Ha3DM`yiWce@~fS2F#E{B`(mwqfx;t_GjO0TUlGh7IKlkChxq^< zChwnZC4cl;Yf$rHfHcvrU=7gESD@_*U{;#S-0Lf|*H=b$-og7p{FwNQ@>O0m&Yq}a z1AHfxHjcfDo|AcE=!ZXIExH%_o^AMtXV-A#j6KL>&HO9jh42=(SmD2b-yPt;ige~! zh0aNU9?X3jp6c<3X6`$&Cx{<;eBe2FioV}A08hD@v;UA+AMFLs7Uo_R^N#&(WyFpi z+r*rZoT>EZ@$>6l$bDBw+v*EEuRXz)c!L)cNwZJ5eFbys&?`gjs&^OlhAg|Oh&g6u zG}OpDoD36BwXo58@ecS<2pic# z_=L_vUy6OV)QVNqq1Qc6zN^U3-jCyvFn;Q~k1t4-bQMIO_}geYXv+M%Ece2KH>Qrx~X$ zzHxag_o`Fp$}D4j(fYH2e(0M(l0_dx_IV!PZvq{FwPRi_=g(@~>$wW=I|G>mEx;GY z&KR<%n7E#5NGu*8{=$xJUL6P@iIML@@jL!?(!u-rzm+2pl<$5=-vjJxfw_u!`W*DF_1L{i(Rm`^x&*tL7xUqrN5I+{okk4a8SF{u1Ugrz z<{k9)34A9l>{km{I^P2-Zrr)O6S=3zn*O5JoY324Bew=_0uOypefs+soxT3}X~B8t zTaF;dCgs0QeC=yBiYb2XRmZ2j1-fnd@s1kg*ACh0wQqA7vhHr$@qC%5sN)%YxX!l~ zoih_kfJtyZeP(Ez$~4OsD&Dlx@wc4-_LJtDv)X$yGg{1gq?Byk?tA89{g zuh^FY27PyPC9oG)TE}kGd|2hgi7$ZidfQLxe71x9BhT&e*>+LhAS63SB%t1zXV$k^HDVJ#c4U?m!th%%!B;s8T2%7j4Q#L@X$^g zdhEVdL)%VlcLTnxFJXVdCR(FDYCREdgb#fq$)Deb|0(}Q^6Oi!%$IIcjz0MFY0fC$ zI`SztdIhl%nj7!$e*L-O@MoX>7~hBY-5$kMz}pyGVkoI^ka?i*Sf62jyu=wS`iAu> z=0iU6H+zbRr_1JhtvP&$H8=G%{pkyG<{fK2bdF7bW~yh8l{9hhC*A!Y@N@sXc48qO ze_cPcw;sy>B|oF)R1fDq_}3wae>?I5vO)VG-y-y6fSuek&o+m_J$y=b%13l|Mk)L& z(&+FnjjMQ;zfO;jjQ!4+x28c`oIS&w!rt^I>V~)SoRi9CTv+3C)^D&Lt-@|=_~fq1 zN!fckU0bcbEv+*tymlKLd+o!y{XZpG&nbFXqLqxcA~oaE7F*Jk?@`j{AN zvmM7Sggjg>^OqbHM#e#2z}^>IS8Rut z4xcNt%(b7a3Jm)op|E`Hi`+TukEX}Q*z)))qmj4FZP5}6h>s+ZoPW@HL*y58Kn~`skj>H3psiO}&MU*pd z4!<@ue?>C5W&>rwJ2s&=C#xU$^cJ0mBi+=Y|8wEC@l#l#hVHw;qt*cUCZ;=`Ye#Hq zY(w|jKnVRUXP|-cyz8Kqd>^>x+|L6{Knr!s_g&(i z7qA|fL<{HZ|M}=t{m7QR$_KyBIe?xFP3H9mlU>l0#zVB!KFc;|6|4jXwYQRX)#iEq zYNN*A@gs{5?kDbLBW>hUKH}2kfBsc;tT8+|?iflo=;8lQz7Hn6??*4dKlL!{>%;v4 z>;~+WnM0jyNn z!v^f}FvcXhY~tX2r|l)khN1d{@;fsZ&?z3C2Ru1vZ#X1Ai5BJlmbH(7%~D@Ip;%cGx+Qsi8-rv{J%A# zP4jNoNF2Y${zhQS29NLc!3(`-HDJ&G81*sli1`_PmiGu=9i6I&GgyUN@#%N^>baCQ zt>9>!Zh$t%;B*wNd;M8LOqq!v2nI`~fAvALCs*MgKo02T+|);aZ4&j*^0jw}xeA`M zW14}TE44}glpXM%MYEcgtq8V;gQ2DfbWk1Wk`1C8@NlZ1h0H^vgX%*x9joX%^xT4J5pE)%W80gW$^M!dE9|`b#Gz8z5toy<+i5SOiFanNBWGwd zEo9xfnYyYdyJF9T)+*J>H^rsH%tu!}!MTq*+ivk}J6XBdPR6qFRk3FtzVm2tE@yzV zRzuI)6H)Qxp}qxOtl2vcL^uHD{_jH%8m?~@^=)+Ot9R>b<$a zV0qJg|O7R9|E=-w$WJR44wl8N!SF zGk5TPpLbvRuGS@2#yW@{%-3GoowoI&?8z4aYnMGWvkuzLcs$b7S9fXwd%$M=nwXLy zd&063-!Jj1Kh~LUS{8sqZLB2~Xm}f!@ zgN%>-5Z_@OU)->v`?*!uHuYgoQQkYiyJ%f>w2<*`gzsl!W7OD%n9E`Kf#R`s?rNT0 zuvD?JqN$VcSQCqH#=g+Vc+xBSg0YU51EEaL?=!R{SxxPD?Fxq3_#4Kb{&AcfhRSY%&p@EWo+kL0m;OYy}XZV%p9w5-?Q*0Igk`fyt`F|S_u z6~9tjfJVOMvA0R| z>f!ar^yy{f74_@6;KeRp^Ne4njy-Z7UROe|7lYRkoMsF>4y_8OL2x?EJ5!A{PtU$M zd4l?kUd;OX9R59HTfxSp`SXNuOdh+ktVv_BJCL)~jK^Z}H)kAT%EQ^ZIq;vIf!z9y zv{e_#U)s;Qt^UYoaa??bzJz@Iq6hm$54M@!%vgtTCfc6x2FAdX&&Hm&_9}6wp1dhL zXd7n%yXV(pM+v5Oa^{8&eMIP&V&B(uR;prLWGhyjOF29($EPdcYpvgau8=@BDL_Y% zzEFYObTKkh7+EQD|Gk5Cv%^L{U4pJ49q4D+5|DAS^iFxa_t5!#)2^>Q4+hEpj(s8@ zJC4rE&53e0x;aZg_m5Gh>`t;Fs1CvFmGxqDo?$K3xCD70;L3P*BP(`Kdd`y>54K$B zJ14KlFTV0i=s9Q~)47E1UPFV^GX+*zuCb*!Fdf48BfJV8!REmv{QBYQG3T>2WbLC* zeXcI1JsB4xlMZ4dn+c9X_uoILct#F-OGjrH?pXc|u}ZH6XP7hD*!NlmUETK2$@8nw z54x;Kxp26M{;Th@8UE6o`JNvo;Bsq>wO#dy)`Sx_0@h;1>W}HUncq;0Aue6!* z)^}p?KP)Sn=bUF&c9AvXW!9t9PX$WluhtxU-kD?U>mSj9dYA)gbjLyD8pS9JpL2)l zP5Z_agT^_a(3o^Y^;5X621kbigXO20+u9dmF>jbZYgWROoc(4_xsMXxsMuS7+7C&S zP28V0i8SVWx{&m~lf-s{JM4;S(TDgQI6WF-{SZD+o^sbwU=waTvD=&X{(im9>gcDQ z2(qSpb*1>Ns;IA;y2MZVCdWFuI2RKB($PsDigFxWdUIX#sFlB&KjH)MgMR^kl1W>k z&G!qJ!}JnEx45Rf1(~3z&iU4m%8Sl7v47EaWS6R(mkeO~+3m^h{37<1rD0N?QQ%t;2`T4MRXPMdbbi9P=&cwGOWr8A8? zWNR23x8%2jNLOD**$A*fJLa4+3;Rcv)*$wT$^NkcnwC82q#3)^DEo)jW!WBLc43xe zvxUC;o;7hkqy5~&>yqiGXZLW>uJhBfdA^%C)pHQMe5I8os1; z#unk*Z|c}l#@iR>)UPvavspuRzM~hngMTfBp``6JzJVX$zr2q*wu65h`i(P}W^oTr za^jPmXLx3}e1`E1=-al|EDG+}{xow>x|ccI4!%J^xzRyIYYq5xM)iR{J;isRs?m!w zDr2{VtQ|GzsmDU4OQO#~Tlj+3^-V(Wzcp{}Lswh(YHlbkfgFhJhs`>>N%`|$9ctHi z#S?-2BiZl@3t2_HMfOSf>N>?7D7~84R5O2BQ(9PKm}l6YSl?zV9%*swh|PNByhG+v z_uc%r@qeRuI2hPA3JEb<{b=Sj~X-q4jHFN{h4_*6!lev)kBUfkqM&i$~ zNu{TQszn8X8Y40SBzn8XGX^TkX@1^~R z(rzS;zn9jkG-B*@CXko*@1&WzJnlZZF?fUShVd4l9{yfE;*a7N3I1N%7nGJk8hAFk2+gf*snGw}PKP+W1&fNcKFjAb+XNk2|K`$^k{ zZvAE6E2g=D^pBE`tZdTP9<-BpEea05Tj764`dtSn48ObG|Bm##0uzVd-Qs&!yPI$N z@?85ZzMY_JDweM1x^@F`x_3C=`x6}3`^u~({jNomhwFQX{~hUf9SjY>EBC)6{jNnB z!|&eWe@FUV8%EwmeD7*&D${S`x%Rfo^ekOJP?;{)^+T2E%eV^vFOAs;(GRX8#L{Ul zc46<+dHD_C+L~Y`L-GCv32bZfm;IG|cd!I_`CjS)BU@{B6`X@8i_H2!Gw8*M{0vA9VME_5LLty0`c_^V-}0=I~m0d9{J8NiPTZ%yN3Z&u z@mp;|h8B$D`B!AeZ^2%-nzgJ7n_axu`F`bi>$GCyy#9H*RL^W{-6Z%P@RXfc=A3sW zoAs57kq1vbvt#ST(|cIU#D|AmAAncS==?$CU=g+`5B@>R- zV}8ST<`_$C-=l5y!!pCQQI1Ixs_JlZ;{85v(B9x!}vDxhqM3A(1dUQ-O$O= z-;m+`(ev%S(>^=Si8A}{G~RPh7O16kky(Fus z(MkI_X~KIPe{s=);2O?IUjBK~`uLafC+=-&gm}F$K7l!QX2xHGQ=<4Sb8eautC)(t zdnvxGMB3F)*{9q5waWWWVsqSYPpRK+t``mTmwaLSuWkDX{mS_n{sqqcN_I^s1g^lE zMr5n594jkxW@dDVIME<$#zK3l(d}C7(xppntAp>Tuao{;J=I#*3H?ge!VbT*=xouN zO~_yCE3aRJtjRYefgAfuSm)bV%{mveG!eFr!9SCcb@6@ci8}})w~fM6@C(~}gBhtW zFy@j6dyyl1cy8iZu_NC&xbJv1onDod>htGL2 zP9FxR%=7L$xY|~Wi3h1YJi+iZ#>~W<_+@<2H?(rpIz;_btVdfINASp5HlF{f=t)1F zo$~zUK7X*u-)oukXWmEI9$=Nu$$XDWe%9Rf?~@fDh95+?a3;7Z>(fE?4c-Ij@v38d zc<_h)5Bu-ICpdb*h0%wx33>Z=J$T>+%){~bgbee6W#{+5UV=X*j{Ianv(ka;k(=a$ ztr9-5!^j6a1zUmRgPmgZpcH=KUgC0ZaP~q(r4wns#}BwRw5zNR&$l|A`W+ZY^K0>2 zFaF8%t9mxMg=JQU*2qW?zCM?CTtOT1u{viZ>TL$rvGD|+-sbsO`zCX~Gk#Y2 zRmI0nu5$dW+CO}e+XuC&wgtm8@P4(gF%bXKS|poNfIn+c_qEuHoV94QA9h542k*=? zPaaU*p4Msf_M_rG;HXBv-HWYT=JDjHV)AL#5!WQ9Y4f@CJ+oxwOq*3}Pc ztgB0=>{y6waGW*NU9X}ab^EIIoj$X^j~|~BV$CZ0wAC6au=K6M8uX%$qE34dyGz=7 zFXt9^IdVnl?Xiwb-e+`1nyO1<>u!Q~NAN{iE3M=&kS{VgL#UdwS{KGj*$OT^K%aIv zQ_m-PCKh?ddibf;jGvbN$p-^m-QK+SpA8|LZ5QQK~cl~~9A`7`2xXj5fXuF8saWRN%1wR*6cy!x&~ z)V56C7k@UCZ|0le8i#l4Y=*M8u2 z%4*CHa~9e{V!FN9OyNrKA;WZz&=vj2x6+SpB$oTNon0;TsS7)`o+HGfQBV3&_+csU zbd8eMKhY}Cyc6zLB9kcH2<-8{Eg)Q z(XpXz-aEceZs05hhPe}C>Ak$mW^8VD$L3LVkA<)8Oz2wt+EWStd{Do=@hhIVWo8Ng zS^P)gfipnVmhkX(>TUtgp#@Hwhl{zF#?oI_yJaKkS3<|0%<}K}()VCX-;M8n7xpIB z&-Aswg{M-69VeV6+EDZ8V&Q9N!lP%&Fy7b<)6RS=y_!LdHr>;&5O}{3e!YJD^8)n0n7TsO2u5qo_n?7=?z80Q@HLJN*Tah=7iitI zQ!VJVl1qlrH@x}d$($Nzf8J(fJIy8NxT6)Dc$_&8pQ+b(>b!B#)uR`GJ(}zOx->Tu z;HOsjc_VF;@5wiiuZMiUcmw$?@*R8w`CR;@m;U|r@uP7SF2DOm>SaDC@2)qHw*gqU zy@7n*SbqMc*M}9p*RkOZ43^RL|Nf4kQg+^coBlW&@Po#kH3-0QpD>xbRz zeeQLudri65pSah!$7!$7y}rr4zTLh4ANJk^E~@JNA3p=Ah#T&> zk2@~F;EMaODJbYDsJP_}FgPg7I0Fi90lA>5QCd-14*MtwR4$a7lvY@#xJIUlNkyeb zrA4;=pXZ+E%nXy&=kx!4zn{bQhB~gqH~Bw z5=|nSL3B4!6VXzlZxFpew4Ue#qHU>tdlD5Yt$@v+yad9hDOH}~eAS_chs!$3{cG=X zns)f%;Y6ygaO1;V;VDE16HF61DEh*mD{MeywO;TCrD06(jc4$XEo1Oh9!rDUbXJXj z%17W&wG#gH_*ag9)%d3{z@Hlb%JDC4J^Zo9;85eA3I9|Z;jerv{K@yipKBicRTjfv z1OBN?;Ln7AtPBp7^apdt45|a0K90g42m%Uu7yin>hd<%M0>kI3d{x1o(Q$F;7Zwy4 z5;AgxfXwjan9zZ@&bB}CrZXFM)6s1P3uJTHO!yCBBiRV%$pYXkL?p+2qgE!z8nj_^ z=7%Iktq4qsic2=euF|qO+BmJo_(y+BL^|>LtH!hs{LkPt*+cb2;n39IL>TU6xJ#D7 z^!xw4PQ8tnUr*HaagIU%r248_bL>o2P!ondJCf7c@ly!He3#@h3Bz2YJ6vIhkJ;f$JABFxSJ~k+ zgoV7S?eHbL@HKY$njNksEcCU(4zspgzlDA}6Bhd7Mi}OUB&V?BD+zZXegt9Ik0-el z!gv`&mu<(du;bU*@f!*E=h2zG9WO7e8%a({SWZ|+7%rBQ%Oni*G?FW~i(gH+9r0@k z3;EX(hB*SsHQ0q`uDm>=d;%;eLe8gvIr!hVW40J9pse!yKFB+zAVN>_fOc@l}L{{j|52 z@C_E}x*@`+5EkiY6BhbaPFTpNns7JD&pv&7`(|&S?EAO9J;}5iP{WmM)Eo)<(qv)oYAV8fnafL60&Vec`RKTqcJAg;#JzH zq(p=8fpp^pSh`fJiHzk1fOJ>K#w0EEwv>nC5f-dZGK?7o`5UyxBtvXel8Bd_poxoJ zmY|K6JOmdjB2Un+rszx_4Fjtrtu)e{JejeH6B+9Z%tbOK;$c;&wq}V9=vSf`RTH>iuXbH=#;Z|XLgH*sj85@W=YTjI2ToA!NggROqqe+fS zl1C*bh^X?YxI|;JK`N^nbwOkJ^Z1m$ZS>eAE^{n`HGfO$wrmTZ7yr*v!E|B`Ny#9I z=vbpZP7|e#*Cr&%<28CwPpwY=H$~xcw}!LIN^UKZ+RFdSBB`Si^{Ia*G^}@Wbdubl zO-eQ-NX?`U3}Wgz;o-qjDS*%0)uI(m@xv2F{TF)88$>H0QzynJ#L7{JjIpQ{YO9cV zPqA%OET27p(UK-1)bW}G%`$ED-wD+|+(U(m`B~-q2gzBb-z1!nznz?ILqquJ)C5gD zbdA`A*rZsn9BZ@&c}$`~F7!h1HA&i(*d!bEw+#j9Sd`~4v$KTzhYE-F{(~IOZhkfW;dkr z!Q6&K=O(O3NL-yD*QP{i^+~ab2~?nzKM5ZR&1xGK*M7v<1f%u>EbiS9KVye?0Cq+O zxt(_S2|Ju+ho7{=PubyJcK9HCr^hxwcy~n;+?vWE#;Yww!HD9Tj-407v5Xle{_B7L z{deSvu`Qlsc;sIKzHoT&RSUS*CVV_IvM83w3``3@PZrJM;K!JMq9G9ayULIlr8OEK zo}Nfoq-V|P;o*^=6)R)#ehjI+w&7uVtBJnC{8keTb5;_DX)B4sWLXmmlU)*q`7VjV z%$7u9a!aD3tT1K9F@=YjGLFkQg()xz!$g@xMLsZl#>0t(hzh>&hKaZ);b3;INjflj z$FZPP50ph0IIxZIKs`W;ItlHP>eH&{KJUiJSC>T01nI>bqLdmheHmB z9*&-H7~sINron+_O(&WG6#XBA1Jl_E2gZkfhPI5oL^ua1mNOp?%%=zr40i+$EZ12$ zkpCeZ$X@^g#4)QI2nFS{95!5du<5WhgS$2G24fHA4>Ob8t$@DZ!DdMHLm$o$XIMX5 z=h}_iF%{7)qP0Yo-8nytXf;vS9u$vg7SU>=@}6WLiB=Pp_u~FOL{&u9M9oA+88smB zSf~m1Zx6N_&J%!&p6rSn$Cz@^WB$-v1@(C9+76L84Sztg&ql3QG=uS6)46E|ypeX&FIk zI>};cK#nM1tL%gf{w_UC!yvWFvL^9GiT*x*Do86z(q5~?MC`wpzmcUv$x$m}pkAT| zR;mA8{#>IBPzwk;rU~JRvk_Nlnm);L6SnpqoFpdA5x}= zr-yN)APsDZB#?*)q=s@qJ&6a&Bug^JK7zX0w56?eMJ3X*6o_Tt-OoS85EfJL07;_E9%fZ|o}#@(8JebRFGqg8!u)*=+NrT|T3>yv(RXH8uyh^7Ygw@6 zw#DB7H!yGm7Y3tc7(WK4U^MAkfZ;;?LpAy^>7HNW;59<>$LRs>Oc+9kCx-Y32Vfk$ zj`-^Js>Hb1sMKj%F$fCR8scLUU>+ecAyk{BON_>_!YScn12O;sM1zQi5LFQkCmKO? zG0|wEdZH;rGl}jdY9d-fw4CTcq7_7s5v?S8if9$lGeoP2ULsmUw3cW+(R)OHAu7^$ z>c{KdnrJ7Ya-s^NK173vh7*k-swS!@nn@I8iY}Y5iKv-qInfHDl|-wERuioy+CY@` z=jC=IsvxQ)8bMS|R7X@#G=*py(M+OQM6-#Sh?ol% zK{SYH1W`TFjYPK-%_dq-w1#K{(XiwM77BM=;mKMyALb6jb;)e1A(n+{k{Ix!HK`2w z62Ot7o%dLUU%~Di)xPlG24ZFn5=V@eI-l_(J?B9Z*Ok7RQ^Ah86=uI#62hPhOU6 zq#JrjGf5jCsWl)29HgI!2=cJ?#{4YUHx!n{;XoG}>J=UBl`5r+I2zn=c_=tJo}_|9 zZx$3ZK0bat-Z60(CfP%XFI_=EK7g^1^Wh&2NH|=JU?FkrvAQA zN^~lscpeHrFOX?8{HNN~2Yv82oXMpy7*1~S!H_(35Ip%Y9&(R|e;j!Vr-fGWU~@sf z|8Xb*`5%7nO#J@H`=d_~+!6oPZzJ51+W(q{PltP1@sqKC>}Q)o2DAPgVBGSL{6u{D z|MN0RsmOb{{fy*l-U|FNODrk)uYB z8S67{`~;X|o}$!5!UAK=GF|NQ6>;$iiTafWV^Z>})hVfK7A;=#$Wry6^VhChzv0n; zcm4m}`Trlw-`CGSAaLrmpy25wFtei03tD&nP?8+glB#ifL=&A^J;E+`l?nt_Jn2`dTrBOFAyKVcQ&0fZw64`%C! zZ~);3!cz%15}rnwP2lYxOxT(5bi%HLXApKHEasc#gl7@oov@0qg77@TK7_@5w36^b z;s+5HBViTcM~NRnSVjgwP1u33p0Fd~G{R1VBiwj>n-R_;esjVm!Yv4w6LuzCNf@4! zlAO5zY(==5_^k=o5^f{H6K+eGP2}ZiN7$9HD`7d|_JrN3{2d4f5$;G>LE$?QRub+^ zSVg!CVKrel!g|8p2&WP5PB@Ej55gwGJqec+?nStgaBsrZgyFeb$<-2;6K){fmoS^e z<O?WEdTEf!^HxLdc%%<@APABY2cm`oP;Sj=>gewWh5w0ejPPmpZyrV*L4TRx) zIlTR-KerGA6eX8OOTw;%T?oqww`GWkxFcZ|;ZB6rgu4*d6Yfqpjc_l**@TA^ zHWQvexRUT>!qtTR3D*){LAa5yj0RNBzP!Fpgxv_YAnZ=KC1D@JE`);!wl+z(+E!>oK09p14c99mV_$^yAZA-+?sF=;qHX% z2~Qx*{CRz4G@x`P+>)@IunS=Y;qHW$geMS=AS{#f{-Yz@l5h%P7s8n$KH+Q;e*lkf z7V!yJi1>u7M0~Rezh3YOHwqlU`Obm7{w_fr zy9pe^vAe)3j(rF_t>ZX|aCgEHg!!y4gIP|TwPi31s*$MJ9}z?|jZriQiL=D$aNQ3b zuKsa~v&D${TqW)kKo>(ZtU3zMXT2G4c{p5uMYo*N#eEp)5-8mS!g|7b3XiMu=y1ju z9j?oxGqM##R}#fpX9lr!M0q%z@kSR5H9?n5@o=^mozYS+UcNYK_8RxMa2!MBT}AcO z(HyS^b~T{WQn}(OT@6SA-3p3_tMBM^6d(6%pj$!VS5SIJSk*?COz9?3{(4Gh8Rdhk z!02#h8{G(1O zL8vbtEYLC^je3InYQ%jB>Wc@=`iuJ@)EmTslpbF_o@Uv1gL>lus{!J^3iSu^914$m z`C%4}tMkFI9>N{!l?SXwi1-Ph`H01R80wh^%;XC`>KoQq zL{X;DBM?J*yi14V7C_hnuBfMJJ1EeX^H-fa0FXVxG>H(#tZVtI)df}Gz zP;aq(B0bb!tRI;PC=KQ}*QUKtUVhU4ER+E1HRdPei#vg^{Y3t#=N?cxao>shj#$VK z^&ayR^+WylfY9Q;7W)C#kCx9M^co2-g#QpgNu+<+KWy~|`-um{67|Ax9-u`+?@}bW zSn(Puz7=EtvJH>@%mcJk)Qj(SnQCb_>~~lXQSNxDo}rd{;tmm8z2N()LM?K{ev13W zgq~x6MJ)OS_FKdvf9${5F7|T3{%os1ky5>_<-&e$TTblnwix?;Q_TB^s2AD+TYkLM z{?_!-KG^b&cIlzLz;+SoMM?FX1{%d3+7a7!CqodW5+}P=Pt@RY;Qjvst zxk4=chL@3i%0p%k!IVsV5H~ZqXwi z-q+HuJbZ{vc`*Hjmi@mxyy$JBzs<1WV|bOtj)?k*>pV~2*QWj`ow=5N!TEl6@l|&G z*%tla=?kmL$K9xH;=0ZGK{k4T`OUPs?ju%N>TezI^D8~-1v)WrEbP6QrytJMfSJPh zeF-0JGp3|<5@NG*;@6NzCE=@ts|mkMxR$Vqa0B5d2(xgW-#NmrgfA186TU!LL3kfw zCE?cys|cSWtR{Sou%573CrBfFhWJ^8PY^Z{t`dA&HyA>=ocJ#gt|GjGu!_R>C0tAV zD8fM`Um4*B;*0Sgo6F_1p7^eWj}TT-e6ddGPJGdCbi{X}@IJ&BTGeK{$x;F2WInYYFQJA0?bZ_$=W}!Z!(L6aJX6neZosD+qr?xQg&Q zgw@m^`xCAqzLv09*A(lb^~8@OehS4OM7WXoIuW1R$Az$S1eae3;R=fHN!X3}M#3hF z--@t1@plvUAzVN>h;S+42*TpJt|NSy_$h>siS($yiFMaZ;%5?{5q~V>>`d}-BV0xNGQu^4#eG>h<=2Mz^~9e=IFr(IC)`N<<%E^Q z??c#m0dF4-VX-fvEnzp}(=?oApF%t0yA%Iu!ajs&6RsM^(|08tM0|0-E!M?7h#x_G zezk`8Ls5CXiLWDm3SqGhJ)Uq1@iPclkK_5bC#N_#65mAOhY_wPemvnu!jBPFP<=uOh4?yoazW z<=2gH3h@_-_{8r=IFtC%gtH01LD)?AB;g9e<%FvUR}iit{667&!tWAxqx`!QZY2I< z!eUcs*oSZ-VHM%$M0m=t2VouYpC_C`cnRTJ3O|x?Ch^x07W-6` zgtLjijj;0sUS1!{!4_Fgx3;Iqws?XM-V@ju#Rvd z;S|D-gfj_$N;sQv7GX2t9KscZUm;vYct7D9!cz&?6V?;v^RjDTE`d9KpR0lUcUv3@ zv%9u9npQi+eYTcXrv!$N(lrUcoK{l=e+8|Y2pk8q{I==G!3>-&#<>n#oCvcIws@r- zA7@Ez`8dmIi^VLr$UoJlzBt>-X9J^b<^j16|BnN{NCDTQJfQR4 zc~0*5d{iW)fX^6O)8XqhI4i&%U#E$($QjGx0a6xn;_EiiFw4yypC`lgY_avZ49>@Y zTRuMJ+Z2oIpqK}ar`0CD4z!GB9mM>O24;u3mhy5eRx3sLC`nwW1aXdVmYMWkyrTsWxtU8MH^)~Xv`tWCitoiFL zdduVEthjCZ$#(ozHtFF!JHDGjgvWIcOocnX4i#&WCto)dtIoLYjO%n_m6fkUqr63W ze16rcKLQIo#4-Mh{BZs?&Z3W;A1^)2gdzAkh_!rtoyOWP`MRa`xh1~-WXs3&vEOpX z=e5PEHm(z6{l)4zV(hOPi~jO;K(U(5*VoZ52zl{!4r_nqSggK__aiKqp4q|@@b$B0 z7J2dYt`(O4!Pk|n^NUn6szgDu8!-DVwD}&t#N(M+8_D) zpjb^uKCU~7+z_K3#DDJix|S`U_ctpaalA$U`1&9IbH~?(#qI;XAHi0C_&Sl;rGR|Q z-!?p^Z?y+}9nV@GzRoOmHQ;+PP~Rgh^5R(Rc3^P5<10bJVhAjHyuhMI@^xylOM>r{ z;7@7u`tfx>YkBxOjshY*AGMrIG>N#B$0wxAF<`* zdWkI`B`QiJ))TB4*U|B*dXb;#Klru_+{?f@e0}4O*F)}a1i3#fJomS~e)9D^kvrBG z^;MJ>F|POGQ}v=eSOyHiuiSi{)+%rQMhaVu>;7Vw3hv{;+FSL9?*qVp?)bW~*!{xy zuXuot2>ZeJ3Fs}?Z>~N3-&Q`h{-wvp6x{LkcWZh0dI8!KZegsL_aK2qe2#UN z@^FlA5a5nuqs2aOoMgFv^7Vde{uE7m-;p>MR0by%2{NX)dEb#gtOVg2*<4rw!nuXU zrF@(YFSnNFD+j_`NBY7labHRV-rFttBfM>1A&J!i&ZFRnQ_EPK3UjgYTElrKII4kH zZO3Z^{vj{`uLE8k@xbf9#PL$_q^h^dnEO)j!Q)RHa+EfFmCZRu!8C*e@%@hHN;sr+ z5sGUh zi3xIFPkBIMf;J&W8yCG8UZjPuaWgJn9h;DpoUoYQYra@xh#xa*u@PQ$sMqLYb&19# zPknSG&k66%#r-+bY5wr}K38~m6DX1=yp9ih7`|72R1M()@lrPnxDWo#g&&UOWK01C z^n}OplQaXQTUdC1x%7T0PlX~X1}5En1ood3J|M#17a}obQ@@4i7u3WrSPG{CKPVRS zl;Q^aV>orwa8g`d6F-#{SKlPADgxuCH4PVm0W+KWN$F)Z@e7yI%Wmo?g)@O)HKgIW zEG9M@%#_|y#83qpnVR7n$?T z$Le5jh{`n((|zo`Q&q;GIyFe&0ZLLb9Nv%S0q;eCzKbp!&P_HLzSjb2!TZwcZryT+ zG+d-Ksxt-v#xy|O>^6jN?FoA~DUDjd!vSw3>;vaY8@|AT-vaW$bTE&ZOz(PgQkyL>vs~ z{iO4Lbsd-zOdI-Nl0lNU1_G}e@jRKz*&WX}^^;^Km1hFT2@dBUIQ4_@<1i11j&*6q z(wIY?Lmh;5t^@zhOsQzgl#ZXeOn=Y$^ybsh0K@BSPXCV2<-^N89Lh5Y(!@HT4&XZ& zBcS|qq%vaCgX*A;&K;@zJyAZ7Tg#rQgzF$2u0L`*0K&>4JuG_%DlgPUDtAYsK}1uS z$}JJo-Fl(Ld8r!il1#u{u^_N(zrdku_opvf=Xux;}s0_2xtWyNCh4X@kU5#S^EO=oTfuOIPlyIbe2pd z9|_>*1((yAr<-H%eH?WR+b$d0?inc4;}G`d4r`f)Fr`ml(3kgGpWb%bxoUHjlV6Lv zCO~@kp!3c2U+Nx*_+6xONd5X3OnV6Sp~w13=L(n@t?eXZl`)LrxWWyZ9`ziV)8=?d&@6%7$ytC zplj4i1oF4aK;#(!gMAeoBG0zW)gp`9d%PY4lHeWUaA0akn=!dF8U<(%XKo{Yse?Y_ z+6QGoG9C?BVT1QDm3)vSBa1$xxFuORK)MIvmMnS=TsME>WxWUI&TFl>HRRHCl{>mZ*~HhGPKw(TvIkE{yRdk<5}ccR}GOTUn07{kYGX7q!;Dz^xPbRe^26wXmD#srPODr4IX^8FV^Ox^_A|h6$rz4fusf z=~YYV6dG>AH(1^+@>zAkHTI)I20777s$~~YIClm zqTC@TK!$KErf@C5es;q4v$3C|z0iJ6203?<()pzh_7lRj8p1-y5!Xf|Q^@};?mK_M z^8{V$C8aA}7ck3P3eY9Ejzhn<`eC^pAkR_NFy@6r$U6|=PZ-%3s(ox9%Sm&U7P{>DQ>;;e+oUwJ6xLu$37kR>X z^G{_rR;o|aHokTf=8ZlDU7>R5L0_j!@@lHDIF6LsjO?eVhwImj$>Cr>V+nCiOCeq3 zL|YDehzy4}fc)^FJiFmFA>BIOafN;z4u?CWi4?Chl5K?s<>TZuCWC=8_`|I$jG1M( zao7xYU+L-ymw`_m{ObI`qdkp*psl5`Q9GipM8(*tJ#&@#rY=$y@ZG->hVi5`j9WoB z`_;i^Px6B%kth!d`ZJ%O>EOQ0-a)=B4gAdy;o&_EI%ry{ZQ&ZvD(6XFP5#8(ifuRr z<%t0?6}(5q`yU4PICx)&_iV!9SsL)&fv*6*8u%(3z6keM^e)6fmqoOk=ozAT-)QTc z?b4ql3&S~!fLM4Xev%Z{cMhI))@_po>Mq$173;Q+#1; zB%7LOh}R^A!q)=eyC=}+3>ZHIK7JHvFyQxt>L3ohYj>(at7S6+=gbVWd@gd8(;Q8# zQ41e^6rXcRNZ-vk7WG%cFlpT{{!c-!*4daI< zMkmK3Nduqb z(PQQ2vj(R*NpW)%U}iWP-c6hiv7ij_9g`K>=&A4-Oq~>$shWW)wUKABY-*g*Hx9xv zb^y1#qY|UFszjIr2PxmdkgBNgM3flH2KWewdgqJ*n9xQc)tn^%PQPR029JX0D6&k=F zKHa2c+okiVvG5K)j47q|qU1LdrrptNJ~KH68$rUc(f(K^(0O(e`;f1{im_tMc~)R3 zo?no9mP*p&FYJ1mFLQJXg|A1&`lTkp)oOlhwANpzF|fYP#c6n=h0lU9)xZS!l2c*= ze!hvn&KfSJ?5L#u4`ZeNc^mpBM2G2P6C|ByHBEaor1_3xi1nS>fKOxYByp4=+#jG1 z3Io55!|ovBMM4ZQbdq$#;FChI??O7Cqvdj6fx2vCTgpL4LD4ZSzvP%0SVMu49;W9H zU)4&C)6Pmx(kCaOG~3wtLW4ufewY@TMoP|W|h z{{<2lk$c1AxnYcbuta_#<`Rz7xWn-}p5Z(--f#T(JJ&yTJa74*y8r&N{VQ_#^B0x! z-2ePVn*WN@{rmFxQ?+pAW%^UKkpKJ2{NI@K|8O;tIfyroaT(>q&7e9XHKQ+ytndau z$tmD8eQ)hAc&6NtCQ&nle>3lDo}9D)@Spx0_fir)^bdyjrSOL#{JGq^-0tkN-1^)_ zf1dt^94W@^W{l0g`p8u|TY7c*)e(OTf3fD1T9|wE-asWA#P`kcyNjlGp~CMEi9E!k zW;0LcAuP%3-_5r#SHqTvmp9Fw{D*Ry*8Ab%?9;Qy`b|8ZB8^jj`v5jrCbt3qg{KaX z?myFp|7g~jE?$rSPuy>CEywtonOr`q=l^TyqvWd*BBX#2HF#-9_Rp|r+@|kmBT{98K7xEFCm46gKI!* zfz|=V%}?R*t;}+u#}Z-x7|<%9u!)4#0F?u+2dXDp3A6#j0c`~83hNF?RY1{Sjq4Dw zW`OGv&OjT0x&l>pWlRn<4d^JK@@_Cs0u=Z4;odM6(8WM;J~<93&MhOwdFC{rr~2WZ zcXf-n{+!kTeUe4WbORW>3{*Lgu^m9Gfu3fOP6~KnEQduqR0G|?K9#9qq3&tNFPpo< z{v!>mWqP1y%m?-k-3E&Lg;ZMD)9Ho$I$15yV0KI9qhPEXOJgdD)-oO8G@#d6q(h}Q zenYaBH3BVx_&OL;Oag2MIuK|zVb_uPtyWlZ1G=8YIFtj$c=hD(Jc_X;fOSACARXt? zP;a0$K*uqS%ykTdcd2M(dZ1b;Uky=Q%gS+xb#fn%`D$c3i9-HBF~2IJ^+2&at`o36 zP!6EAkbVQuQ$W=dK|g@XCo$N;9VyEO8V&kU0kpHMPF6h`{WY?nDZCt6K<7d_^+4gf ziVhJ<&_@Vg1vDPY=i|%EgL9_v)QCep&}K57V}l>8ok2KN0H_J-Um%RfAYB#E-GIx1 zV)?3w)&d;}d}r8mffV;%tcP~30XmM|l*y-IJAEpv2U@ud^b+>WGy*M$y)ft5myT&O zK<_}mD^*}afx3mkw?CmiKH<<_5DwR^LNQ&KAn^s71~eG;2xtKCYZowf2(W1(=n0fB zZ82j7kiYv9SZ@V>#3PJ72lZ_LIvV2Zmcn?KedefC!yY8aF9qllc1l(QR0~w6fp!J| z>?r7`v5-E{2EdIl!+H#8trnKTfGT59G?gq9=x)IJWgsVqcO5EYu^oJ6KFguqWv3i! zR-k_S$#iir4+ZI!$76neGS>u7^+3bHU!Dl%1*!)+TNdnCsfRUL#4EAfl?=Yu%K8F@ z@6kfL`pV$@u^{hC*e{9g<0pgfy+ZkYWo}0JhCTQP0Y$zZD8|bI8Vaf(ku7>*ypg}OZHI6A|*+4r1#{DV> zfkpt8uK+m#tpV(_4)%WmH3L1*Djm|+V|{#O6+rL9&ffA3_^g1R%>7aPW~-kp4d_M) zpOOZ0#rWxr-GLcR_YB7J0po{R=R!TyKz$({6VP;MH~8Kzq!Z}S05lZJrQC#i?I)`Q zs)c;rH-o-Fxze`avq-+OdZ3dq-%Pldfb=4WW&(W;@>6c*;i`e^A)fAW&J7C`e z(3B?_+YZ$AN$6KV6}wUId}TF2Ps1F)`Ds{#2mhL9K+ZrLo`pSeu(>zwImTXQz7C4# z8QX+%06Gf%tM`Du0ad>Q`Uc_QJHMbu&<;R{K=~?xUIYJ#T(Fbuqyu~xSZYsGKFC3K zN>*FI*glX$W+B*RNGH?G>kpn_FF{T~p9a3qe#U-f!Hzz~*iTQ&;5)`3r;{@HPBFA6 z=o!#vvT4mL4nRKxJ!ECj9*`bQ9h6_P;57!m%&~`xNM+jfa+d> zdH_|wis6(FRX`6x__Tw#m)zG;bqMMut8z#?jO92bQyjtapOn=AmGrv;)Af~Q0!3U) z*ylAUFVxrkI<{XWs|VUC9`;cm<@#6!^aDsQ`xwNRfj%6^{^2WAzJdC8O6Gb3_LxKX zOrYsdZnsL%XTUzEpnZWV-iCGs+5q%5=I3C32ig~KN)@yt(5&}im4m(Os64~lMF$l1 zu@WfeTk`?vJ%&4r*S)C@x^pOB@CSMu{2QxL@1b1h(Jq1gxBz<*;MtI%i&+0kSp(4B zfXi#ZUIKN#%BlKeXn(-<*T9Yd_4x$$;Q+0;3HB6d_NTm^8-Z?ubklBOz2B5o1MLad z{jATR-^u)BRiC3C_{r2?K>cMWWx6l1oB<9&U!gry%Cdltg7Av3u|6kd>^Al*Uq=(r z#o({Mi}mr9Ie&wC6zJgG0Qv_0CZHcd`BT0{|5GydJ*?@2KRnMOT_17J#yD2XaNo%e zxE|p?DR&rO1l$Mv6Aj}dq8ot*z;zDy_9g+veY+;0xVP2`>^tsLtpsWUng;eA_kC^$ ziu+749_}yo0gQXng1~O!{zEsQxVJ7G><8}0as$1@eK|;RuM<+-&xGM|&k|DHmxL7e z9U&k0lOV=DHBxw@xHknc?k6dS`iA%C&*>#2>#$5(GSaR$x6`?(E&fDJk)B@>Q@h1)qPNG}zlyF<7p})y&w0 zWs?Ta4WH`aGguBgT;L0taq!U3q`|3L)2n$+%QGB#>a7G&tFiFy0uY z)5dFz9`UhJhD2jxOp-@bV*GfGG2U~P_h30JMaRa#wwigixj|BL`NSkcvM~vl5-GKQ zf0NqCeo{P$Y1Bp~!}@(HVem0%SHed;leEz)L+mP8KU=0XT9~%%z!ZpzU%d^{uF}TI zI>5fvkFax<%N}n)rGZ%4TY@8wMbs1C{h-wiqu8= zqO_u{B2!U$QDsqeQEgE}5i`4*{ISj?Njg5?@Qa4wa>J#d|&0h>V38Q8uqdMuKVTt75kO@Rr}TZ_50KIXYDubFW+Ce zzj}Y|{)YXm*tJ+*tSD9%tBTdd`r@?WtYTAfd2wZNb#ZMmj3P>0OXMYr5@iVp#Z|`O za>&ei<#`o(HF>pp^?AIW^-$ABsH-#7)~(PT>gxkF4uU#IK&?{>GYhk!)@G=8MWNoD z0=3A5dSpXQ%utsKs7)2rrv_?N4|QsUTBVg{mS&Y^mzqk=rRAj+rIn>srPZZ1rM0E? zr46NxrL4@k%(cv|OkU<*rYQ3%Q4hif*rv&q@yYH~BlP3|Uz$;YHL1({T)2$R~RGwDq!rZiKg zDa({?GMUV#a#Mw=(o|)tHr1GFP4%V*Q=^IHIOn+LxaG)m+;bE;J~__CZpH4!KE*-B z5yiUVl;X_d>|%3qMR8SeO>up3W3h9ITZwy#Pf1WoM2W5>r6jW?yTn{lQBqY>Q&L~j zSmJ!Z?ST6Mp94V$A`a*dq#VdRkbS^>pyEK)ftmyL2O1AJm%5d@m->_jl}426N>e~e z*&v|`kWLLqrm@r+Wa19;2m)E?Kn|H812fdWs;ma;4mDT6H4yaJ8S3l~^$mi$>Y$#P zP)9S=uL|l`5A||}I=MrAf^s5qbU7(GnK{`x=A4S0s+^jf`kclb=Ulg3_gtUcAm|M` z=na{<*}3N2irlK)8dwi#%yrIl%X823$qUMh$kXMeCC`PupA{EGal{F?mw{KkCe0=EKp=vhGp5e2$}l!DBH>;iK^ML|_T zO+h{AnzN*5KA>X}g*wtOGiX;;VNGFuVPm0lkz0{_kxx-jQAClhD5WT~D7(m9R8dq_ zR8v%6)L7(fb~C%1eau1T2(u0}G844X44PO4T38Pn=)BKupZh+aeK2%li$PxhyZg4P zL|vjUNh`@JF_n~;RF+hi)Rr`qumi3KnmRhFts)usBJhIp4d>v zRQy>)20c&?y-x`}PYt~;4SJjjdRry*v|8w840@OxdY3Xsm7~tl=cMIi<(P8Hb1HMH zb82%Ma#*fwt~^(fs|4Gu2HTvLo0V(IEzhmYtNGm8Z_r=cVOk z<@rFVBTAwFr^(=%Ik*=D-Kqh-as$hy11p^gcDf2|bOY!LGdqJ7b^|-?4we{oMg>}< z2EF07%nY`+5$?lqFKs6vR-2+QA^eZ~pDzK`K&XR*15;!SR5oupbaT+=AKS=UIG8d9 zdV`mTgG}b#PSK`$OZDa;nby(CLDmd{I#oA!8P;6pB+D4IH@3Dh&7_41Ncn@o)1cQ zS(i6o7<3`H*|nE~ukXk$nycMgoah?5xLr+d&8E_OLoykRKZg!^;@RB`#y~m^kK`} zaV0zIC$RyHZGJ6cZpz!eZyXvl_~?$2&qtL-{J64%YsT#TrX|5!TdlmvM!~)Ghc7M& zKbZI0*lwCH7DO4;Pj-AZ_-2M{dCeys+yawAyT~B)_V(+dN zKd*N9{=nnAJ_>T#Q~j<2U!uL_lj-NYmUmh6?t%OFmTp*)^q$Y!$3B{P$l>!JhoAFN zI>1yj++TONw(NV|+P9lcdwkeX^$P4LBM5rNZ5~`zv;kv1fm4vFW|VS;@~m z898cmZTa>tx3&ay_SKesd&_yz)dy<^uv3gJeCNqeKj}3i!X@&T9v4P$_;`)^$slyWfQL z>X}uT|6N#XpUJ;;xO#EmkSU%oI27TE?WTcG1-<=wK*fh^cVr2OpH=e$8lKtBDqqknVu6XBkk6THO@yqiDCvE+r>#&nIdoeZ4-E9oMQ2pq! z)xSP?k;QyA$k(rL(*1DHW0~)yR3BYF;qGXBct3Ps;Po#~mHsrmLsadAs1NTtzcTB7 zzaj0OYxBT(=-}}-H(82|70mjoxYLZm>o>eJVfaPgvk7aJZ|~`U$?443J%3B;G*Dg3 zGGy$BxKZ8S7_zifwxyRMFRbIRTa7z@*!=05mA;dG7aZLF9q1ukk4jFipFD8J<(GD? zIdXm4;gOrm-jITB>@H_JGI zJHEUB)@KjeTziJ?e8^3vof%em+gU6pdCcb_PwYE&)ZFRfl)4?QFSOk=u8l4=yzb8D zn?D=;?RO8Rm@<0Y05kNPBE#`>lcR&o!NIkS-}Kry#%KPnR`xyHi8WluG|4{7T7eZb zc(+otZ0Uk?9x3FuQkqE{RR0K6GQ&&Z2UZ%Dphv8T#Pj9)RH_y7OU|6$~k5E7!8M1!16$Z2K zm?3KiwqNGrkRg+?w?5ZwT=w?1`)!=_1K*2UG5f>u?fs6sBqxXNKG<4yXprk~y{ZzQ zT+#ZOFS;)d`X=DB6EA2!==@~l!iwLQwb~Q)>zL)~4~8^eAEw{*iG$nsDe=eF|MYHr z%Smr6U$y1SZO0ooF6q7Mblp3vR*erkfBxdaF;BdcchjrK_#>Jt#@jRFM{ZlUB+z5V z%c{?P$B&)2`P#1jJAPCRtdIWu`o$-_<{#Q3|GMqY;g|bgy%(A?^_Jsf-b1D@ZtcBo z`u5DXe^e~|sZ&|!rE&hjEq@);8-&nKA zMea4D<>{x3cD(Z7O4Q>s3k*H)?p-4@Hg>!6TbtmG$Kk8x@GMJShO7c4a!}D0q}i*T zO!m8zlcPh6G(~H~9T7Vyn4$}!frwfwT$($>m9ts17LLveU-XwdjaT?6#_%ib=3nDl z$P_~qgGDF@S(hHVq$K@#FE4xj@$y%NDf(a%{hiztUD7(;JA6~#>$f4L7wR+xujCGV zrK;=b1Vt|l(AKGoQ>QTjn|J2FULNztF?Ww%0b{P;TE6C(VkNTsI4xJ`6eD1y>bZG1 zO zBzkq!(*AQ|AIo^qJ?u`_nH@P@jcx9IuN|kK@%^4FYt~+EeQeT*>K^ak*Zh*~%vK#s zZ`)XTbMd)T?G0b_hz_pl_k40^zxy5|eA2JpQ|G>ZaL&(*Yc&G~&U)p>=^^umvxJ!W<6_2cw5D>poQxr3_7II`Yy9RUCMJ>5^Bx0MZmjnRv~$Af3HWa@#D8ZVk5`YlJL7{V9=*Qr^^E7< z9vALD|8W25r`om%968%{ef3Z4LvCucm#Jse0tsn1p$n!9%ugoc(#m7gJTg?jAb4s4jW(>)Gu# zKDyVt)0G34zl(lr}6aW#_JzxZb~B zQu5`z-**Y~coxj#dp1Vsp*G6ahy{L~TXyfM_cOdbW!I-r%3%&D`Y zIoMGLnCod`a%G#Pt8cG?b`~~O7!5Er@ro7ypsDc}u@nxQ$NnRx#$skrWy!pxr*qS~ zMvVx6F(E;G64L*X@s5^J>bu=3|u`znQV| z#Czu)9e#bg=ww^D-|({4-w*54y6?iFN1Bc5wY>DX#=*HUCtB`T-&mDDepkE3SI*b0 z>lRu6NXEe4r(W;5c6i@UuPhqgX|%tS?v;;|9cm5lW?z{Y{aK$NSGT2GeKetwQwMB` z+WzF6;blX%ewNbbN~=x*hHo~gVS5!+^6mF(r$ z+>?(y7qzM1cFlXKyEiQOWqZ3@iHBr^4#yST-7>pxU4;6D*IRoGi_bl^;pnM>!=^bn zuL~~ic=+ZzKR@?(+~%G09d$Tn$>F56_vel5=hbh_D>HJZKV5UbaOWH_niF8y*q<9s z7mYj=ZiB!Lwl$Z{V8HljQ*+r-(H@PTb4Qo?8lw&dcu9!~-ma2fJGJQ8VvaUCJ~1KM zySJhz26O4yIaDlq$o&%y`b2{U*A~2Suprq`xh+3hD~DB(*qGQTDU=))6Gx9Q=p5xe z+I!rn(IZEKA%%gS1V6DTeT}^b=Ys4~ku;<3=5y6O;$elZt|7{qIcZ zJ7+#Ke*W{Nqn~+rj3~VES^AXI9}EmWymIKt%ezM{%XR5lzBIN_`SPhFuRKwmoxjg_ z!T5{Uc3+wJagtB|^fRFYGk%<25Z1xx?)Sm*E1%pDb-1{2>B;w) zy4CzM=Ohif_#RDYHQT3R#{1laroXR9uaYzcI|b*y=+jKm3MAdD9ga7___aeI z8OUY|M=+2!7Wd`JCu1EOPfU|t${y6++2Nb0=wTzbVcImt5QU8SYS6hawR@}ev z%*t8|_0aoMXX<2>4+ZZqXidZig3wr4d@Z2c8sn5%zhxmY> zq=l$`(s;oM`HB=cBzBt`45~YTdk_&RD=OP zceP(%U*bK)yd*~N`{FOjr!T+SecaIMP0dt~mpu92sa@}{EO;@0cVa~A2MO|1&fbSM zy|^L$rwfzbTjaJW&SmrMel2?xB#oN&sQa;}rx*7+oL#=8W&Y|_8@?&pxn$!HZUf#w zA76MfXU|)%$!Wj8IO5mO&-stf`0?x$Hw{Byh&;0N&ePq;E%w?SA9O^vbfnkOYaYf? zTb=w|dOn`|Q{IEF-#p0a?s}v4l{cUMdU5M!&rTRU*)22lW6EaZg^<9EtvR@_Zl1va@S09Ll<1dTrcumk&>T_D4VrWR#?$1NQf9?H&{PtPho-GkyYSeFh-S+h5ZtM4cnR2{sN#EoaPwY#m-M)P2 ziaoc!&>lPHF){v{)3s%RV@8xYr5`i11Wm}(Z!f;IbLU>KH%BBLKi*#J*y4k4jy&Qp z|LW#T-Q7o(Wd^@;uHbscyhPppPMDswxkQU6#Oa zwB_!2_b^1Vm_FWMCApI=$1@J2J;O?lL0oK4jr9l>0%STdntOL zeQejd6D}RYD;ngp49j5AdJQ+_Wc-_TY%yxYh_U0mM>e&`?WJxG7Yt|r#5gej(TO&f z3%|tVICu`&yNjX|X5rGY&HPw{c3QH=5G@Z0@fZG%cK&~Bg$t{P#*ZAJ?AkT_+?=$l z{u8=&dLcIM$l0YIe=s9?;A0y*^!?(-p^Sm*=$y5?+I3peeNz2C_xPaB@4tUEF1YdC zBc30=clCa;!%vxa!=76HQQ(PVSwm+nzS8kUukK;qYqoVOh-4wr4R0)%7Uwhn!S}yy z>#S7;J?D34^a66CWh*zItat&np2t=dJifb!J$P-@kli_m_Qi;U|*DTNhmRk66x{^?z6Y?DFwhzpwb}l8xQX`u>Y~oz`FdWY5Ar z=e@p)+^AgM>_^2=G^_2M`YZaH`iKFTv!e-gsCB!!VIH%Bf@gV9+czEH!khw5FWC)n z9TjWQvyW4f!k`#y8f_Z6+1qNT?FNd0^H9l2$5FjKmU&A?`4188MyG!oaRz2CcM4GW zDaKj`iH;8c)abHB^JdG^Tgx038-zSysBTc`|G}Ypkcb6C^@*bDe>E%6q%!`#9w-QD zI-u#4jm?EhOb$GHEA-M!udj04USS%w>f4>YcJ_ZgY|U-i;L1-nIMqcAS$*vHE6w&QKdhOy_+MeD|9misL)#HBmIY&trWy_PSR42+zHI#6(6%V-LAzHDb^JE^+{UzD zO4CN4Km6gldBaP(4=?$4W6Ai@9mYTPb6PM!YV%N#WE$d!a^&-QzLcB-b#->c1$ z7Y+t{mj*;k&d+(KXv?hr8&1Y)2V~8iobz7)b#ME9d1_5$;0x^?j;3zMuTU>6O1AaYY&!B>M`TMs`BlN?~Zx>v9A>7kWapCH8jFEvy^Quc0AuDL*8%I*N=qz zbcnopK|kQ*JAL#QqFbhUe0uoBvwglh+V}gok857-wsxCp?>D!{xGaAD$n=ZOrg5pi zUOzEue9M6}JKz`r_f6sdfA8)c6_1L}4<^H?Sosf{j9?K9Ova>t#AKk>Nq6RW2NO8Y z_8$KUZhq6ZDAG47l%~n1iJQlZB>ymhD(;E?EWNltreJ4@|2~Bb*(q+6Gh|2Migg5z zS8YvmhAdw(UI+kg{XMC)f0$f08sj{o^hOVZR-b5$O-eMRLeO|fF4__sEP`xa?+n=- z*w~%`o4@4@Kj)$an#f|{A793gAJCA)N0_zn;{7B*_yGX?Xb8T(Uk+!95Ys4ek{~wv zVt5U3qTn1q-=KjIcy602q5w!~85|;Q%tgh#r2jA}jRaUj<)y&zb07`;Y!QAkU>RT) zq>N?4a{Gdh0r*>e;ZRGUU=_AK} zd|=G!y3^y29_rz_Cil~O{(fCwIx#aR^ybSSf0=%Luk5?J%*$W?*7DcBZ{A*#cA??Q zoBnV8wtCpdKYjS*$g^>mU+FgCbI&WMcg@OF?)>29{SDu?duPEsL;csweUJCNJoWOr z;P+yDI=^|M_DTKfE!R%(@IKvoNbblxJ3ig=!PXs9di;L5w)?Y>ue%!mv-zD@#>cE! zD|_{Wn!-`HJIIQkjwm=4Hs7rN{0oN;rq}vM=Y}3#mp(AM?;Gdt`nmZ(d$eF|^Rm$2 z2Ni^BX560YTcJH$k@}MJGp*9MZ=3MMyU%UU8=8XE-^py&7JZXH-$6LDoiZjU2ZL!)~umn`h}=)lIv&pw~ge%ka; zw{1)h9da$c*PMQ}Er$)$U&uGy^?PCbu$$l9KY4Na(Ou)l4cdGy=}5QL6VrXWPB_tU zdYftXmD|Hy?vFg$X?dJy;)^E_#tnAaKBXw|$5FrTIDc+>=d48uosRT-?Yi5zev3PG z+4;0`%vX!nj*8gcss7!m@%_DKULE!O5vDu-QR*xYWqzlTLtI)s+Ba-b_=+39=43c1 z6d4Y~t&(i+o#D_6yt+vWwE5q$?C|hKXxIj_o^zM>jo>K#c+&P%AZ2UuXW?5#y z-szKej(n~4+Q&0qFdI6C_9Wz=gGM*2i!BPX!y7^sQ&TkufKHq ztF61H58r%j%ewocnt5G%@7DQSYrnm8VfO#m-nWO<)V2MN&LIgQ2_Z_Tc|NUq2qC0G zNJ5cgp;RiO5QQQnQ4&HAp>oQ(2t_4?5aJP05kd(2UU}a4efImk``Xv{-@d+Wt*d5- zwZ@$D9{2eDe)qk`xX1HPt=3x`f3M$ZR<}jtq{iW)B@4H=bH0@~t?p<@*n}Rvr}{Fn zGrA7(>znL9NN02=mlc}2;B#7W&&p4J#<<_y{P`QQSMS^0`pMOe`}=%(yW>H@)UH2o z9Q7+WdTZ+ORo2l-iJzQ%eSJA>{qwzl^qluh*f8+8oA->S;_4TM*E*+FIHz@Yn&qC+ zxILS<(|_0_{C)owM}H)?y?R!=@S<7CxlO10y)4D2FMa&6+vC$=#}vc&cgGhDT6{NQ zj5_P}|5NXsG8K+aXej)wew@>O`$IkV#IX6de;u2Dg>TyM zRL{Bp`y&_rpaJcvcd_|JV?*KS3)C?j=;YU4UCGU_!C?*ut9o%Yi~TXw-8$VS34NI0*5PC ze^);ksBWs!e2;2@un<~T?_2b*s}KI$d2Qgzec`EoBq9Oc=Lqka)%*_i-K%$0Q(yTk zpyEJy&42xyIjn0kM+8_6M;xq8RR2W4&*shd_n&QK1OFY^d|tKC_($t%DKn_ErD(t5^O{_w}nUUEuu-pv55Qi@?8GV)Y~b!@ zz^iQG_4e?k0i#0g3IDj;e|avfCsa@7gUNAfUs21#U;8Qj%P(4JXKMdmPxAlzdJ^cu z|49!cSU;`=a0&Ooz3@5s0z3o{$JgRp z@D%(AegeOV-^DBN8oVC=giFLQVj{7dxJg)$)}#aJM2;mr$(f`txsY5!E+&kkw^Vk@63yZj}9L>qxWNs$6jEmxSa_QVD?kQKzz2hwSiF_=d#OLr=cuk>| zzzJ?boKP&337>^ws)?%UDqmHgYKbaam8<%sYAN;?M~Y*_0CBsxS3D@56R(Ix;uEn> z93y#1^QA;-pHwKlklsoIWJ(T@m&++~mRu)yP_um>8y%>BI``9JZ1D$?9@OoH;jwa{&uZ;{v%D zZWFhS+s~cgE^t@560V%9<34deI33=I_u`lGF?<$(f^Xov2o{2^Fi}Vo4hR>7N5XsI zo6t$sTScktn=QLgbw>3_)n4SqA)<#kO$-s2iW|fVZl=e$UrSsBt z>5YWsHgbPCR1TM;>08PNMNRsV zU|$_96g!PQ1yAjPPsNwuk)zv@a4)zITr1v;@5%S$NnYR`_zC=E-k;yjXY*(HTwX_LFSrX4LX5CiI3nZ< zg~CmtT+mSItA?qZRkKyGsy(UZz&@{P>4TM|2fuim~D*u=h2wR(vnEle$Tk zlC8u@j?!dlmy|12fz3Zj-=!8ZA=}IDvZw4XFOm<*=j5C6Q~9O*MmAHd6iykdOji~v zAxgNiPT8#N1TW82PAY#YwZA+Z3Pix#Ug(34SQl&nHW%B7C1UrmCs+;iMoYX6J`taV z2jgq-UHEbQJbnc)246QMIOq=_@bjxg5m80FA+$+jvMVW)L&yOXOO2uCQD3N^lp)=T z?n>LyZge=EO4ra0bWgwop2=qJF+4jAP+&G2#I9ktvisPBY$lrrw!O|)vQ_MB_AOX> zJYYf)7su`34se&i+I5^BZwNN_=R^3D{006x|CE2tzvX#hm@q+@BSZ;XgcM<)a7efg z9{pUX5n8GSf`ubhYgOA-<>1e6RG(F?#V(?SI8YoRo)-(mr(oX?;#bi{nkWTJ;ZnSm zB4tXKq|cJ3+*Rh}WpX6=^FF|dwaQT?M=4Y)lov{)0`deUHv4WHOn^_ICj#ys!WuDC zyf@wt$8k5@2M+{{jcfMhQ}_kE0Dp?V1e|z}f5tTkBcdZ=M)V;15iB7SqlmGD8!?5L zK`bGb69Hf3i26gLs66?X_=3>bmNt>VZmIYzJt~ ziT0wmI8O`}_lU>E3*uX`QEVj{OI@XY5-thSAZfVdA}x@XL7${cS<-3fm)p{P>6!Fe zdMBC7OQCNL$|Z7lMNz_(l}Z+%H}d@D&$ck(cp%mTw}if6@dP{>--l=5!QitRq#oIp z>_jGz$>csV13Z?bBx*2KK$TGCV7c~mXTXIG^fq9z2lNyA9sP~gW(=72OdwOtd}1cD z$!s=zgB`$4!9Ndi6<{C4>+zPniXY9pfq&lREBIIZ8y*R|LPx+_PhpX;UN{fz z^+@<2v{0#3LsTBBnJNp>23X5YoFOg{SHhT&7c<2?u~@t-z7#)+`cfOIqhu{nk}Nq$ zPSONvsx(ttEUgDi6iFYYp0Xm31uhGfSIBGSjdHeJB;SY8|4#lU>nLrM&Pp!@Q)I_tNVFwQ$zH&d6e*B{$#C)zd5(Na zej^R2&J<0F)FANnIn-wAA*Dkb(S2x+9z)Lp6c44>(?{uI`Zirn)676-7~{b#2j9+Q zt}yqRXG|T_!053Z*e+}j){-5-lE9qsbZ*$%db#+6m`Jsjwyp&b=AC6PvOb`lvIO;}ie~VhR#JPZvt%L5a1ZZfBd|aGG51${i!fcHC1FUk0~|Lc zx`9eLM^>@5YzwY6*Ou$Rb>{rJTwt6nGQ6i0mT$x13g$$#XLc}9vZJ{k!4smI7(g=Q zelnBHq4KCkN&~!l7URP#U;;peEMdYyi9|t<>|xfi8`#ajlu2-}d)WPOzeia0+94}w z*MOc#PobyN%jlJKG~Fm%QI&v~8HoL4Ql1OCKpoMV!|S%<<@96FD_waDeyI3U(gW?l ztFI5%4ypft9CufeKqKr?_A3XKBfxhjl{3l((80fIE~IlufD1~wGT?&>^~yI`E93!( z4ctktrO*rX`6|HvUBU_Bl5ih9rdH5awNQ0XnW}75LBPVD#BL%6igGt#T%mYVd?5ZQ zHi$Y>Z)voYBpm=PnJ4v-`^fXWx6#-q;Q-4ZDvw;JSn{AO``s;X&Mi=){(s zPi`cuNFBO1&!f6#j)=tXoC9YY_cuhRlMoSnwH zaPHhJ7`v_cK0L;Y{7ilgAIEP4U2&1`Ay|PnW`w~)ys!-v#U3F;n5SB#x~qBu-rh?b z4kH$qR!G?}X3bO*7n zaC#cxo*#V@#>!6^EzZn1MoS!^i~{cab?#@xDKeZ|1s?T~dJSHs4S3m&wxI`u>Rk#- z>=1nhVu~_Qy+3GOrY&R1Ol3AQ8O#f&D{IS6WmmE<+0QJ?Wpg=P6~qqxc!D3okLBHY zPksr%f|r5Yan*h)R$dNzGFPqSfj^)g6&zfNEyOpXBWX@8|2!NQ=l}KeH zcyy+suG^+wbDR$c*d7eC0G_kKMgqUB#G--U(y?spB6bxk0%rU4SKQnO@IsBMBOu-j z##i7`cnq-GIf#@^LAQAkbBS1DC-mk;V6bNpmoVU!F%XShBp;F0q#0!a%;iRTQX$YE zZRvi{e`f$)E4g8yVt4U+f*mN=U{FdHfUFKGcZg`0sp3?-K`UJW#rj&+r0Oc#f?6F5 z3L#2zk<-=VH4bXyz=43P><=@r1bi|c#4Y8Hac8(W;sVgIhru@Rf>c;80*4X!8ukF2 z#TKxS*&4vdAM8eMFXzcGhUnx5{|H!0Q|KoUf|D>#@D)}F=Y)L0SY@S}ql%O*l_EvG zw%QHaxsMs(r*Ll=eJpu`tR)5T%64=zt1B1?K7v1tm5V|h@WcSsAeAGq!);YJaez1- zdh4EOD$N1Sa#!xAOi|WC1en*1rLZ?Iv^y9J!gfN&;tXUge!_=do8T-D@NuApcN2NU zQcwa{$?v2ojBbL?rCH$k1m+n#3dS!?q~!{@+hEW4oB$qrkFVuF@;`V-VS%tz&=%t% zRw|MjB^?Eo!ahE5E*=jgLm}?YBMTvaai9DQ5u+FA=jBu^wT(Ijn%Np+L}z*`Xy(Oq zENEi{Y@?1;0zelGhxqzD@ccfW5ylHMg)_ogRS}?kmFlBP7nE#>xJ--!{Z#T0xYx2K;I;l|_{TTa0C5*>v_iTgFzfC8{z}Te_lLS01Tz7xlk< zqz%>q{J9NaQBN3!cd3p{GII)I+7FBo=pZl%Wbln3jwyl+P$m8x&e?!B;yKh=Kn;Ds z4HJ4c9RV4gXgUsJ4{t_L^PRzYPsMwrNCm9kY}pr8bP~TDg8kygE@>eo5zeDW{){w zo|reRI&*{QdbaE<`^kZFh#V$IK->`{$I0<>qMQu4lLn*m7-)oixd8Ncsa&QWqvo1O zAME<89Y@RsbH&^+kH1>p;I5zwJ#bIl8&J#__k+kZ1P{X_fSsZN&*JcSJP{(KR9s&&kc=P#Gm*?BbIC%o zlKM*+L?4_aN_LXH6bJEfqLeI^N@Y^HR3TML&!uXqR;rg8V3ajM7D!vxmknei*;qCK zZUj4En(Cp4TuBee)%XI-g+ab1hKz^kD-EKrW8_&fA0pZ^h-jY!rZkXEq&8(h8B=QP z><^qSQud(yT_Mi(rhFmJ4TJ1X3>8l$Q)v+G9s^I!r;4aDs)Bk>)lv;q6L_uxZA_cd z7Qp=!EzF$AT#Mp2hw5S%`tR5olK|E8T2vwES*mm0UK7(&*@soQ8YD2d&ZEx zv|#!(6eBYB5G%VvRN>9|LJSzjL_!WO9>!4`aON>ksQFA0Q^r&<&zV|?HJTW0)_^qz zrE0Uh z;zZ7#bL3osC%rjeK)f(65>P0fO9t%A0L(uNkw+0|vI_1wSIaeUO`JAwz#BtG%7X6? zvAW3H^NzeL@4-5G~^`L@NR`tB{||wQ_^pBx@@Mim_s*SO8j5im2Et zj*6?|p?E_E9mcvRa0wi&u>P2x`eV@usM!GrH6EDD7LXsdmtAGIzpyj}5LAtw$K)J2 zS1xbXHJa*B5m`Z|%N4M~4{#z5Fd~yICCdRH8c9TH12S;Uh!6-K82?Z2Q{#Zz_i8B< z827H=WxxC@9!7XBFqAQ{69q zP^P9j%?DaO4sbaWuvi^K{=!~0-df;|FxnBT4eVtA{ACObW(Ew_A2^JH)PG*qgILg0WfZCE5@01DI?Gr`O-b4+b945MI?wTRgP?>PRq*&?TzDSq*UnkA~S zTDa0*9HBnt@5ldK3%~~Bf7QpKwEsDOMqM9={D`d}-@#m<|4<*Nef;01V?lkK_L10% z_7U(k{BM7U`Z(=*Ep>gI_Qb9g?QQ;3eVq0t_=@(%@WQ`M4pZ01X%BO>+BK=Ie(!&K;tv%8K>a)YCxyk$QyBhVjMsT=qq_>wIJNL%%Un z*ShC9IUPQ*A-f(8KOgf}T_0y)`s|G2fw$g9CTDjUaK%*)UbV~TXKm2=$DM8HOvgGO zb$uLe`^!ElZAXo3z30t@s@xAZ>SD&Wbv_(3s$j>MJUy&3Mx6}$c6E35Jm0d5i{1n| z9q`k0R+SjPWFOyuKlG~6_$cPfJoO|1Q}JoejH%a*aVFzo=nebl*ESuYQZPA`v)Z~1EKFs0|^ zxr;~MS$J=$Gb+@t*{`mTWAmz?+YZGq*=XMJIhK`CEtj6E{N&vxTgNF~x@@rH^i6ee zGxYLnP4>&YISp>t!(SHo<*d^VJ~4jexQVT|GafhdhR;*i$9Ze{t;cR(s8dvJ+;iv+YVbBG2GdC%cY%P@822jtgerfHtf`g!@KS{ z6#Hj8xy&AUx^=&Mvy87UHkz_CsppqZt}E5`aR&Cd?wozXJ4a zsEa5K)cdQ+Oy}`GHu`>;z37tJXWfaTuJv!LKVv}m`CSrJ7hhgN_7K-z+;Z8`w#&B< ztI)bJcLuEM5~x4^!cp?x1|6kkYQZE0xoV(Wfqgul>1Th}raY#d+qJl0$cqzkxA>q{ z`nANe-oEj<$Qx<{zlyB+Vd8XUdYgLU)QQfAAJB z`maBh`?>XN-_V)k&rk1nIP_9+JF|*6>x8^R?^~%qWbKuA@M_C@C#{I9C+FYocHZk) zi*YLqXH-{vZESU;{zuaS6t982zNFmq4W0P7n{w}h)pz52d)%UKuD9I0{DtY%G3SQg z%0OuvNGIUfokzy^^Ee-QQo@7}d0k)ZFB#ST!b`2Tc2=`DIj=&;G|={awfxqHli&7q zOX;<0SyCs9%e&?UrNlYxZWuakveuJqbt&Vi>l_OFIsfntWiPvqxH)~qhBluI(xfhf zJrV*oKa9H20hPhImhQ17-JGv8f9UO6_wn$hq*pJ76h|$Y>ryu{7EfDe4JGK&8QYQR z!eD%Lj#V$i#Q9#aB^qC}Zp_av>5w*LkgLn!FQ^vkA>S}6soDB=TC1(GyO=WDVVO|E zXiCqcn=4YotUBc{nv9w>P?KR$xb^&mBR(Cs741KL{8@19!F4`Sx6CHp?VdF{e3Bh9 z&_q8YxA?tR?&)+ideqg-Aok|rSESASJhx@xo6S?~e{3I)%rsHmtA440ACu}9U23Dh zL!K~srHy?;fqQ?$(S^s%U43uuL;W?8;oGkaSMJkg`T?h$_0og+x2L?>mXPhy*v~K2 z@E$!Q0EwVGth8YXmggK(?!eK zM=h2ax&_*P9M@&Rgu=bIXAOLK(?2xtgH|Z=&_tV7I-q3h+Vig_tT)!Mu3D#x^6J>S zn7MZAz`VV)#udLuz7X%uo49CI=$X>z87sn~cRw2W`QrAcp%I6+6U75ZI(_IDgTgdX z(Yi0Df2XqS^OS8UuBG-gNvL+BkFJ%u|nbY_#6iOtPPNP`7AR zi|@H;pC&4=J3^)Q4PQMq)bN0j%bS5K-1mnVOj_!fqCKH=Keu(WP`W0vmh7)B7;M}H zKQMd#s8;XpczhoFz_T^OeLNnUeeM?PhjKuBel7laY{c3=sn^PjJET6WH>>L>44kIZ zo4UT+C22=-Br4EEJ;upH<`njpmv}Yay>_#Qd;A4?QW)b<>DKCHi!mv$cc5}jG~Oz1 zaO|?q=fumiygDtdzcFRzg(S??eD9?C-mWEO?FYklx~OFC?cwRswhIchl`SQ6b`J^a z@j3QR`J`bnQFlCGqL&+fQqY2--~G8OGklNP&Y?kf zES@zDiu8YQ194hN>F89seqdB+%d_{lY`fT!Pdwtdc@FPTfMPv`3r2@pA_pz>)oip) zc^pwRqI@R3_UyVxHmk1JEQ=g7?evfVCwEDAwUL_^a+s)yT`P& z9}f@gc-Xb;owe_J_uDJNW}ztGEJKyKH~!#;T^dhbeh%}B)DIT-pZ7cd;OywA{sZO1 zC`1cQIeT(^X^rzmc1fQJ!`5^h>+cfy=2Y%8%lCF6VuM9m5{lMBRi?%%4KK5|cvzRj zuYMkPbFb!~B{Sj+A9Z|WoZjKN>IzEKLippWExMbq0|S#ecYFI=J!22_NpyG)E=+h%++DC_#-({4s6Y!H9Cl~lwv&Y~K5abRvwT8;ZqUwm zEl%$LmQuMP+oAKwqam>QDjN2smvc~7w{h2Q?3Z>W9Pfy2Gag)*mH&A{t0*_`q|9Pe zt%YLV)H$@XDO}T#wwXOSv%OiL=QqElP7U4k{`^=veoD$d)To7WA55~^wXxyzGWTnl z#+C*#rm^lDmX4V;C??%Jdi~H7wn$$aMW8yP(~dJ|JQN+L-PoJL<%G_q2OjPjyfmV} zhRGIYGBVLdo%iacZ8~f?;mr5bEnIhWccvD-eL-_?=jeo`#v563@yJRW#oqK4Z=GF# z`@RD<;>o+EpN}}6NeBvizq+RDXT!h%02WRg<)I%VJ@Qt!yq)Bt+iS(DupiNxQ49SO zI*5(-{!`}Uw?+=y$T{a&PZVt5YGDc%f68-Ej^o_>X%E~xmlaG(JKptxQw?&{M&H?u zgBo4d-Mx{Wd_7xf+SYgOnUQzihnk;j6+fx>{f{rvY;AP=)6&dD?^*J?p=&4obXvWv z*77haLCd&nbNY_*pIerLLbTB)+hLKLg1TpHesJv5uAc`p>Yr=)ahuz5WXIG(gZDf;J(h#j$Jn_e$8dG!!WS5*wHl~ED_cly7 zzpS;#B*_KkXrq*nsw-ZpVis{Q&dck;xcbBM9KL#zuYHCN{nGttuiHE-&_=tjgg-L% zN(>B5*dAoG0E@EjaKb)1Q82nnRgZY#4I4(3YokU7hna)pf&*Vqmpb^pH7xNN5qZ-! z%PD)u4AWy>3QoJAYHc)Su#bF4EF3sWd$r+u*=?JKq_Kd@@asBtW}9o&oq$)UQ5yv- zR&BzEAC|5(C23q6;gy%2KJmql;f!a64kcke$h)>BFR?1D!bk>0KQA zA}bvNTfFH<|n8FItHlbWo&K_gS6Wcl(%S=3K<|ZvQx7y1eF*{r21d zlj?19>D->kO$S+wtbG|j+39t7Mf{BTYu$91wntX98NL$Z|Kv~E4s5p%&DKGRe^S!o zcRD87&pO-OelucVm(xnShC@AaHfZ`BF5Psh28HOLhA+n-S55m;#9uraziVRr$R3$e z%bYiPC99_HJ~eCpjv^GTgD#buk#?2BwN-znynfe_<6Lvc-CYqd>qve`{>@HrwU?np z9n`-rZ7aXJ;9U)n=$s2f0rP>AF(gU6(Wps(hb6 zG)@gPS@_5R&DKRvPW#l_nK;jOyr*?eH}*l9RlQMghbLX{+v{BbEH4lIp+JP z1J}Yo9U7i8?M1g2F-}G(Q5Si3xbB#^X#AIs9rk|@o|@EQ&XD(ST7G)7sL+DFxp{ul zG?cE3+i>E!Juq`=MR&P{Uz^r5A9}cR)@Kj&#;^mT%Mt^ z^OdjUz1P_~Ci!+hyS0b<@92RzJyaG==!nUa(W9!*9bSbehUxAv7Ejm2Se0XayI(H! zhe;ZGNXH|^!24$A=&{RB8cq-XaBKJ1Se++TzWMm*jymH!*n!AR57o#mJ9X~;+SI4E zTd&WhyI&aVZHOIJbTFa!C(}-q?hl@z*?P!qV)>zrYO_A4Lt78@h@McDkxJ;lS=7zC zugTYsIo;nAQQ zIgh41%C54>`RSF#SDT`AJw$A*a(XbSf{K`)v9f;j?$BP_F7|3?Aia%Sl$2QK;Bgw| z=%EkUK?!5C9(6eR^m6+B12c1S7J6L@O>^In(sgQ{{_7iws6Y?h-_UQt!L-h{OMJ_H zOcUp5n%T#BIApjxO>f(_!@!c8k5IWD8oJbN(Yv&=W06tUKigMdYI=8a;|i_voffw` zFQ~7-K1Byr>mkv@Dl57A)AJ!?-!R|qA2MsPb=Z|H4@{5dKhnSGe7#H$HR_=f%^k@4 z*rGQr^PN(2!+R0l?7^|?f_1fh#l*AaQH}mcUmwLR?|N#?{oO-x=!(r>S(Uwg&NMxmt z?$+hl@it)(mu&P3N!b0PL}%yxQQqC-DtaxT{ciSg9)T$R|7$`Krj5fI*S{@&(>5Qh zzvRX91zXpQ@D+`HuV4Eke`v1>PWyd|`ZuP935CSe^$Z#UE}q4jaOTB)vkZ5 z5%aso>+c$`|NNArxytqL8n6GT`Tbu?VE?Z1`l|%??;5YaYrO8i^t;CE?;5WQf7f{Z zUE}p{rH6jkc>P`D^>>Ze-!)!;*LeM1|Gmm2{@>a#;CGGJ-!)!;*Ld}X zs;j?iyt+UcynoeQRact*UE>utF#BEO_5a%%uSMVSyeVF<7iZ_Otv}XG+u}WPZ}_ZK zWBW-9`bjUo)mr_BdY#rm5y>wO3m@YA``!5HdhgJsu5TypI92&9>MnNW%+SdhCll(N z%dETa{4k{Od7e%l;n&#Z{mVp`;63df>>FmH>8V3cWScrC#w^<^pgZLAzU zap<$dY|%bb{LJcZ4-REErOv;1Q6q0rkCzLdRn;ZlIP&rC#Q8w8obeoxNxN6I+CbLk_E*1lh873^-d1LL@u@;83DOO!P zHh4!m{@Hfjs@nefda1MCI)}EA&pDbrdlEN8^4RM0F)p|@sbhNmR8z!?il7GTTGJ2x zmgQ7!n}6n^bMm!e4h@5y6^poDqsI@}lz9J0;*@twT90+C+I?-#*QASm((muoziu|* i=%I<9vntmY?&|UL_4vh>^)=m24~w%oXliS6?SBEW>RxC7 literal 0 HcmV?d00001 diff --git a/documents/img/ssh.jpg b/documents/img/ssh.jpg deleted file mode 100644 index ea3dde100b2f7bf6aacfb0069e7ecec626495b2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 63721 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

m&807mJO zf%{^D-J^0+q}Hl?D3FqOEFQ|n{#nVW{V0xgY;$I+WtuHS>zq_En{XYfDzKjLsycMj z;tRKD8|>gIPA5EKBMwB+aoOvu(|Lql74|QkdD70Nn;w9s7J0;Z6qHLZZ#&oej8iUs zQA(CI57HaEWG!18&$4M5&E(|Aa9E$SoUj(Xw7QXXEcFLEmN{;TyAEFQG_pUsnE5d1 z{}j4A!uxUL)lah^D(AXrG54|`?P4^8-b`Z{PZ76XRA8$sQy`!vB<>Ru;HbT&MiVC( ziZ3sJW-50^g@X{V6C_eSU)Z0Snea(^I^fVS_6Fa+M~Owu73PSpjRaeNnzy-Yfnih05((B`I=^TuC9roJ zejDC7B9LfkP?Y@jX2NGlJTPKNl!F8O$2iHk#v9ta@PgJ=#kdq2_{+9Pv*j?xPDn-x zS*7Bp9r3KfJ47~#1wp;81ZGyTm0o`E{U=4gn;Jy>;$?{rSz@g)Qj=o*b3K{ugMjer zwNA6EU7z=^DxkZCRPJ2J*Xi3mB4O8(Wied;{T#VOfre2Q-dHY8@bxxz_k|N*ct6Z{ zpUE+i`nRt`t?<@$(I5Vriv-2<>fn>y9iQHf^f_M=L1=)roo=wcLO8|pI5;4%=W$^z z$GW_i8`xo!68&?guCmmt;%5xZa2XW}rAX#sbZ*UfdyESmG1NehcE7{oe+vy2i z;G;0~WC%C-Z8;S5sj0tH?dv9bbb$i)9Etn9)JJ3tJ2UwzE}ufIn%QokZI1`X@4&Rw z3)qH)epZgVKi8XL@6C3Le$pdq;vlcU_yl?aV{8eMB$6&* zTD_R|$R;E*Zu43RwyDv}NRA_@jnW^G?O|GG39cJ!SMiA}`u=%>(cBX0hIZCV_iK*+d=4FiT#^3f)hvWTj{;&^lMz7m!Q zGEg>~VEq5`5M>mE9w}cL8M31$fCl+%JTIR-e!2+TVh~Wi81Y1Cln=Z3N@yTWMpG%x zbw^&fWkF__P2z#h=?O5JbN7&=D7`U59bLga+D1g*#tDc;)3wrTMo7#@tevV>Z5eAP;DQlZwB)~*&qp-vJXDlhgh)<=HQb$x&v zC@e}$Yv_zlg-gi!p%G$ZVH6d`67XB?Z5uQ~xAmw`dHD7omoMgygw*)f?UQC#KRQ8M1n$f<#iQ$(U ziw*IVWUtp#hBJY;ynKyjL`-85A(lOWHX(lsIG2ve| zp(O5IvRLFAl>#$oRDA6KauCo}Lm}sgijhCiH^DYb;r6Y%TBEz)hh~8O3aB~~DtE7o z{EPc@C-u&fzb27CS>bGb9kfB@`XAMPR4wbsa6%W7L|&m zJe-BU{_4`c3vWo(c?3bBy}MnO4Yb#lo0(HeC-hF=vO1M0F@%%n8U1yuE?$PD@3$#o z^nb>TQR%SU%_h8v@E*8-+AgkS=Zc9LwEvB)hxKd)A)!XJF2ck&DnvrEzMobUk13VD z@3dFH7d5S>0j8>$KgkSQn7Yid*m}n=f_DECN$~8XJAlhY1u@3&jru6fT^U{bll(@% z9+0iBtx->#%u3E40u=3yqihoCY-~(65@S86?z1)ZIrp!sF_wLEP6nQ3lNX09fgtCG z*Twrvd}d@}n}kQ=*JTg!(cOt?2Vw<{afD!KfRC!3^q63HV^c$Uo%`a|)b&Az*ilp^05Q@Yxm>Mn0)-@H^SUPl>6m%+F4BISUL@fijiN-3ugJF_l-Ln|3Q#1@5!0 zM>~}$?c76cxH_!Xxy=DiC8f5SVhp zvI)dpJE~JNN`8=?eQW0zSBTs;`5qAC3}Xs3R^7`lB-g2$=G90Ge32Zi$aERdT?J|H zn!g>_TMe(ANEphLP%`b?H)C0pi2+FX49VAq(6*U-@%q-L+7+lgWK;)k4<+;m3Mp|W z{kep{j2%(=D&W4VH*p9;Oy?-WsB7l9e~UlY{ny3QrynCOF56iS#?)~?od&zKYz{E# z`qx}{RfjKSa232SU_2Py&RP}3==PY^sT${f1JdY!p$J}v&m7(8Wyfo0VDhU3dE>ZB zb1J%mL*$(dm#CQ_JN|@LqVF7g-E%4soRRO6dVYwk_x2szl+LJfNvcNzwZ1P~033+U zvibuBtGHezu3Y78wV20tVY@n(q{}D7Jai-3R?MAQwGL zl+65dLw|G67OM;RxmrTTg~#D4&E)w7^2RzdP~ZnE;~v+ftX|bzQ4bDYHJ%?t+F$Yk zhv)e5`mF43rGr5~IincOTc~rop+2%`rqnyEJ_>))q<>V$c>3Fa2<|!s(qr_voHfF>w9d> z2_+DFva?i8o0QUA;(}>mFjpupwkgn7vKI)}FyjeMq$RmHq;mwsM$c9lGax_S84M* zoBmb45u3KlS6OZ%bEXpBhX7|EdMC5wwws(4Q{$iW_f4)4w?wZ)VY4$LU9x2LY5mmn zH{cR(o9O5d+1R@|G|j9YDGAH>7=IhMb@T&Bh1S#6^^-@QJ&*YZe@lti3fzO^tC@!w zIrF)d3G-hf{y-FWuz!mv=WQLtFj%_pE;7mhB(m{;v!C*MAZ-*LBhwTw6poF5O)$wj zFk|kRHuFu)0HFIyK_^v__@lFn5$AD-xdj%ls=(pK7K{-d8S;@mXg+1j54q-YNW5py z>h^P8!3$c5Bc@8d~&#}O3Db(xd% zOu4&H_xQ#hCCLQ^mM>W#D4wV4$GlCo9boNQ)VxpoD<0>!=nCTQX~b2wP!yM*=d1-) z`Bg&iposH{<7`1Q*mo|UgJ>%}?9j9maH&$xbe7>xDzs3D>HmGS8@{N*ucUK7<*C{E zx5U3`&T1V=-*i2aHER+T&Fh>5rpOg#!dU()=y0y5c2`Q~-NoJ)puX&Ti7&GFdH_?j zNGBGWi3E9wk(l&Yqz2!ErP?j%EDjub$_uwZU0EUMX?{I>L5Jdqclnm7neIL$QSYCQ z#N}PE^LsE$YE7L)l^o*DxnN+_g_V%_rbt3f?79-8`X5dNM<*+HwS{ElxSIIf)Q4w5 z>FN93XTYkvw{^+;SuRJH`A~}ewmpKWtZVUTjs)0Ht=$DCeOSjb@r>BT!!&yw9JC}O zkvD9dl^u5zR6KJ%8El@&Nsz-PIY2nw(0_Gjx!Fup6sq4imPG(Jx7cCL49b^@1A4p2 zY~r%6`izSRBEHR1!AVi>1oJNlPSc@*fQdcy0Oa%@_=5e$c^BdMRtMd6WnA`Dee=}>}%)H?a3{^ek~ zC3mF?TkNb^*CJLdz0_4NNTJ9AT*g1ZL;qd|{a0!#tQzCJ6Ok(y_$na8Q}b40d}^x6 z!|X{(%gXg>X(UM%ol!Eh214N}()KAPB1q@w z;K6-t`GQ?`|C35Z^G%|K+2=JIJ z^Oi9q9ezC6JH+CI6;MRn;AZYf?FwY0GvE0Bjp5stmZq=6_edmmc%$V4eFW`h{#dFa zkHAW?{M%};gUFcgNE}8;+0I!gV@z9k=bM#*xG)Cl`x5X#`Kg5&JxJn@D*lRl@wKbq zfFKp>F8>KX1mdL{l%dkCK_!06yA=~+8(Ch^IFrntVx0&x(D&V%{GkyS^Qz_sg5Gkm zjQp2=gtwh?f0)2S<2wTVe5OBrm9hz&p1*7whwGiFrwwKxu#Z3QiaP+wg!9uqqlQni zOV;O+v7PB*eZ_^+tpQ-l-4ZecD64+sg;9=7mNc>f{Cg<`W)lF6iJCCwDt^Z6!iD-( z_+Fdv@tsXh5rU!Fn|OC;e5vYdMQ!}Tc&~$HoLkB)4R<8T@)Qz>Dxq{em9mT{#REVS zgbBWP1sX))dwTtkU<@b661B(`G!h^ za>KnDyv2*sP%L$vq@qP4Z{2UgUcqOnH)QWE-`N*;f&`&dT=jCxFDB>wy@yw51&KOhzkr+4hZHqQbHCM*}&SVYbTo8;t;q;(mLMvWR zttdI<1?T#!m=(>3vJ%cPT9j(TXDiLddh^A}ipYl$>8!de4uVME5CBe^hN^o2aH1rI zjI$vO7-7rLog-1ZSB+T%(Sx@sF<(c8K{RvDNY`x4=ioOfIce%ygYlei72WR7W3UlJ zM}ipM&-P-%1idwEO^YNQ%BJO?IJBq$!6R$co>7f?(UgtWNdwPz2zx@GT*EFaW`PX^ z8s*@qg`sl-|D{IbOBLvKewja040~+CLZ7bBM8xa;OXqaUuRAcz^^<+8Jv52*n@`CD ziq*U%Lt8+rBXHzK;k&XM%@^>i^-5aaI@m zvVX46P3Ad?xYKvnhi{5lb~XB;C>loo2eP*@o&?fpP=2b-wha7ga*SN0L2to`tM+h; z%nPS&s7~~G;F#e{`(rzVPE%w7@e=A-T69gkGQ~lwVU4l<$j!lW6b;w1oi*2H=N_(QeqfujUO?( zE!(?3h>?C_7*KtG-Z|YT+bb2S%lGK+ppz=BDj&-f)!xpzQ)`?*=LjQ1otEi;8`~Co z;60t}E=QrXe0%zoEC9Ilto+-$_AIZkTlS53uh|iN&$U}(6cQAb_RP7OZ9B_7{lo?Z zez|939C@D!dK`vk32P{?#9^i!d5# z_VL*c@vW{;eHsi~;Uv4+b0iz!MPEJW^x6)`jVoB^m!ooGyC+HRV z83Tu-!F6CJv9h>WE_V<->-FO~C4&j;+A5B0R%A+$i7&*Q0cm98%4eFr3l7|7v9}MJ zLIP)oh@LURj@SP79+r5<&kk?XswyademJELoq#??;$|tc4m{{*rE-7$qq~R$h?Oqj zOdg*`rcENW5?)6V?d7EV>QKNa1wn+0O{!Lo@JDmkWrLwCel>2@&GgKhAnT^8@N~38 zucTPt$i_*M=V4D_A&nPfwguuLaQ6I%gl5kF!=G;0e$_g5WyMnb4zxS$ARuhZ0$oyh zDv3@cNSrC&k1mJ@QiTl2@TXwJ7TvO4)lJD1U9152VxNCQtN*f<|0=TR-29lv;FbC% zuWu%naMQnu)WQ6fPGX|4`mYG||6ZWc(MbmtZH)F3r`WCb(rv$L(#90MzQcKAC1TaW8B@V8HR3xCA@v&rgw^|%&pY-M z8~Mc(SpiqU=A_v{c8lUw?yPye*Rc!BN5r$ zOEEwD=~lP{#J4JBxvq~j2fOh<^D6-%P_g&Cio5Ele`LpM6$=vY=;oN?XyTP5_MdTgUFR zJ;eqi;rs2#=t`&$YRO$oO{$ZS%|}VKXZM>RD7_O9oY2ooDYZu#w*ePAoo?EC`}S?hQ3qgB?Xv}oPT9XOw7AK$D0qtm2Z{gecP_B|1i(WJch*NIrPxi zjZhhePrS5t_`JdVo*>@O_``KwH&9#y75Hjr)*;>>s5h+T^fT1eFkD5?{j##C#CYp7 z>|s8`5<*^hRgw%w=X6TYNjjMpI>Pn~M^RfIezC*=H265lb64Ja6&FTXuU6%1^LK=( z`zi{-4WdG{#WCAil^K6#^lb;OQ@oG$m;09u6zkLYd`T+W zmh3ih+bf1 zfOADKSPgDJ`drxi8l=f%ScK?%>fKxL$Tgy6d>RLmFaO(O#5oR~yHYc(jNhlOsjiZr zT%A@b$2(-8g9d(9PK*fAa+zh~%}81(GJIn|c!<(&z$n)+pI?(*fDACuWHk2p4Xk=f zh%jC>f9L}AZvX$ZU*c`rv5g54cyqq{lFa`_(O~XI2S%2?)eS@aXK4yQL4p^Za1sux zi~nh+Ko<<)wf}7kddh82_q#L+?-~B2o_eT>bP782FRl9jqw}N^f{F{LKKl*}1RR1u zg^2m8?zlx;p)LO2zXlt;YZGfOYc%R5_6qPvO1>*^pwj3=2p~XmKx;E|HMqCI)!J1s zHmron%*;gETOB|yG2wZZO6jP<;z&@|8Xife7de(W;{)u0m#WA?bU zk^UM41WTBHGWG>3C5`z0zPFY1xU=w{)fz^Xf2ouhYuevzPYAENA3HBH0`RWgv6#qc zxs0f1^hv{2&d~Job))4uU@R(miLJk_ecMPq+N`dAz?(qch7qs5?ilOeBmEvq-3hJi zsZ^{Qz&W-}V^8Zzle_HqxD{KxOy#4ON>~WdUM7$z$4zqcRKG9U)W2wxE_^hP z{y8_VU8954px=9pVDB6>ok%2)%Yf>_+Ao;n$|LF@V(JPS(O@6**WArdkPdhKY(-rI zKV~VBYFhF(xVKFVZThpq$gGU$hj%3J3DJ*N zvhYpzb_qlhk71X*7zCF}9y z(|YehN851QUR26o~T5ihL@w;Bf<)SGw&yCtd}GyPCT zL<2CV{e>WG$A2rsU#Fq6hWbSFN5ks?vYys8arcuA7^7MQ_wDiPyVmEg99H|^(I-70 zi>&aIP=RbWss%Raimf#~QYW59vo1|q50m3TCiblineC&L7%O^-!GkG92D*lkY6OWs z41vDRfSS3F#QnH7t_|N^85vc6FY8>q&d%|^&H$o0VhFfs&Rhj}wsu8>T4?gPc3CJ2x>y zX2k8X>SX;+#0;g*2|+7N-Rlob??df1t#)$1`#}G zB#R`3=!a`w-zI0Qs~5!Hq{`8{K}7A$s?AqVAV;s^@ujix2_4&T-a7>3x!ukfzeo(u zPqInm$>oJQca`FNVmAFNGCdE<8)T9NF_y{Y_Y}dEE{1qoxP>5Oo?cj_1@Y=J3L*kxiOuI@g^pWn^HDUkq<0{9%S@ zm$)17EHNFZjqFtQ>yr@mx^lVGKNs;JWaCmW5$ytm8`^)KbT{j`>=^fWVCT4OdiJZQ z#9q27?Uio@j6o~MR5+z@N@ohXTN9gO`jNF3MB~)SFK^Nje z(Go(M-OC2$+}|-{yr5N~*_&0N@3>ujrA@_W??{rcwBmQfq%nQq0P60R-H2038&f&) zY4ljcnY2liPnJmjl&O@*2Iw>y3xDqId11FhiCnaoXbWECgJ=JTG+HRRv)w591d*y(NAizMnK9BaOTY_8KP4O z**ambyzOtJg|r@@UdoGZC9WO_V#u}ec{pE_i#Cr+out+bUj_9OR9*Wg`ze$PM7o1x zo$kpz>ld3Cp{{%(wGVIAGqqD{mbcPffbp6LK)1gOf-j|V(QYD4cgNs&YMa&gMN3{V za}z(*X94pL1lbs>wMM*J;H*6OtS@KnOr3!}-TvlL^IP!Cux#RD?cFKz(+3Kp@-6xL zas(2dLl4OYl_ZOH#=!NK#;B6S!|`6nTYVkNkH1!HT(yE@=@4T|fil78HXfxNE2BcN zMgnle3NaJ5=6Xm^Xts!u*=ReH-D)6(BHbTEUbl2`Vm|dJ^w3uxJ!}}z*Jz0}Mk(+n z-_)|}xY516o5Kp~2aAnsa&dR?-Xib@+Kjp2_S+8~za*^i{`RVrTP=Nfi1XLXXLTvc&w9V<&nJtVnPI4{z;JUW?uF98d0n zcb+k~Mgmqiw`(y+fg}6=scBoacxHs_mvwiMd$w@i#ATF1v%Dy-6bbXgEg0odn9n^1 z^$ey(O;1TAyMAmVhZrm53}eiwG$Lmvu^*9$dWN<*q-A(}@c4sjx+$ZC7ZR7m(=^w} zw%Sb`>x7%ARk42tBQY+G5wS>fWrg{1>{4WI3uo!~dUAcgYPR`8_)D6_YV9nbcDUul zqF?l+0_??a!AtQd_22;7kX!$I9ru3}&fp}!-C_N@ro2fYU46v_n`BE&n`@e0LTBScb;Jr_+dqli;kK`gA%)?0=;XvW?cN|#*IHJ+3gA3vy(M)L5#ZY*( z6_pt>b8k3R|F_1ttV@Ho?Y-5kV^Be~5tz^afhcXN57w(S9}BKNuGM|7z{(hkue5YT zb1aCV)|#ADU9S*MzOX`LmDkKku5)lXJTu ze*#_x_2M}&H)fIPw!YeJZW|})*|{aeWD&8=H_7gH9xy6BhB4sbt~4s7R_$+XGmO4| zc6VVmW=5ny$}b2c;C1~yA(=zH(&-G&D@Uf=?Kpm=_43!3uHv!~9)Z72DDvp<5U(oM zn^|{~IF${gcf-lY@CI4+a;aiO`@U0|?+5R<_k zzz&UiV^&-HPV{wL`_t`P(i?~7-O>4CPimNYo~8$g_}Aj?m| zvQt$_Sx2Uirni0!XMb%>33^;oX||0q~x1aun!2hY~A@otUu^l`0p|8d5Q{|Cv&HyaJ$cAlwqQhkqW zLq*Y~8Sa+_)3iP2Zn#1=Xd6xgp$>0j0i zUus*sx-EzjFS*OK*D2y2bV4c%G9)@WcRP=2!rt(@_kWlRfep$^?Rs=Va1R!}(=@r5 z$IXe4>&;PyaS?WKPho_|`mHnlZf1Hc8p1v?Nj> zQ=kg6K2}Ccq?z=rg;9A`o5$xNtNZEB@0}DG-J-r*jw85ikL%SUW`=ZvjK7w5)^|H& zk8Z``uS#eayP`qb80`X_^tZmp=;NPX@|0`2zbwqKI&}C9C5nm?1&ts?wvUSYFM?ci zPcKxfE8lPH*yn^B-A4EHb~i!J(u_a1%*R|Wh%7ap*l7vJY}H~A1j6h^p;c*cb`H>u zs_bsZm9_G;6c`#9qW8bp!XG|pi^Wmp@0P;NgFKJ^Zh=_iu>{3E+$gI#U9%VbJq|?) zQ61;Rl^DQn$&^77RC*||kU`V$=v0A+J+cL1bOW5KbGPIwB%sWOPV1v9Wi@rVQ1Mio zJBJj#bb1^x;AMOmX{0qTG$!(!c+r&lYf?|ae)2eIYA&4^cNrY}Mse15zQy`_GlnI3 zjb-8N_F*zZv>-#fva0-Nsok5>@$DIr4(Boj=G91$686!`%Rc-Qn!quC<6QCQ_8r>V zhB~!FpXf)0QhqI0HqeTj!o2Y<>+|=iO{FC%KE0VJf*3WzQS)a$>ON0E zv-vWt(l)^5HSfjLs@pUG+u&^oF%pXPI9qA9rZE=Hautr=%v=bP0j3Xfk5Hvi!IuAr zy|<2wGT!%nXBbK;L8U_^4M4hK5GAAy5QdTt2SmC{Kw3aR8bnG7$)P(0B!^Dvj-hir zKli$4?X_NKuYK;_`>cKL>)!JxycnKmp7_Q0`}w>-#olJ?1M7?q%uC~u)2JK}<8IRjDyD{{+G;Cu{K3XZtGmb~j~+c)7u;$i~LTYrt7T z{;fO2el*|@jLhiY8Ori9tx7EtL5x7Ho#K7mSZ$fRZwXg$8G)Q+^2EGj$9*2*F!Z|2 zozsNdq?+9Ib&fIp4p+M>LH(bM4erd#Z$)~#?+X_6mb5lE0t`u^>u=RSe?{-l4JNS7 zZ1&LjMdKiDl%s(zlo}vLd^ZJ#6X_i$K<44~5GPKQ2L!e)k%{i^6m4w3L|vw&s$Cnz zfp#ZgQxN%fUp=xdk=4@fj-zI^u8%h;g3fJhiMP-$XJoDmtIt@XzP~rEsz7#jErMos zrT@-BI&M;vn}}I75A~YaY;;XB4e!`Ze4qPkP8DK(En^Ni%l^b<(PY_ph2Fy3%cn8Rws@LM)- zt_6|K#@;_5dIOE2GNgIO#DjAOGTWTi&+g;8w|{iT!8nRUfWSvfnqr0V79Y^uS(-uZ zBBg)W3U~MN*Es9d4?zRwJw^0fn$IL}5S=W0i%R|Qn62~k+IHgwQ(?J<^c>YLjs=R9 zMCoa=nt*tTkxZC5({|)fAUGihML(xhnk40+S}O0k$vRaAsd=RgJNkG6!j`MNX(wdB zxi?QJ!8Fe$v?r;Ge)e)ok-6+m6I_I41N7>6i+A^3P*{qXdFwwoBFl5B9e^XULy2x| zZNce<8uCbt_3MUiK4IO9dm;JofabXI1~diEAm4KNv_ok$8{7X$hGh8L?Gx<%<|x?KJk>Q= zvGx(d3MiHIhW&Vemo7o_b#fJZudKSpj-KGHc-q|57ue~%4?Bh_DE~gi{Mlry5LM06 z&o{ES%x|ThNEiVM2d<@#lmFMpU#Cvu6--?Sd5L0)vwTEEC39hMLlWZnN#3uB-LNTd zQG7_tf@NZ^MwVq8myNW>eggZfX%)luL{#`exV(x+kncE~Gc|B@ra;~>*`l{ROcKMB zO0?h_S|6KN% z1*EW271FIwrMlcl*6t=6ua~!LG3*#n3-P5R4wD$3ob~pp#PpP-3tEYIj(@DnqhN>W zS3DlOTT1&}NFr|z@u`7w2OsMPO0`_zn~nOjN~yco8f}qAg2}OZ(3I<0hTY-p`+g{{ z>H^X-9?!dPf5Ju~pA-h=og%epxvtH7IXV?dFhX#fM;tc(O45ZGG`y{dfz;bITap3pS3{ z5d+hGFZ7Y;Z2ffDf{p#?Nrvt9-T$=sWZ*o_EzsRL+dKYnCTRsr9%W1NqCwmj}@)irsG5JAGj(f z5mQkF%s`lg$OsC>oCKhm3!btnmAjW^{OgoDsby^4x)ZlxW~4GK{*$1KToh=A7UaR3 zJ#pH37Nk}gy{aqE*3|rbnJ;>txqXvXM?i?jl>v#l;n!Ars_{QeTdxfgs@dKRn)Fk1i(kJW3nw z=66MRfd#x&Nquy_IV#4VS4XNJ>XMXnl25bnWN(t~1NwK?zrV}+_YMVR0o zy!VhoyF6KFWwL}CR$X%y7nQivYmXfT%AuaJ^0>1-It40+(#l7SUy`pvww^U%>8Vt* zCAYtE4T=OJei-79+)>3Jv`fuluz;mi^cTFs;PoaISl!EMv#j|U{|Y(A62SOtQ5`Rh zYE{fEoH0o%*3M3Xs-H$yRZd}n8blca_5R!r$=LK7mDn=MnX51b_TDGJo;AX*@rP6< zsMm);46dEnG>^(xj0%WX6_%YQw>F!$Mm0#jA)t42a|D(Z0jRlJdBm~0-vr{*)>Eyk zXvO1ak^mG!nM%8>pXnu##9(u}lXf@yqXF9|7&>er(xBZ(G*l7?d#*3WRr{(iPBXmf zRLio7^^8~@zrEUjCL(4MD)x9*esUh{saAf1s;s_AnTviWMa-8XAZNK2n> z$Yw6}QsPHOIoyeZn(Kq7{OJv7)O;G5aFC#eO{weAz8o7WiFzt%6zGTZK~g#w#M;GN z#+!V@&N5H<1A}&=UQz{wAI&213tR)LvPjZ#gEcAU8uwtmvhrszIk+~TLG%OfYBA8@ z?-w{J*K_M?c5)1vR}WM7X=GCNhZDRPIB_U&J8GUz`HcGbIf3v#a5ny#tN;3*{12}# ze#g5_WV!V9v2lI*0C)qzxqG? zi+?Z{{NrQ(SAXDd^s^L}69Mh9xw4*CO=bZoW|mt_Sr9j#uv#_G&r_DpaUj7wJ`N}$ z1+3b97CHN5;$AOI4uW~x;d*i}=Bnc6>RH4qD^hf7q(1N*HJZm@GAP*T+ zdtxK&6OCIq>M|@|Z`~blKvyW^g=yt-ZvJHDYRk_EbfxpfK8GX*989EUNFKJ&3ORG1 z%i)7SETQmxxQFN~@>a{zS074VzRJ?ZDj-A~YOBre_q9?^4eY=|O z6J~59M(S026_WX@eDvUw8*#2DtA-XhU4&-Uvrs(fBifi9^x?o*r877NThkH!zD>a`vIRUW0mM3coI6^&pwN6 zi;F%fyn)P?#H!Poz7rPd(C6cXRV2L_w*B%O5+bcVkT{jQw^>2B1Y>yc#~s+?;%c)E zv|-fadUBfE3bh@f*$mpHpyayY8;PY*az7bDqF)!Lo56gSZZMtmrju&Pq+!WtAyY0P zBb-@7=GA*~+FH(W4GTk9Fh5_{`{WWocL4-|ejr+(9TS7)RpO<&k@d;oJ zMWECem$X14ciyp8`)~0!n?c9w}{p`+@ zrJH0Q;41{wtW01{D}Buctuk8n8^T;G5~V`_kdxzF!}uD(_X!XenaVi2T`T&PXv0*i z>tnR=c?t76*&-**Y^QyfJHgTP^Qz{VMlGFroTzXq&l?fyb9;Y&TAUNPWwm3CEy88e36ZMU*!S$TALqoq%~%e- z?a*B(#30u^*HXqO^p_8cJEh~MwK^#lj9KYuh9z2*zUiTTV$id~!RkB70-PCnMGMjQ z5(Ic?5HH8370`lNT~JnKRc$%<>#0CU+o5gUpJKJ}Y*gTIc*21>Dy@sIM9TJ~8?iA1 zuvtIF)Q9K2>_GAauw1sX-*XavcPdM7h};4Rf#7_&GIew-@>;`0r*!D^R7CL9)A-74d?DH=-Rr z)F34v?()Lvx1+og^$U=mY+v>6lU25G$bkY@gUU0_s``i#@fRt_gvP54D+LhSmnS25 zg9Z75DWtP6D20HS){3N z=4kC-1YrcvtmnPSWG!)@xk$~pehydfvQe0UpSL2{0UXjfSVG=zUg@AJk@{Bzb*r5$ z(<8uDJ%CPlaN9H1<%(S{p-ot@XHOu$5=cv9ARY83n_N4N*Vv3+GBa8E!9 zowNztkOJm@_m)AC;a5Y{wd@CZ!3nl}$3QUbmmerApA;Y04 zyWO26d}dB)Aa9?{lscX**-8HDkuWWCCXF&PXX=3yDz;*-a*>`%2HYCKKd+g7^Q>~| z_fQpd!KB+{`((+~o+`c@km3kQ&q)q=mr9pP&yh!NdYLCIUY0F57fq#;KIzs-QW-WH zdhHXUU@}Xa3O|y{q*o_+D&d;b@s+!PERbuyftyyltx3G0E};EJs+_AtB4o)?yl;y^ zbNjq|4~rJ>E)|`u3T_9qO(HDwXvU~i+mhvnmzQzm<_OtHGab3L|?Q^)w=k{n35W2u!!2R0%f!<`dEhi0cR{*sa zz7O+XWPH#K;)3f>imSpO&@?A=yO|sT1MWXq;$8>l%r7u2Mc5qIUvn z{%fGn^B-R?>y;0bE3JBLs0eM(^X#<2#Qx~_`2LFL_>ceN|FyH%-!(8%{@#mk1{*e( zEFhTpiwN4HOOJAh#-IRdL=d+$Vt!`&)AeQD`c`ThP@n{f4St7uv>jW$Bcqx=K4_ip zIxR{5Mgb^kz?_Ow>$M)qJimJ`s-vm%D(}g%T|wnbF!IWjMSsiv#JHrlrB7Ix%#y2~ zx~Kl}SqwM*5-`AEree@L=`hP1-`&W5#5h zxq1Q;s%iY_lER~v)_f!W1jA%}JFz)_JXCO>ht=1#I-K<>^%lG}^3#V0GN98t*z;v$ zhGADOhgj=OvNw$-FS&7x)u;GHqX-OLGzZwX0qOcKf$Q$p>|u(;B%%})!Vq|4qXcMu zJw0y}tBZ&SV@itYo45sjd0V05Fy|lbBA|-I7@OUfZ}rCVs0|lgG0goYvw_`*L`x-2 zerN@~9YT|i%vWDhqxppufIjsRz99MyVJS=i@JcnTp9O5dsntVS;`zf(aykiPEW#D5 zwUQNpQ_0dW2PYZ5V9Pymn;}XH=Gx|35r1lJDV*&o z3Y*DxpHdI6@o4Dv zLL~}i_92aL>F8Fo|0urC9cw9~VFzm_lsH092M4BN&3&kQ& z@1*-CfE;KPZ33DbckjaPA`gKf_UMs`Vdgr8g^+2XuSH$nQWmi2-J|agh@;9#Ra9L< zjLB_NuHwFj1VDdK&|aK7b0YiPmI@(D=z__~k0M;+vp?1-BNbJOmd3Fqe zm{_CrjQt2!-+q!a*3PWwSg!-f4pWn5_DR>3`LeJpw?pRLJzgo9EGZmA%t0oV@Y!bd zaf`}BOD<%=Q@WuDkpi^U*NdF}Wgm$nAHl;}5`z=x%?6}}+C=95^)An`wWft4mWe+x z#X;szBb%$I5})GN?z3FmZs(sZVjzK-oQ$lr^;qLF-VVlVNT|QV@?xULf7&znS3dz9 zRb7}iw?*$F)GYp<^&MS~f`3(fSkM6=L-cycQA$tlmX|zcXhGbmQLrmFU15nh=~#o) zNbiOl0MQD_tGYtE>eadI>h64T2~gVEp8IfG1JZ*@q4=Rz9nbmkm%*$(cr3F>1X8r^IuHE4T9j#LI031Kjj}1a&wd`??Hjv9%1GXYo$wQdyPjcUDd^qSA7|znkU3V z{yyc1KK%f4KIA6RJ$dNeO|f|6#=fdQ;>&s5GY2fE6S_8;@T2Oh^FV!P;{#BgPDm$V z;CMLX82=k0Tbk%HFl^#7D$WmQ^j)CDya-IMIKvgzJkv|54iH)k_+=99*^mXXt2Xy9 zI`pK~QypCHdG};<1p8|Ld&8#hEJ^?hQ1O0(_ei*raNV(Kwql$)uACX5ZVLpPb#bL# z#)7Yw7z5W4r#F4roUNxH`8*-`K6lBktoyeXuU=<;qxVC#Ju0I4fvOAy)3gPrHJN@Yox5_apDCa(4*oz-z+)UW`V1&@K}}KS1~h}LRs5XD+Fg%Dt)wIn z$WY~LdTbWcya9NF=Oq4NY3)f&&RECQ#`w+~@w`RUtSK!K?*hQ^Zq7V#H$_dAPZKSF>%hcxsN=G_d_;KZyN zdP@_t$X3MQEUMofxhGlEkB?}el-f(^?nUz*m0%i%<~3VDkB$$)Zv z2qYeicPLquLFn{5dy&~Cpnx*ovH1hkiQO>jaqJ$*`GCaTS&Ty4xOAYhfX9q(@!|AHG~k1@cn)R(&89%-%IOZ!v&!D>2y;xTBYQdGsF-Q(&RNm=jka)& zOzrZccVjM2e9M}k+KHTCcesFSe9v9#(5P-_4rc~Z*K}fU+NqPO0z>~+ja#UF;Wk#Q z8tw1yMa94+wXpghUqJr-n(^;{`kTe2U@aY(A_HT=mA?n4t-rHdQRg}zE;m2>q$-Yv z!%Y79Tu=doBfx`;{@CsF`kBYRTF%1~63LgDT?#+w?a>SXTk1%u8LjK0ga{-fmWttO zg*?VTa!#UIc|}A0V}%|#aYT!*suaq8nbzhB@-8pysJv~Vr*MxGCUHmraRNO+#9WG0 zcT3z^tI+i|VTzum@{fb33Dg`c4Yt^Fh%~&oa=4yqU6Ydh^jOJ@b?;;N_pp?1Q{uuDVt{}g>w8@AqEoXSS01A9NQnya*f$QVR@sZF|U7b#x z2XPWN8|fDTOOY`ODOA2&y`nOR{#hHit0-{S%Uu6~9VmPkr^B9CBt1Sskfwa)iFcM< zNx&tRThw1+O1nG+QiqL4!;U>_s`Z0grgdeBy@J7Wbg^n21n68KTe^v5otV;dNmDKl zAl|P}qxhP`)_c6R0jE4@Al}wK}VO zNX^!cGKtybRiN%i7QhC+waang>j4G;!wygr)$tMaaN#aPw% z+87`tDumr^a_r1Kqz8>W6r623B~~Q&K@{YZzh7_=%}(k>Ua(1z{f1adzY^<9SlnWA z?>%hyCZ6{Z^W=L8sLaME(fwi_j*c+4n(L8CFyAv!lHqibRTz>GG%eKMaN4r8KoXmp zvK$wTkr}A{iM$w#mbzwji-JE7;&~F`Lo|EJD&~Cot`FVTCYkZA)dS1|uAW&H1+lye!%B zk~MDqc<1*Nxh2>NS_lG}6yxS0xBcD4d&2g=hlQEgN7!${X}n1V>Qu)C5k)0xwB2TF zM6oRPesTnrx3JHlZK~CUA^T&>qe5mc7#-+B2pCYL3o)Z3z$XX$6^KY!@VmjMbPxBD zetzpOD4gLiElBVamfnhzEPIM5k4Izhb{an?QQxWsEUKjcFe&^A0wFFMzLe9u+HkMK z58tZ_sIJ4sNZytI(Y!-OK4L|r-E-`_JMT#X>a#1pUG>a=^30v5FZ`3F8;DsW zb#JD^a)fgicu1P@+@DY`b0)mXEO~`Bj2;RyjV(`RU&QsbM6CkJUtgZCGJDNSf=)u% zI~eNmMKDlcxWYjPNiBD##r$>Ba!T{%ik_C43CLOPH za-U6||Aw5s$2agbj=pzk+cG*?P*}7%!=~*bzUy)}7YslG$TfbKke~V+`gfiY>p1#1 z;go!hH)F{bizy>(RVUz?BdFCaFq19$n9QTkTZ-jSQ-k>oOL-}1$)sV=B3xl|AK7TeHi? z&iV~2H=vCt6<98{Hl6S(0tYo-!r7UtI3VB_5MIn%CL;WgY_|wwrG}Xj7)MAB38qZE z9H7Pjfg4OBO?q+{d3TV@TcafU%n&y&?g95!PuMrnRRYv+$X6e}xS6AkjyNsG`{fpt zdvCPO^uEG>@tjvRrjsT&E!du@NJL%tHucP;d(dFM$B$;(#scXCQJeZ$)4cmh*_Pk1 zDKidz?!pJj(nLt4_U=(;`eoAjLpHT?`DOWXe@~fX8sN*SzF67M>4#W|ij}pTuU8+SpJKOo(_vkJXArtUnck8`C!macVnFeL4G zOxL)9h@iIVlg+^}$`?K%ZFrLiL$&bAUwje&jHU)uaJ*-6@i?j>1;R(O zRFS~$rxo&CR7>t|m!}L+{K{e}kltAT#7Hb&Cnz_(v`TzmC)|u(*+Mp@_1ZqbMAjuA z0Km7NySDa(<8aZtUYs(&rQ>Vj*8!0u?rfyMf*xTN|AD~hCre7aJ>!>OdlooE-!^L2 zaO2~j=~k|y6?7w8`=QqE&c=7*de^Nj3ENjqIPDH^Kv0PR+v)4asF-QKuzM=^L}8Qc zKKDR@jw(iHIJ2BG@2mcO2$KB%=5@wTy@0-6urwT2j|3&XJ!880w?X?iAbR?G21?5_ zx;=L6y!F~d$(||@$BAbrPNT{JW|)W^idrl5IO0o4#`|t=JO5re3m8wE6dQINgJVVO z5;r1lF>=p|q`Xo0!&@=>7?4Xm$3FTSvMLH1x4(4L7T`DA3-XAnxZ5)vxY_lhRaS(K z^+mXnqZ${+)N$4I^>n`E+1d--5!;M4{*W8xNyi{xoQ^LmP_ci%Ujd^Kba%CtFHIc~ zqu3J>!HJfOt|sO2o7q zWBnU)KtJTY%dG|ykWO>rmdz7g^|f`EQpQSo4_Ob>38wV*^iI7ktKz)5oH#x$RTQQ7 z$!x*FF4ozYC&}DVu=Z2?>K9ILUO3+L;nIblQg>dVE-`-M!$mK4AGY%0LviXbO}Fsr zTJx`M;N!yG(NW4dHMduNk#fKJN$l z#{$Cfn*BY*RQ!)`{JNsSdbb4H|A4_Q6;vNKkPpv#g(F7x=5b@vzyjI7Z)YOD1ZYn0_ems0LXKp}zS&V+Gn2wu{r+K&O zr|RQreXul;D0>Mk6j%}`fEa#Ew(}1 zvQT#MP;>)Mh8?J%L~WtyJa(iRt&MxIV82)gJfOELt5Ua}QkN|`VzCF&yk@p}0#F8s zZrO;SiWJMuxGZNR4YG|n<->Kf2tg5SFCDLXq#SYLk<_EQw|sxa1YKI_`9=luB$Z{< z`0=BWa=gdXFAzBpG8v%nsU5N{r_`*ktE@JmekPE=E>Yo^MDX zTqCG&25`fgVIJ<=9PFYt3`1Dle%#=BNhg8lD%R@HH3D8#zX1<+Kc01u+_{FS^~Pjq zwoCy3lU}X082EK>n{}-GILNWF^+-1EK9}9SQoqo12^U#5OZUG}*@VKou^&P-fVZhf4L?N!1DCa`2FV2xH`QM^}LLto0< z$pPxQX|Jo#ImWPPo^h1zo^wi4ozp>I9Yhv{DC$qoDwOE!sk~Xwc#-v?>Z-WO(XtN_ zVzP$DV^Tb?V6k2JF|+Jx2!wm5h-Z-k{NdYQh600BEMP48C-q{pFG|F^JM((d+>jrjDLMfp@OqiF@MRXsrx0#-J*@5E*^Td zZxl^+SD0@u=+>@$9mIrzNrMs}M9@Wheo|UYAW#9+j?#s2#=a+*yK57A#c*cj&!wW~ zqHD=%Pav%|gXQ-KN0fS`*2n1rEHg|9FrZ{Z78GA0d^~c-FcXxcD)eJ{`Q^<;NmV|1 z(3N~KA5fq29P(Kc0&QFd$-aY!Byir6?^V@AO%Iy;B7dmkYEhymlwVZ^Dwyc!-Hv^A zLJKLr4FBpGzvVrS@-}bZ5&wbwbl3|J(TJD&`rpb6zU;rp)V9$etJabo(yydF8vfA5 zq*QCbY7;NCEkyRLgDD)MRbExJR^ZF2C5I&&h8;4+LhL_*(!6tPH0x26-erWjEJ?E|Yr8xPzy`#$TbtU=Ns>K7 zq3>^+)|m4VSv-VAOp#i^!ojy|Na`%DNLN>?{@83rhwmBb3S6s%~mfDQ3oSK{Ms7&+22}hh2E#f1Q ze3J^3PmSKOFu|v<{OZU3ZYdVqQ(T2YZ)f@1Cm07L$cgRYbHXxCg3YZ>zpbrVB1x*H zQs_$BsNp}g_f*Aq!8xeYj=IoZ%kCA`2+vxpbyuHf3sU)>{tEA$eh`3Q=7NkwD@gQ*r)KAz`MPrmrn1c28^zJ)Hf(WqXFOQvGNjO4i=j_G1 zpK8R7)#otaL0BoBwRn}d>dFQ?aJFMJA2#!@w>r=>g4l@@wWj?IwDM@&@%M>P%{ZFq z+m!OSkn|}3p#zYkqg2QA96e7lvxagSh?`)vpn*!-MT_kMADxoQJ&guAcS))BB`UxE z=3WADxyqtyp9;KXVaaQ`M|{nbFdZMT#DAq+9W@wbBpR`^wS}o}(6or)l4S0u6KQFs z;@6f`DhSkUiOs2hVld?+uxa#DmF^RO_UMdH8z?R5AXMzw`Ab;2y_vy|$<)!9C)O1S zHjTZVnVfb90#J!W_5#B9-B~lYK5a!h)`YLnG%loaLW?rj?WVJm)<&)EZ*@I1G~AjE zXb1SObBX4K%^9WB^CMS;>ehHW@2Du)JczA{#mtn4tR1U3_;W!5Y8!czW82*$fox6U z@y)D?&@4ouuWR<@(xrCtwxSZmR? z2Ya87y5@&a)BB%I`p~z17%a1_gjf5_bEb7a@9I!%Hv+Y$X0&jKT*M<2M-kIkeqxB(+2$erXY?Fd(Cf1WG9f|X9D5u zJ04UCM3a?8(|hY+=>{)$l?!%JC%|dQqjs`wBs_{p1=@Um6fA zFbbk@eLsDH%`So4ZUwsCZ}SOno8vN0B|^3@w7#x0(hnSnOOiU3mqhm~E;mE=b$=zN zb%1`ck>y!kU&w`aW=BCM{f}r?@AbwR{$wGsMGSzvJegq~C1_*XY!XjWD?61on-Ds6 z*@(;$O~*wK2z=|K4;QVC!bN=M#Vn;IW7hmnjeG>l>Yf}e=%a5XF3X45lCthG+b}+K zHD0%XQ8mvKF$F!vp|36M-NJUzQBEy}gId^qyTD>v|^!&7)Kl0wtZe`P&_Ak=v)~R|F&TUf+OncIn3>$NIQw`>em_^~wlm<=oP3VZx zAz7%0ubu1`V@K+gI<6E)Md0N8O!BKRjwE{|K<37ALqVXe^s~{!0gTAY@tIy`#Fubn z|3!c*k*OQwVj?7DK_SkTaUr1stx$mrG7mkyS!6%4Vw6i+?7Zhb;ho4|8b}~r^ zVKF?gQo&oM%>RR<(C)+w(3JixevlZcD{O%Hp)gdQGX2%{DOpXbs`PEL1vtG5#HpU` zw4VONGCmd`uCSJTa7hATBK{nCuBK5#SfZyh&Oez)b6O|A3IK}zyMaZyE^*zuB_rFk z+xDE{hz@ZxU0iSnY5@PopI_1IZ5V4@`%CfKV zn!3cCF3^?cIGs2gq2FIrc8n_7t<}6NCI>hP}bw}O+exxcl5HI!*zaFG>+$Odf7;kxvzHhh(-E;*KbaR&jgr&i? zg=)&K0sot=#iY3J7`-Lb@fWMRPO>k&ja=$C)T%ytZMR!I$rV|paRCXF-4^lHy-^D< z%hq^U!pvg8#$O}_;bwIhI;?!1Ja*iB^-DTQ-_7;vpdbNH(mvUgg4bWl zC|tk$NQ{ESK};XGXHKO>>tYul6mz>uYRONnnsA~eF+=_MNN_80f{PpsDAv9kPV4I2 z33jg#HX%D-C2RK)snC??Tz~QLm)`@Kbkco=Dxd_=v(K}%U7AsGVM#P);^U#F0bSd1 zb?8^!o&pc9AdO+VpEZ8_-F{9m`y#uvKI~~aY5en{BIMzgDq*3C!sp$E-uGl(C9oD_ z0KDRL)Xu7^-6h`tF{s4+zNWa-xLyjt+8u$(&Z*%#URK>yCyt&x?c|+iz6$=>RmD!x zjUI^QIHCo)U_jMolg?bi6~HhqBMk~|y(f1NSn|l56{NGp&}6%0Nr>)=CG-(6fN47J-?`%C@SG&FW2aLcI+&RZ57X4EBnA_q5F<3dRaU1Hx@52Va7|6m8{?$?q#C0Dgk+1o?pK7WveX9 zi<@*UNPOd!@Do`BvcUb%lftd0qb1U%Bt=JQ3GFkf^;M_2Fu$}`T7TsC6{o;v?j(BO+Zm+ne4bLv^x&%=7D(pWM0x4% z-}M{dpk@LskosI$Qa#43Ja5fl95w1~5%iNR;Re-@-w^8ph#P(pS4C8gCFOUQtOM~^ zRz0^pXkD<^k6T@hz5y1)!t(v*&Pc z)4~{#P(Na2G9OG2KxOED?^Wp2*nv|E{3mNN|2IE=UGF!fWAQiSZ~%10#MSEJRff2B z(mRrD@BYXr^^@r(1#B1qv?PLQVrQ`v=YsKRN8Y8UUI-wJwCxo(7hNfUnC84cZ1`Y$c+RZ4;`5Q&Ox2kI;~`3P;9sFU=LbDyn>!hTk~x*wl3DR*j3EKK;w zhQfvJZ8V=Gp;W60Fus;AC~Z0QY?&9M=O`YSM-yJHAzxP%fNb6C(5a8lDNKDjax^Tp z=xk?uWmA+M(1nYOrZ%W{XUgl!zkJVqetw&}u?fK6_G3nTgc{~dH|n*{l9?Hw%?eVGevg}5ZQ@@);@88JIwdF$VA7y{`FYE2A4y=Ov{^CG5dC^{*O z_{cO+gFZa#sS?BGl2Qorn<>eEbb{yi9}_n`i-FS*Lqw_XeY;5v?L`AFL10axo=ebE z;bUtG1r2r9Cl7NOd|3N&W;aH4;$mK9Psv|Vf^^^TRox>qvJZO$+)YGh(&6P?OiHv? zljC|5-zXndzpHO{6=`DFKkdgAO1)QAKD=Gd|0<`CtG`8F058hTb08|>BeZ~{wdcvp zK~VuunnB)oniWP=tHO408aLt4!UN=8J>J&>=iS<7`w2YEd?aJAY&aIAT^=@2tm}aM zqj;NOT%B zR6^8V-BzT}BPTs~!)J?-XW+yE4pMSQOGfR2&9ZLsgh0mSnMn%NaIgS0=JQFsdjGZ! zyMI?Dl~RSvww7Br_E=O>4Jcd+$XX>;N!A!svbajw`$`Im$%&I=%7j@lqd-O&m#8LY z9&f~c#8r-e^Q{|nOdZAv`#dON_ic?5r;*6I`^EY9>5f{S?~C;6c#`g|&ic13qy}uQ zZ7Syc*vb%~q%j4SIXz766LFDQ;eyD`!mSsfxAh(xypl*Zj5r0OVX_q45V972HeD)@ z*Il7)tjx?XYMXNd>irzET#&c6H}*b%b3X0TdEAK8^bEA`iqcOSGn;JA}2o7CMCxEA$BizH<2@!HngS^e;QsGI0~SQi&lUH_o1X|aqLu9d(-j>JJa z*D%bfO=+HKyL~_4fd7W<7V}82;$?uoFEdEC1Op%1kuHTpBM{rutOm1rDkq;oFcua9 z85^m!^eh~&%RdtHxGhJ#Cj-?ol2l~(VF$|~->;vH_PGSsY%)}$W;$QfIx@U}y#P>v zGI=h&$Ilbn-9s59R7}>_Znrv1&G-fzM{NNa*__Gxd!@;>M3vec>AH}yK&D_T$kJk6 zRorD!s+lksN22LYgJ|ChoZ0sRfIbC77WPGI^=_FF);E9INB2`p>5*wR8(O)hmBj!A?czdn8Vl96#cXnT*DGddz6W7xYJ?JC!Y+f@z{1$@bzRcY% zYdf1q=0j06(VnX?0_9O#2T$E~Cwvi8wvvzr_A5(v0K!Sz7gl&!Ls^!liZ&!|N>p;W zTE#NU?!%DfvZ6V7aEP7|M@(7((N2dejzrZth^Bl4bLmn?Ur(qCK2$&qH}AE1V@pejtT~`mwbc zvzNc~4QS}U%n%Dt?thUBu|53ovnHwz17xW}sQK=cx1C5fblHXp#T8+lW{qG-gerKL zt~eCYuTSWwR=dkvyCa#-DtB(rTpxQYl1|)X{W2a=Pi2poM0|()a-oe3-A#Z~#uLmb zMTIAEz~F9RdT!M$)y))_`V`rxswU|I7H(1h2}9T4wlDs*c>ixMA)taQRHeYs{3mW6 z@Js*Azxn^oFL6gbW>kP1i4M%8|3wS7|HxJp7O0wFn{3qRo|6h2fR9t^U znEuOOlk0jqYNmJF-Xt+(MyT_Vb*(a}F@9|X%dh`;zNnx%c;WTe*9W>vRWvtCcH%9Y zxX_|L+v91)>daqeev8|k9(cBzbvx-tpLXAPNIl)nYPe#^;hTE$uLvQS()0epgHUU_ z(75+m7dS2zysUR1`yGG)Y7_K%YGWet<;2IQGUidel!6Q|0mJMk_F73|`#O@U_1e0< zc1E-XC4P8CM3y4Q8!(Xe>O7`D{va#ROt={JVu^(rD#CT*m`*fv8@(w|r>~?Ztd2N$ zgam)|G00GCJGByhH(j$qKl6{D1@}$^Yv+ww19qu_5;7$pI|aVrlaEo;R&zN z#my-p^Gbyu-=`!SN8NG#p)Bg)w6aS;m&8d3EBwfwW&F_7S5AHi}H%juNmjN3=2z*oZ zE4QH0Za}DCFd8VqGk4%O*(w%$eDp3Vm{I((Uhzut5751`ecZoYexA3r8D`brj7BB4 zdZhb~IfCjZ5@LITX!)>Vw!MC~IU=9ngy!@ktupS_oJ0*?E0)y^o;X_ zUzkJt0|I@}<>&xk>R+xg=|ZzsJ;N|h*(L2l^KRK7cSp4_vC_3Kagz}zpaH7(GEm&p z>WuIstKHO(_nF^`;WwZtGJoRgneeR3%5Up&)($UQuwQPPwF`U9Isho5;N7oJR*ULh zyeAfq)PW#D+(HiaVZZm77_fUN8l@A1{r=zF*uTHc|NDNzv2HlINc#(t%K}z{DY^*N zzwsMVbp0aQhx3GXd0Il2B(^un(w$);-+YUgVDEIjY6ngyT-_H`rbeN+CM?BJmZql2 zO3}rP@7ckMWc7h?*bux*17SXLWrGghSM*%sJVgcZ-HX^q=ssNiy7G-w&J7T6dYsUJ zC`&~Z{JQMKVdK4xMDV2pF*I5 zD3p1bs`R1sP>OXC9Xlg8J6-YdW!Z&>oFv0#o<*VnY4>Vse=VB!ID+d`xu~_-fgD=Q z&a0vyFY32}9SVDqWQD0Or}4=$Wf?NaWPaW*V~w**(AERAPJNanHh_Kqmw&CqiV3la+(tvQEJp(Q6Qc{ z>3Bkc-2La*-7ba5uVTT1vQO)6hAif_EMM4q1Zuy>?Ge_6dIQZAv4gjbKo7@797Y6z54Pb(@3X(QB_2MSr9^f8K^TD} zzo@Jc;{q_NL&D<+7{DkyBuQ6Ck)%|Of2lFhT_oT)@GC^#fN6=7TiKAL4~@=zb|{yb zcW(~7}3D52=+hpGO1{vUEu17Z2EUg=5z_8tdw_wmi7vQj+3%;R0S39XV zR^+KpKjj<;OF!Y8H&J&CTHMeXq7aHo_lFIRJM13xUj_Xq z{IVLPa8Fi?uEdC~SX2Ew8{AAcVriuXWHpMO#$RJ~zC0v=#cIBd3fQk?vgX|aVLBBm zj^wX4g-KcSF`wltM7FI&-+GE_r9I^s6SUVHrfruOJwczYIQtxba^d);MYb!sESUq} zD?q)L2p2p)bC@$TI^cuKx=z^HPA}FSkN10pkIeyK`yj)+mBgIqIehwprE9|l6 zEjU&!ALEb>`L{rp^G5gFh6sGN?hCazg#w?EkitDboXq`Xn4Ixyi>PbcJrj`0E1@GT z#aX|a7I=Fk><+ceYt%E)J`=!!lUQ=|O#CyGS37gwzjNlh`B=<`qjwr-87dBSwX4dj zp{21nySe%V*W5fNoQYC8ulML00EoT;n3P)fM(#6wbis7{KC)edbF`v}y7~^C7+)m3 zbgq0=ua=RLPhv9KeB~lhIWpU_B$u3%qX1fKrMsDS-B)`HpO7T+m{t#JJMidCc?ZQ;oGAm8o#hoZpWAO}L z<2w}CqR?d9*kP{d4pqVFX{UZOv~*X`Tu}iz=bSB^6~dLT0fKI&6I_W^g)F-5T;v1C z-A@T&9tRLbzPEfTG6e)c% z;|_<8w9^YVo(5Lb)`5HZZCz0ka|4(->x?%ZYKK?4gf+^}Q64CWMOy1n^oT?_#%5lo zkc1O?MGIKpZhNgF#pimEC9iCzIYA#O^(6M_b!N>P)0R2m>7*8s?{Z{hAG(6QNQ8@? zE~hzm$}0qznZ;ZcCdsJ}ncRwLRfK&u0X^%uoXd7zsoIQw@=$^})#7xCR7sr6D*}q= zGw_)c1=T9k6-b2&2e15K`q=H=)(jU&->##eL21qe}w& zATf!!uVS-6DY{@`Hdm51l$J~GV0Dmk??yH>+1eu-5K5y`${oRAA4+f&8phKJ1Go*2 zJWg0HP_MATYPS)dofTbs!y#d{Gb1#)sLZ zUbv){Unn;st^e{T8^7BU8V3(PIm}Td-?1cMKh-~OIbiIs690*N)SI4>h6E<~xlJfG zU>&=MZO|<@Qbr%SbuxyVgYB*90``0_+Cdixb&QDmSng|Cwk_D)cZX7^HMmVB2DYtj zLJIeZd>*VM!OM&b3DvOyla1bTR@6j{HCMBKJU7V50?XuNR<%&3(1eGQ{z{npK_UG; z{$yiPH9&hrsnYAV2yIy1wmQv-@l1=GhLR=m)k;xNs|iS56ud!}5>rjEGVfnWwa(`< zNYapar1Q6;wb3Oa{F+UtwoT4C)s1F5l}5=(vp%C>oi}=iTX_%V>)fzHN|J8OYRkIu zTV3v2yK}F|tkQ1aQZpy-1PVGof2nlnLSW(SG?uK=G1^0Z{Ncb}1{~=WIR;eH>C(9H zAGPJNk1RZ&)?GPU7}+dzqTRt8kJJoC()14fjen{SAE zi$&wKRkLaW58Y6q)>W3p*U6Oo-n=pyzbm7I!?3pM(2JsWZx!_Ace5(v1I?Ujwe`Jz z8Y@y1;F2H}v<#Wb-3`#f8p&oV?(ux|IB0N~|JZC&Q(ZxbfrpU<03B8>a%R{PP_8@;=^;xi2Ni0&#rRa z%mPfWv9fN~CE5vRD5_2oe9JVZK`a4sUNxa4Ss}LYoa)TOg^>J@C5YG!V~Y{3HQIKx zACBJ?D-N*s*Zp~@o{&bo=+fTrnR}6<(J_VDdH$z-YYQX9 zXm05oNDq1)!omr7((HOY%L6}m$UhP)A_odJ^Pqc%1E-YR?4Vqhp3rzX$jnipc9-0@ z!_@4yIp)Awb3TRd8l8tL87ZMjiPIJ>+>7cfd|8AcnmFDxO0L(5nSdH-;f6V5!Hk+X ziDJ(X?>Q#t?()$mGlUd(jtr@Jne>c=JonT(s}{$gvZO|i)FV1bgrMQF^b&{ajhiYO z6-VLG1^RhjB@c4q%#@<4O!X9T`G6Nhwh+eqpPqX$F4S|8shVAE`}FQl${P#j=6SPI!fBKRYOMr@((4C+&g+RvhRsZ zJ5%MVH74QpCpYJy?M8&Rik!WLi@>&_XizZGD3GialknSw&V7@>rF`k$VyI;CAZF> zQu*Z289cD!Lm15h+U$R&6LQEGGB z`Q}CReyUcH90mBk;=U6#{^j2;pZceT33ZruQ(WUV%c!U;CC9r|CkJ*YgTB&&{~ewC zzbm9(?qfv!`kBUVp|qwegSGyZ;xZoYev2AYBG=SCy~Ww18*}c^x=h=bb?PP*E)MB7 z`fozI3!BgYca-3Z^P5mxMfIne|LKEwO_ly*Z23RNrhYqi?;m6TSq|R!d%ff-|EL$$ z_nUh45`HgN>Hn2uVT4%#%M9g9car_Tu^=jbjCq^m*vF_XFJT+`X@>Do{iU z`zzk;w>M?$&b0yCs0#SgTwiTM8N&bm)_^Mfb=JQs@K*)?pH+Yh6}h@b7kZi*aAxmI z=niD{`>%4&H`GoU#qVZOS3UU! diff --git a/src/args_config.rs b/src/args_config.rs new file mode 100644 index 0000000..924edd1 --- /dev/null +++ b/src/args_config.rs @@ -0,0 +1,468 @@ +use anyhow::anyhow; +use clap::Parser; +use ipnet::Ipv4Net; +use serde::{Deserialize, Serialize}; +use std::net::Ipv4Addr; +use std::path::{Path, PathBuf}; +use vnt_core::context::config::Config; +use vnt_core::nat::NetInput; +use vnt_core::tls::verifier::CertValidationMode; +use vnt_core::tunnel_core::server::transport::config::ProtocolAddress; +use vnt_ipc as vnt_core; +use vnt_ipc::port_mapping::PortMapping; + +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct FileConfig { + pub server: Option>, + pub network_code: Option, + pub ip: Option, + pub no_punch: Option, + pub rtx: Option, + pub compress: Option, + pub fec: Option, + pub input: Option>, + pub output: Option>, + pub no_nat: Option, + pub no_tun: Option, + pub mtu: Option, + pub ctrl_port: Option, + pub port_mapping: Option>, + pub allow_mapping: Option, + pub device_id: Option, + pub device_name: Option, + pub tun_name: Option, + pub password: Option, + pub cert_mode: Option, + pub udp_stun: Option>, + pub tcp_stun: Option>, +} + +impl FileConfig { + pub fn load(path: &Path) -> anyhow::Result { + let content = std::fs::read_to_string(path)?; + Ok(toml::from_str(&content)?) + } + + pub fn save(&self, path: &Path) -> anyhow::Result<()> { + let content = toml::to_string_pretty(self)?; + std::fs::write(path, content)?; + Ok(()) + } + + pub fn to_server_addr(&self) -> anyhow::Result> { + if let Some(server_raw) = &self.server { + let mut server_addr = Vec::with_capacity(server_raw.len()); + for x in server_raw { + server_addr.push( + x.parse::() + .map_err(|e| anyhow!("invalid server address '{}': {}", x, e))?, + ) + } + Ok(server_addr) + } else { + Ok(Vec::new()) + } + } + pub fn to_port_mapping(&self) -> anyhow::Result> { + if let Some(port_mapping_raw) = &self.port_mapping { + let mut port_mapping = Vec::with_capacity(port_mapping_raw.len()); + for x in port_mapping_raw { + port_mapping.push( + x.parse::() + .map_err(|e| anyhow!("invalid port_mapping '{}': {}", x, e))?, + ) + } + Ok(port_mapping) + } else { + Ok(Vec::new()) + } + } +} + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +pub struct Args { + /// 服务器地址 例如 `-s quic://127.0.0.1:29872`, 支持quic/tcp/wss/dynamic + #[clap(short, long)] + pub server: Vec, + /// 网络编号,相同编号的会组同一个局域网 + #[clap(short, long)] + pub network_code: Option, + #[clap(short = 'k', long, hide = true)] + pub token: Option, + /// 自定义虚拟IP + #[clap(long)] + pub ip: Option, + /// 启用加密,设置加密密码 + #[clap(short, long)] + pub password: Option, + /// 启用quic优化传输 + #[clap(long)] + pub rtx: bool, + /// 启用压缩 (LZ4) + #[clap(short = 'z', long)] + pub compress: bool, + /// 启用 FEC 前向纠错,损失一定带宽来提升网络稳定性 + #[clap(long)] + pub fec: bool, + /// 入栈监听网段 + #[clap(short, long)] + pub input: Vec, + /// 出栈允许网段 + #[clap(short, long)] + pub output: Vec, + /// 自定义设备名称 + #[clap(long, alias = "name")] + pub device_name: Option, + /// 设备id + #[clap(long, alias = "id")] + pub device_id: Option, + /// 关闭打洞 + #[clap(long)] + pub no_punch: bool, + /// 服务端证书验证 + #[clap(long)] + pub cert_mode: Option, + /// 虚拟网卡名称 + #[clap(long)] + pub tun_name: Option, + /// 关闭内置子网NAT + #[clap(long)] + pub no_nat: bool, + /// 禁用tun,禁用后只能充当流量出口或者进行端口映射,无需管理员权限 + #[clap(long)] + pub no_tun: bool, + /// 端口映射,格式为:协议://本地监听地址-目标虚拟IP-目标映射地址 + #[clap(long)] + pub port_mapping: Vec, + /// 是否允许作为端口映射出口,开启后其他设备才可使用本设备的ip为"目标虚拟IP" + #[clap(long)] + pub allow_mapping: bool, + /// 设置mtu + #[clap(long)] + pub mtu: Option, + /// 控制端口,设置0时禁用控制服务 + #[clap(long)] + pub ctrl_port: Option, + /// 读取配置文件 + #[arg(long)] + pub conf: Option, + /// 输出配置文件示例 + #[clap(long)] + pub conf_example: bool, +} +impl Args { + pub fn parse_compatible() -> Self { + let mut args = Args::parse(); + if args.network_code.is_none() { + args.network_code = args.token.clone(); + } + args + } +} + +pub struct CtrlConfig { + pub ctrl_port: Option, +} + +pub fn build_config_from_args_and_file( + args: Option, + file: Option, +) -> anyhow::Result<(Config, CtrlConfig)> { + match (args, file) { + (Some(args), Some(file)) => build_from_args_and_file(args, file), + (Some(args), None) => build_from_args_only(args), + (None, Some(file)) => build_from_file_only(file), + (None, None) => Err(anyhow!("neither args nor config file provided")), + } +} + +fn build_from_args_and_file(args: Args, file: FileConfig) -> anyhow::Result<(Config, CtrlConfig)> { + let server_addr = if args.server.is_empty() { + file.to_server_addr()? + } else { + args.server + }; + let port_mapping = if args.port_mapping.is_empty() { + file.to_port_mapping()? + } else { + args.port_mapping + }; + + let network_code = args + .network_code + .or_else(|| file.network_code.clone()) + .ok_or_else(|| anyhow!("network_code is required"))?; + + let cert_mode = args + .cert_mode + .or_else(|| file.cert_mode.as_deref().and_then(|s| s.parse().ok())) + .unwrap_or(CertValidationMode::InsecureSkipVerification); + + let device_id = match args.device_id.or_else(|| file.device_id.clone()) { + Some(id) => id, + None => vnt_core::utils::device_id::get_device_id()?, + }; + + let input = if args.input.is_empty() { + file.input.unwrap_or_default() + } else { + args.input + }; + + let output = if args.output.is_empty() { + file.output.unwrap_or_default() + } else { + args.output + }; + let mut udp_stun = file.udp_stun.unwrap_or_default(); + for x in udp_stun.iter_mut() { + if !x.contains(':') { + x.push_str(":3478"); + } + } + let mut tcp_stun = file.tcp_stun.unwrap_or_default(); + for x in tcp_stun.iter_mut() { + if !x.contains(':') { + x.push_str(":3478"); + } + } + + let config = Config { + server_addr, + network_code, + ip: args.ip.or(file.ip), + no_punch: args.no_punch || file.no_punch.unwrap_or(false), + rtx: args.rtx || file.rtx.unwrap_or(false), + compress: args.compress || file.compress.unwrap_or(false), + fec: args.fec || file.fec.unwrap_or(false), + device_id, + device_name: args + .device_name + .or_else(|| file.device_name.clone()) + .unwrap_or_else(default_hostname), + tun_name: args.tun_name.or_else(|| file.tun_name.clone()), + password: args.password.or_else(|| file.password.clone()), + cert_mode, + input, + output, + no_nat: args.no_nat || file.no_nat.unwrap_or(false), + no_tun: args.no_tun || file.no_tun.unwrap_or(false), + mtu: args.mtu.or(file.mtu), + port_mapping, + allow_port_mapping: args.allow_mapping || file.allow_mapping.unwrap_or(false), + udp_stun, + tcp_stun, + }; + + let ctrl_config = CtrlConfig { + ctrl_port: args.ctrl_port.or(file.ctrl_port), + }; + Ok((config, ctrl_config)) +} + +fn build_from_args_only(args: Args) -> anyhow::Result<(Config, CtrlConfig)> { + let device_id = match args.device_id { + Some(id) => id, + None => vnt_core::utils::device_id::get_device_id()?, + }; + let config = Config { + server_addr: args.server, + network_code: args + .network_code + .ok_or_else(|| anyhow!("network_code is required"))?, + ip: args.ip, + no_punch: args.no_punch, + rtx: args.rtx, + input: args.input, + compress: args.compress, + fec: args.fec, + device_id, + device_name: args.device_name.unwrap_or_else(default_hostname), + tun_name: args.tun_name, + password: args.password, + cert_mode: args + .cert_mode + .unwrap_or(CertValidationMode::InsecureSkipVerification), + output: args.output, + no_nat: args.no_nat, + no_tun: args.no_tun, + mtu: args.mtu, + port_mapping: args.port_mapping, + allow_port_mapping: args.allow_mapping, + ..Default::default() + }; + let ctrl_config = CtrlConfig { + ctrl_port: args.ctrl_port, + }; + Ok((config, ctrl_config)) +} + +fn build_from_file_only(file: FileConfig) -> anyhow::Result<(Config, CtrlConfig)> { + let server_addr = file.to_server_addr()?; + let port_mapping = file.to_port_mapping()?; + + let cert_mode = file + .cert_mode + .as_deref() + .and_then(|s| s.parse().ok()) + .unwrap_or(CertValidationMode::InsecureSkipVerification); + + let device_id = match file.device_id.clone() { + Some(id) => id, + None => vnt_core::utils::device_id::get_device_id()?, + }; + let mut udp_stun = file.udp_stun.unwrap_or_default(); + for x in udp_stun.iter_mut() { + if !x.contains(':') { + x.push_str(":3478"); + } + } + let mut tcp_stun = file.tcp_stun.unwrap_or_default(); + for x in tcp_stun.iter_mut() { + if !x.contains(':') { + x.push_str(":3478"); + } + } + + let config = Config { + server_addr, + network_code: file + .network_code + .ok_or_else(|| anyhow!("network_code is required"))?, + ip: file.ip, + no_punch: file.no_punch.unwrap_or(false), + rtx: file.rtx.unwrap_or(false), + input: file.input.unwrap_or_default(), + compress: file.compress.unwrap_or(false), + fec: file.fec.unwrap_or(false), + device_id, + device_name: file.device_name.clone().unwrap_or_else(default_hostname), + tun_name: file.tun_name.clone(), + password: file.password.clone(), + cert_mode, + output: file.output.unwrap_or_default(), + no_nat: file.no_nat.unwrap_or(false), + no_tun: file.no_tun.unwrap_or(false), + mtu: file.mtu, + port_mapping, + allow_port_mapping: file.allow_mapping.unwrap_or(false), + udp_stun, + tcp_stun, + }; + let ctrl_config = CtrlConfig { + ctrl_port: file.ctrl_port, + }; + Ok((config, ctrl_config)) +} + +fn default_hostname() -> String { + hostname::get() + .ok() + .and_then(|v| v.into_string().ok()) + .unwrap_or_default() +} + +impl FileConfig { + pub fn print_example(path: Option<&Path>) -> anyhow::Result<()> { + const VERSION: &str = env!("CARGO_PKG_VERSION"); + + let example = format!( + r#"# ================================== +# VNT 配置文件示例(程序版本 v{version}) +# ================================== + +# --- 网络配置 --- +# 网络编号,相同网络编号的会组在同一个虚拟网 (必填) +network_code = "your_network_code" + +# 服务器地址列表(支持 quic / tcp / wss / dynamic) (必填) +# dynamic 协议使用dns txt解析记录值 +server = ["quic://1.2.3.4:29872"] + +# ===简单使用以下参数可以不动=== + +# 自定义虚拟 IP (可选) +# ip = "10.10.0.2" + +# 是否启用quic优化传输 (默认 false,设置为true时开启) +# rtx = false + +# 是否启用 FEC 前向纠错,损失一定带宽来提升网络稳定性(默认 false,设置为true时开启) +# fec = false + +# 是否关闭 P2P 打洞 (默认 false,设置为true时关闭) +# no_punch = false + +# 是否启用 LZ4 压缩 (默认 false,设置为true时开启) +# compress = false + +# 入栈监听网段 (逗号分隔的 CIDR 和目标 IP),用于点对网,将指定网段的流量发送到目标节点 +# input = ["192.168.0.0/24,10.26.0.2", "192.168.1.0/24,10.26.0.3"] + +# 出栈允许网段,用于点对网,允许指定网段的转发 +# output = ["0.0.0.0/0"] + +# 是否关闭内置子网NAT,关闭(设为true)后需要配置网卡转发,否则无法使用点对网。通常关闭内置子网NAT,使用系统的网卡转发,点对网性能会更好 +# no_nat = false + +# 是否关闭TUN虚拟网卡,关闭(设为true)后只能充当流量出口或者进行端口映射,关闭后无需管理员权限 +# no_tun = false + +# 端口映射,格式为:协议://本地监听地址-目标虚拟IP-目标映射地址 +# 端口映射用于在本地监听指定端口,并将收到的网络流量经由指定虚拟节点转发到目标地址,从而实现跨网络或内网服务访问 +# 例如 port_mapping = ["tcp://0.0.0.0:81-10.0.0.2-10.0.0.2:80"] +# tcp://0.0.0.0:81-10.0.0.2-10.0.0.2:80 则表示将本地tcp的81端口的数据转发到10.0.0.2:80 +# tcp://0.0.0.0:81-10.0.0.2-192.168.1.10:80 则表示将本地tcp的81端口的数据经过10.0.0.2转到192.168.1.10:80 +# tcp://0.0.0.0:81-10.0.0.2-anyonehost:80 则表示将本地tcp的81端口的数据经过10.0.0.2转到anyonehost:80 +# port_mapping = [] + +# 是否允许作为端口映射出口,开启(设置为true)后其他设备才可使用本设备的ip为"目标虚拟IP" +# 开启后虚拟网络其他设备可以使用此设备当跳板访问其他网络 +# allow_mapping = false + +# 控制服务的 tcp 端口 +# ctrl_port = 11233 + +# MTU 设置 +# mtu = 1400 + +# --- 设备配置 --- + +# 设备名称 (可选,默认读取本机 hostname) +# device_name = "my-device" + +# 设备 ID (可选,不填自动生成,不同设备ID不能相同) +# device_id = "device-id-xxxx" + +# 虚拟网卡名称 +# tun_name = "vnt-tun" + +# --- 安全配置 --- + +# 加密密码 (可选) +# password = "123456" + +# 证书校验方式: +# skip 跳过验证(默认) +# standard 使用系统证书验证 +# finger 使用证书指纹验证,服务端启动时日志会输出指纹, +# 例如 finger:3bdd8675606837cdf95d5e13445606315762315a78555f9da652940a25feaec1 +# cert_mode = "skip" + +# --- 其他配置 --- +# 自定义stun地址,分别用于udp打洞和tcp打洞,需要单独配置,不设置则用默认stun +# udp_stun = ["stun.chat.bilibili.com"] +# tcp_stun = ["stun.nextcloud.com:443"] +"#, + version = VERSION + ); + println!("--- 示例配置文件内容 ---\n{}", example); + if let Some(p) = path { + std::fs::write(p, &example)?; + println!("示例配置文件已写入 {}", p.display()); + } + + Ok(()) + } +} diff --git a/src/extract_wintun_dll.rs b/src/extract_wintun_dll.rs new file mode 100644 index 0000000..a633404 --- /dev/null +++ b/src/extract_wintun_dll.rs @@ -0,0 +1,33 @@ +use std::fs; +use std::io::{self, Write}; +use std::path::Path; + +#[cfg(target_arch = "x86_64")] +const WINTUN_DLL: &[u8] = include_bytes!("../dll/amd64/wintun.dll"); + +#[cfg(target_arch = "x86")] +const WINTUN_DLL: &[u8] = include_bytes!("../dll/x86/wintun.dll"); + +#[cfg(target_arch = "aarch64")] +const WINTUN_DLL: &[u8] = include_bytes!("../dll/arm64/wintun.dll"); + +#[cfg(target_arch = "arm")] +const WINTUN_DLL: &[u8] = include_bytes!("../dll/arm/wintun.dll"); + +pub fn extract_wintun() { + if let Err(e) = extract_wintun_impl() { + log::error!("extract wintun.dll {:?}", e); + } +} +fn extract_wintun_impl() -> io::Result<()> { + let path = std::env::current_exe() + .ok() + .and_then(|p| p.parent().map(|d| d.join("wintun.dll"))) + .unwrap_or_else(|| Path::new("wintun.dll").to_path_buf()); + + if !path.exists() { + let mut file = fs::File::create(&path)?; + file.write_all(WINTUN_DLL)?; + } + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f4ee9bc --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod log; diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..738c81a --- /dev/null +++ b/src/log.rs @@ -0,0 +1,61 @@ +use std::io::Write; +use std::path::PathBuf; +pub fn log_init(log_name: &str) { + log_init0(log_name, None); +} +pub fn log_init0(log_name: &str, yaml: Option<&str>) { + let path = PathBuf::from("logs"); + + if !path.exists() { + let _ = std::fs::create_dir(&path); + } + + let log_config = path.join(yaml.unwrap_or("log4rs.yaml")); + + if !log_config.exists() + && let Ok(mut f) = std::fs::File::create(&log_config) + { + let log_file = path.join(format!("{log_name}.log")); + let mut string = String::new(); + string.push_str(log_name); + string.push_str(".{}.log"); + let log_file_pattern = path.join(&string); + + let c = format!( + r#" +refresh_rate: 30 seconds +appenders: + rolling_file: + kind: rolling_file + path: {} + append: true + encoder: + pattern: "{{d}} [{{f}}:{{L}}] {{h({{l}})}} {{M}}:{{m}}{{n}}" + policy: + kind: compound + trigger: + kind: size + limit: 10 mb + roller: + kind: fixed_window + pattern: {} + base: 1 + count: 5 + console: + kind: console + encoder: + pattern: "{{d}} {{h({{l}})}} {{m}}{{n}}" +root: + level: info + appenders: + - rolling_file + - console +"#, + log_file.display(), + log_file_pattern.display() + ); + let _ = f.write_all(c.as_bytes()); + } + + let _ = log4rs::init_file(log_config, Default::default()); +} diff --git a/src/main_cli.rs b/src/main_cli.rs new file mode 100644 index 0000000..55abef0 --- /dev/null +++ b/src/main_cli.rs @@ -0,0 +1,142 @@ +use anyhow::Context; +use args_config::{build_config_from_args_and_file, Args, FileConfig}; +use route_manager::Route; +use std::path::Path; +use vnt_ipc as vnt_core; + +use vnt_core::core::NetworkManager; +use vnt_core::utils::task_control::TaskGroupManager; +pub mod args_config; + +#[cfg(windows)] +mod extract_wintun_dll; + +#[tokio::main] +pub async fn main() { + if let Err(e) = main0().await { + log::error!("{:?}", e); + } +} +async fn main0() -> anyhow::Result<()> { + let args = Args::parse_compatible(); + + vnt2::log::log_init("vnt2"); + log::info!("version: {:?}", env!("CARGO_PKG_VERSION")); + #[cfg(windows)] + extract_wintun_dll::extract_wintun(); + if args.conf_example { + FileConfig::print_example(Some(Path::new("example_config.toml")))?; + return Ok(()); + } + let file_cfg = if let Some(path) = args.conf.as_ref() { + log::info!("loaded config from {:?}", path); + Some(FileConfig::load(path).context("failed to load config")?) + } else { + None + }; + let (config, ctrl_config) = + build_config_from_args_and_file(Some(args), file_cfg).context("invalid configuration")?; + + log::info!( + "server: {}", + config + .server_addr + .iter() + .map(|x| x.to_string()) + .collect::>() + .join(", ") + ); + log::info!("network code: {}", config.network_code); + log::info!("device id: {}", config.device_id); + log::info!("device name: {}", config.device_name); + log::info!("cert mode: {}", config.cert_mode); + log::info!("compress: {}", config.compress); + log::info!("rtx(quic channel): {}", config.rtx); + let sub_input = config.input.clone(); + if !config.input.is_empty() { + let x = config + .input + .iter() + .map(|v| v.to_string()) + .collect::>() + .join(","); + log::info!("Sub network input:{x}"); + } + if !config.output.is_empty() { + let x = config + .output + .iter() + .map(|v| v.to_string()) + .collect::>() + .join(","); + log::info!("Sub network output:{x}"); + } + + if let Some(password_sign) = config.key_sign() { + log::info!("password sign: {:?}", password_sign); + } + + let group_manager = TaskGroupManager::new(); + let (task_group, task_group_guard) = + group_manager.create_task().context("create task group")?; + + let mut network_manager = NetworkManager::create_network(Box::new(config), task_group) + .await + .context("create network")?; + let reg_msg = network_manager.register().await.context("register")?; + + if !network_manager.is_no_tun() { + log::info!("启动网络:{}/{}", reg_msg.ip, reg_msg.prefix_len); + network_manager.start_tun().await.context("start tun")?; + network_manager + .set_network_ip(reg_msg.ip, reg_msg.prefix_len) + .await + .context("set network ip")?; + if !sub_input.is_empty() { + let if_index = network_manager + .tun_if_index() + .await + .context("tun_if_index")?; + let mut route_manager = route_manager::RouteManager::new()?; + for x in sub_input { + let route = Route::new(x.net.network().into(), x.net.prefix_len()) + .with_gateway(x.target_ip.into()) + .with_if_index(if_index); + if let Err(e) = route_manager.add(&route) { + log::error!("add route [{route}] error: {e:?}"); + } else { + log::info!("add route [{route}] successful"); + } + } + } + } else { + log::info!( + "启动网络:{}/{} (无虚拟网卡)", + reg_msg.ip, + reg_msg.prefix_len + ); + } + let vnt_api = network_manager.vnt_api(); + if ctrl_config.ctrl_port.is_none_or(|p| p != 0) { + tokio::spawn(async move { + if let Err(e) = vnt_ipc::server::run_server(ctrl_config.ctrl_port, vnt_api).await { + log::error!("ipc:{e:?}"); + } + }); + } + loop { + tokio::select! { + _ = network_manager.wait_all_stopped() => { + break; + } + + _ = tokio::signal::ctrl_c() => { + log::info!("Ctrl+c received!"); + break; + } + } + } + drop(task_group_guard); + log::info!("stop network"); + Ok(()) +} diff --git a/src/main_ctrl.rs b/src/main_ctrl.rs new file mode 100644 index 0000000..dfe9112 --- /dev/null +++ b/src/main_ctrl.rs @@ -0,0 +1,42 @@ +use clap::{Parser, Subcommand}; +use vnt_ipc::message::ipc_request::IpcCmd; + +/// 操作vnt进程 +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + #[command(subcommand)] + command: Commands, + /// 核心进程控制端口 + #[clap(short, long)] + port: Option, +} + +#[derive(Subcommand, Debug)] +enum Commands { + /// 输出当前程序信息 + Info, + /// 输出客户端ip列表 + Ips, + /// 输出客户端信息列表 + #[command(alias = "list")] + Clients, + /// 输出IP路由信息 + Route, +} + +#[tokio::main] +pub async fn main() { + let args = Args::parse(); + let port = args.port; + let cmd = match args.command { + Commands::Info => IpcCmd::AppInfo(Default::default()), + + Commands::Ips => IpcCmd::ClientIps(Default::default()), + Commands::Clients => IpcCmd::ClientList(Default::default()), + Commands::Route => IpcCmd::AllRoute(Default::default()), + }; + if let Err(e) = vnt_ipc::client::run_client(cmd, port).await { + eprintln!("{:?}", e); + } +} diff --git a/src/main_web.rs b/src/main_web.rs new file mode 100644 index 0000000..abe2eb7 --- /dev/null +++ b/src/main_web.rs @@ -0,0 +1,36 @@ +use clap::Parser; +use std::net::SocketAddr; +use std::path::PathBuf; + +#[cfg(windows)] +mod extract_wintun_dll; + +/// vnt web服务 +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// 本地http服务监听地址 + #[clap(long)] + addr: Option, + /// 加载vnt配置路径,配置内容参考web端的配置格式 + #[clap(long)] + conf: Option, +} + +#[tokio::main] +pub async fn main() { + if let Err(e) = main0().await { + log::error!("{:?}", e); + } +} +async fn main0() -> anyhow::Result<()> { + let args = Args::parse(); + + vnt2::log::log_init("vnt2"); + log::info!("version: {:?}", env!("CARGO_PKG_VERSION")); + #[cfg(windows)] + extract_wintun_dll::extract_wintun(); + let addr = args.addr.unwrap_or("127.0.0.1:19099".parse()?); + vnt_web::run_http_server(addr, args.conf).await?; + Ok(()) +} diff --git a/vn-link-cli/Cargo.toml b/vn-link-cli/Cargo.toml deleted file mode 100644 index df9ef7c..0000000 --- a/vn-link-cli/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[package] -name = "vn-link-cli" -version = "1.2.16" -edition = "2021" - -[dependencies] -vn-link = { path = "../vn-link", default-features = false } -common = { path = "../common", default-features = false } -tokio = { version = "1.37.0", features = ["full"] } -log = "0.4.17" - -[target.'cfg(windows)'.build-dependencies] -thunk-rs = { version = "0.3.3", features = ["win7"] } - -[features] -default = ["default-feature"] -default-feature = ["server_encrypt", "aes_gcm", "aes_cbc", "aes_ecb", "sm4_cbc", "chacha20_poly1305", "port_mapping", "log", "command", "file_config", "lz4", "ws"] - -openssl = ["vn-link/openssl", "common/openssl"] -openssl-vendored = ["vn-link/openssl-vendored", "common/openssl-vendored"] -ring-cipher = ["vn-link/ring-cipher", "common/ring-cipher"] -aes_cbc = ["vn-link/aes_cbc", "common/aes_cbc"] -aes_ecb = ["vn-link/aes_ecb", "common/aes_ecb"] -sm4_cbc = ["vn-link/sm4_cbc", "common/sm4_cbc"] -aes_gcm = ["vn-link/aes_gcm", "common/aes_gcm"] -chacha20_poly1305 = ["vn-link/chacha20_poly1305", "common/chacha20_poly1305"] -server_encrypt = ["vn-link/server_encrypt", "common/server_encrypt"] -port_mapping = ["vn-link/port_mapping", "common/port_mapping"] -lz4 = ["vn-link/lz4_compress", "common/lz4"] -zstd = ["vn-link/zstd_compress", "common/zstd"] -upnp = ["vn-link/upnp", "common/upnp"] -ws = ["vn-link/ws", "common/ws"] -wss = ["vn-link/wss", "common/wss"] -log = ["common/log"] -command = ["common/command"] -file_config = ["common/file_config"] - diff --git a/vn-link-cli/README.md b/vn-link-cli/README.md deleted file mode 100644 index 3c37358..0000000 --- a/vn-link-cli/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# 端口映射模式 - -## 一、特点 - -1. 不需要tap/tun虚拟网卡 -2. 不需要管理员/root权限 -3. 不改变本地路由 -4. 使用端口映射来访问目标服务 - -## 二、作用 - -和vnt互补,能简单快速构建网络,外部依赖更少 - -## 三、使用方式 - -和vnt的使用方式一样,只是多了"--vnt-mapping"这个参数 - -### vn-link作为被访问端,不需要额外配置vnt-mapping - -### vn-link访问vnt或者vn-link,需要加vnt-mapping - -例如: - -设备A 运行vnt(虚拟IP 10.26.0.A),设备B 运行vn-link(虚拟IP 10.26.0.B)。 - -如果要用B访问A上的tcp 80端口,则在设备B上需要加--vnt-mapping "tcp:port1-10.26.0.A:80" - -这个参数的作用是将B上的***本地端口port1***转发到设备A的地址10.26.0.A: -80,此时在设备B上可以访问本地port1端口从而间接访问10.26.0.A:80 - -## 四、vn-link的子网代理 - -vn-link也支持点对网参数。 还是接着上面的例子 - -假设 设备C在设备A的子网下,C的子网IP为192.168.1.C,A的子网IP为192.168.1.A,要在设备B上访问C - -则在B上加这些参数 - -- --vnt-mapping "tcp:port2-192.168.1.C:80" (将本地port2端口映射到C的80端口) -- -i 192.168.1.0/24,10.26.0.A (将目标192.168.1.0/24的数据发送到10.26.0.A,也就是A节点) - -在A上加参数 - -- -o 0.0.0.0/0 (允许所有流量转发) - -***再次说明,vn-link作为被访问端时和vnt使用方式一致,vn-link作为访问端时需要加--vnt-mapping映射端口*** - -***vn-link是基于端口映射的使用模式,不会改变本地路由*** - -## 五、参数介绍 - ---vnt-mapping支持udp/tcp,例如 --vnt-mapping "tcp:port1-remoteIp:remotePort" - -- 第一部分为协议,支持使用udp/tcp -- 第二部分是本地端口,注意不要和本地服务的端口冲突 -- 第三部分是目标机器的地址,一般是目标虚拟IP地址,如果配置了点对网参数(-i和-o)则也可以是目标子网地址 - - - - - diff --git a/vn-link-cli/build.rs b/vn-link-cli/build.rs deleted file mode 100644 index 4d63de1..0000000 --- a/vn-link-cli/build.rs +++ /dev/null @@ -1,5 +0,0 @@ -fn main() { - // 配置 thunk-rs 来链接 Windows 7 兼容库,并自动设置链接参数 - #[cfg(target_os = "windows")] - thunk::thunk(); -} diff --git a/vn-link-cli/src/main.rs b/vn-link-cli/src/main.rs deleted file mode 100644 index e374033..0000000 --- a/vn-link-cli/src/main.rs +++ /dev/null @@ -1,89 +0,0 @@ -use common::callback; -use vn_link::config::VnLinkConfig; -use vn_link::vnt::core::Config; - -fn main() { - let (config, vnt_link_config, cmd) = match common::cli::parse_args_config() { - Ok(rs) => { - if let Some(rs) = rs { - rs - } else { - return; - } - } - Err(e) => { - log::error!( - "parse error={:?} cmd={:?}", - e, - std::env::args().collect::>() - ); - println!("Error {:?}", e); - return; - } - }; - let vnt_link_config = VnLinkConfig::new(vn_link::config::convert(vnt_link_config).unwrap()); - main0(config, vnt_link_config, cmd) -} - -#[tokio::main] -async fn main0(config: Config, vn_link_config: VnLinkConfig, _show_cmd: bool) { - #[cfg(feature = "port_mapping")] - for (is_tcp, addr, dest) in config.port_mapping_list.iter() { - if *is_tcp { - println!("TCP port mapping {}->{}", addr, dest) - } else { - println!("UDP port mapping {}->{}", addr, dest) - } - } - for x in &vn_link_config.mapping { - if x.protocol.is_tcp() { - println!("TCP vnt addr mapping 127.0.0.1:{}->{}", x.src_port, x.dest) - } else { - println!("UDP vnt addr mapping 127.0.0.1:{}->{}", x.src_port, x.dest) - } - } - - let vnt_util = match vn_link::VnLink::new(config, vn_link_config, callback::VntHandler {}).await - { - Ok(vnt) => vnt, - Err(e) => { - println!("error: {:?}", e); - std::process::exit(1); - } - }; - - #[cfg(feature = "command")] - { - let vnt_c = vnt_util.as_vnt().clone(); - std::thread::Builder::new() - .name("CommandServer".into()) - .spawn(move || { - if let Err(e) = common::command::server::CommandServer::new().start(vnt_c) { - log::warn!("cmd:{:?}", e); - } - }) - .expect("CommandServer"); - let vnt_c = vnt_util.as_vnt(); - if _show_cmd { - use tokio::io::AsyncBufReadExt; - let mut cmd = String::new(); - let mut reader = tokio::io::BufReader::new(tokio::io::stdin()); - loop { - cmd.clear(); - println!("======== input:list,info,route,all,stop,chart_a,chart_b[:ip] ========"); - match reader.read_line(&mut cmd).await { - Ok(len) => { - if !common::command::command_str(&cmd[..len], vnt_c) { - break; - } - } - Err(e) => { - println!("input err:{}", e); - break; - } - } - } - } - } - vnt_util.wait().await -} diff --git a/vn-link/Cargo.toml b/vn-link/Cargo.toml deleted file mode 100644 index 07e2e5d..0000000 --- a/vn-link/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "vn-link" -version = "1.2.16" -edition = "2021" - -[dependencies] -lwip-rs = { git = "https://github.com/vnt-dev/lwip-rs" } -vnt = { path = "../vnt", package = "vnt", default-features = false } -log = "0.4.17" -anyhow = "1.0.82" -parking_lot = "0.12.1" - -tokio = { version = "1.37.0", features = ["full"] } -crossbeam-utils = "0.8" - -[features] -default = ["server_encrypt", "aes_gcm", "aes_cbc", "aes_ecb", "sm4_cbc", "chacha20_poly1305", "port_mapping", "lz4_compress"] -openssl = ["vnt/openssl"] -openssl-vendored = ["vnt/openssl-vendored"] -ring-cipher = ["vnt/ring-cipher"] -aes_cbc = ["vnt/aes_cbc"] -aes_ecb = ["vnt/aes_ecb"] -sm4_cbc = ["vnt/sm4_cbc"] -aes_gcm = ["vnt/aes_gcm"] -chacha20_poly1305 = ["vnt/chacha20_poly1305"] -server_encrypt = ["vnt/server_encrypt"] -port_mapping = ["vnt/port_mapping"] -lz4_compress = ["vnt/lz4_compress"] -zstd_compress = ["vnt/zstd_compress"] -upnp = ["vnt/upnp"] -ws = ["vnt/ws"] -wss = ["vnt/wss"] \ No newline at end of file diff --git a/vn-link/src/config.rs b/vn-link/src/config.rs deleted file mode 100644 index 58e2c6a..0000000 --- a/vn-link/src/config.rs +++ /dev/null @@ -1,85 +0,0 @@ -use anyhow::Context; -use std::net::SocketAddr; -use std::str::FromStr; - -#[derive(Clone, Debug)] -pub struct VnLinkConfig { - pub mapping: Vec, -} - -impl VnLinkConfig { - pub fn new(mapping: Vec) -> Self { - Self { mapping } - } -} - -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub enum LinkProtocol { - Tcp, - Udp, -} - -impl LinkProtocol { - pub fn is_tcp(&self) -> bool { - self == &LinkProtocol::Tcp - } -} - -#[derive(Copy, Clone, Debug)] -pub struct LinkItem { - pub protocol: LinkProtocol, - pub src_port: u16, - pub dest: SocketAddr, -} - -impl LinkItem { - pub fn new(protocol: LinkProtocol, src_port: u16, dest: SocketAddr) -> Self { - Self { - protocol, - src_port, - dest, - } - } -} - -pub fn convert(vec: Vec) -> anyhow::Result> { - let mut rs = Vec::with_capacity(vec.len()); - for x in vec { - let string = x.trim().to_lowercase(); - if let Some(udp_mapping) = string.strip_prefix("udp:") { - let mut split = udp_mapping.split("-"); - let bind_port = split - .next() - .with_context(|| format!("vnt-mapping error {:?},eg: udp:80-10.26.0.10:8080", x))?; - let bind_port = u16::from_str(bind_port) - .with_context(|| format!("udp_mapping error {}", bind_port))?; - let dest = split - .next() - .with_context(|| format!("vnt-mapping error {:?},eg: udp:80-10.26.0.10:8080", x))?; - let dest_addr = SocketAddr::from_str(dest) - .with_context(|| format!("udp_mapping error {}", dest))?; - rs.push(LinkItem::new(LinkProtocol::Udp, bind_port, dest_addr)); - continue; - } - if let Some(tcp_mapping) = string.strip_prefix("tcp:") { - let mut split = tcp_mapping.split("-"); - let bind_port = split - .next() - .with_context(|| format!("vnt-mapping error {:?},eg: tcp:80-10.26.0.10:8080", x))?; - let bind_port = u16::from_str(bind_port) - .with_context(|| format!("tcp_mapping error {}", bind_port))?; - let dest = split - .next() - .with_context(|| format!("vnt-mapping error {:?},eg: tcp:80-10.26.0.10:8080", x))?; - let dest_addr = SocketAddr::from_str(dest) - .with_context(|| format!("tcp_mapping error {}", dest))?; - rs.push(LinkItem::new(LinkProtocol::Tcp, bind_port, dest_addr)); - continue; - } - Err(anyhow::anyhow!( - "vnt-mapping error {:?},eg: tcp:80-10.26.0.10:8080", - x - ))?; - } - Ok(rs) -} diff --git a/vn-link/src/in_mapping/mod.rs b/vn-link/src/in_mapping/mod.rs deleted file mode 100644 index f7a6a76..0000000 --- a/vn-link/src/in_mapping/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod tcp; -pub mod udp; diff --git a/vn-link/src/in_mapping/tcp.rs b/vn-link/src/in_mapping/tcp.rs deleted file mode 100644 index d841469..0000000 --- a/vn-link/src/in_mapping/tcp.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::out_mapping::tcp::tcp_copy; -use crossbeam_utils::atomic::AtomicCell; -use lwip_rs::tcp_stream::TcpStream as LwIpTcpStream; -use std::net::{IpAddr, SocketAddr}; -use std::sync::Arc; -use std::time::Duration; -use tokio::net::TcpListener; -use vnt::handle::CurrentDeviceInfo; - -pub async fn tcp_mapping_listen( - tcp_listener: TcpListener, - current_device: Arc>, - dest: SocketAddr, -) { - loop { - let (stream, addr) = match tcp_listener.accept().await { - Ok((stream, addr)) => (stream, addr), - Err(e) => { - log::warn!("tcp_mapping_listen {:?} dest {}", e, dest); - continue; - } - }; - let current_info = current_device.load(); - if current_info.virtual_ip.is_unspecified() { - continue; - } - if let IpAddr::V4(ip) = dest.ip() { - if ip == current_info.virtual_ip { - //防止用错参数的 - log::warn!("目的地址不能是本地虚拟ip tcp->{}", dest); - continue; - } - } - let src = SocketAddr::new(IpAddr::V4(current_info.virtual_ip), addr.port()); - tokio::spawn(async move { - match LwIpTcpStream::connect(src, dest, Duration::from_secs(5)).await { - Ok(lw_tcp) => { - tcp_copy(lw_tcp, stream); - } - Err(e) => { - log::warn!("{} {}->{} {}", addr, src, dest, e); - } - }; - }); - } -} diff --git a/vn-link/src/in_mapping/udp.rs b/vn-link/src/in_mapping/udp.rs deleted file mode 100644 index b7ac232..0000000 --- a/vn-link/src/in_mapping/udp.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::collections::HashMap; -use std::net::{IpAddr, SocketAddr}; -use std::sync::Arc; -use std::time::Instant; - -use crossbeam_utils::atomic::AtomicCell; -use parking_lot::Mutex; -use tokio::net::UdpSocket; - -use lwip_rs::udp::UdpSocketWrite; -use vnt::handle::CurrentDeviceInfo; - -pub async fn udp_mapping_start( - udp: UdpSocket, - lwip_udp_write: UdpSocketWrite, - current_device: Arc>, - in_udp_map: &Arc< - Mutex< - HashMap< - (SocketAddr, SocketAddr), - (Arc, Option, Arc>), - >, - >, - >, - - dest: SocketAddr, -) { - let udp = Arc::new(udp); - let mut buf = [0u8; 65536]; - loop { - let (len, addr) = match udp.recv_from(&mut buf).await { - Ok(rs) => rs, - Err(e) => { - log::warn!("recv_from {} {}", dest, e); - continue; - } - }; - let current_info = current_device.load(); - if current_info.virtual_ip.is_unspecified() { - continue; - } - if let IpAddr::V4(ip) = dest.ip() { - if ip == current_info.virtual_ip { - //防止用错参数的 - log::warn!("目的地址不能是本地虚拟ip udp->{}", dest); - continue; - } - } - let src = SocketAddr::new(IpAddr::V4(current_info.virtual_ip), addr.port()); - in_udp_map.lock().insert( - (dest, src), - ( - udp.clone(), - Some(addr), - Arc::new(AtomicCell::new(Instant::now())), - ), - ); - - if let Err(e) = lwip_udp_write.send(&buf[..len], &src, &dest) { - log::warn!("lwip_udp_write {}->{} {}", src, dest, e); - } - } -} diff --git a/vn-link/src/lib.rs b/vn-link/src/lib.rs deleted file mode 100644 index 990bdb4..0000000 --- a/vn-link/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod config; -mod in_mapping; -mod out_mapping; -mod vnt_link; - -pub use vnt; - -pub use vnt_link::*; diff --git a/vn-link/src/out_mapping/mod.rs b/vn-link/src/out_mapping/mod.rs deleted file mode 100644 index f7a6a76..0000000 --- a/vn-link/src/out_mapping/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod tcp; -pub mod udp; diff --git a/vn-link/src/out_mapping/tcp.rs b/vn-link/src/out_mapping/tcp.rs deleted file mode 100644 index 7f3e9f6..0000000 --- a/vn-link/src/out_mapping/tcp.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::net::{IpAddr, Ipv4Addr}; -use std::sync::Arc; - -use crossbeam_utils::atomic::AtomicCell; -use tokio::net::TcpStream; - -use lwip_rs::tcp_listener::TcpListener; -use lwip_rs::tcp_stream::TcpStream as LwIpTcpStream; -use vnt::handle::CurrentDeviceInfo; - -pub async fn tcp_mapping_listen( - mut tcp_listener: TcpListener, - current_device: Arc>, -) { - loop { - let stream = match tcp_listener.accept().await { - Ok(stream) => stream, - Err(e) => { - log::warn!("tcp_mapping_listen err {:?}", e); - break; - } - }; - let device_info = current_device.load(); - tokio::spawn(async move { - let dest = stream.dest_addr(); - let src = stream.src_addr(); - if let Err(e) = tcp_mapping_handle(stream, device_info).await { - log::warn!("tcp_mapping_handle {}->{} {:?}", src, dest, e) - } - }); - } -} - -async fn tcp_mapping_handle( - tcp_stream: LwIpTcpStream, - device_info: CurrentDeviceInfo, -) -> anyhow::Result<()> { - let mut dest = tcp_stream.dest_addr(); - // let src = tcp_stream.src_addr(); - if let IpAddr::V4(ip) = dest.ip() { - if ip.is_unspecified() - || ip.is_broadcast() - || ip.is_multicast() - || ip == device_info.virtual_ip - || ip == device_info.broadcast_ip - { - //是自己 - dest.set_ip(IpAddr::V4(Ipv4Addr::LOCALHOST)); - } - } - let peer_stream = TcpStream::connect(dest).await?; - if dest.port() == peer_stream.local_addr()?.port() { - return Err(anyhow::anyhow!("tcp port loop")); - } - tcp_copy(tcp_stream, peer_stream); - Ok(()) -} - -pub(crate) fn tcp_copy(lw_tcp: LwIpTcpStream, tokio_tcp: TcpStream) { - let (mut write, mut read) = lw_tcp.into_split(); - let (mut peer_read, mut peer_write) = tokio_tcp.into_split(); - tokio::spawn(async move { tokio::io::copy(&mut read, &mut peer_write).await }); - tokio::spawn(async move { tokio::io::copy(&mut peer_read, &mut write).await }); -} diff --git a/vn-link/src/out_mapping/udp.rs b/vn-link/src/out_mapping/udp.rs deleted file mode 100644 index 04c030c..0000000 --- a/vn-link/src/out_mapping/udp.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crossbeam_utils::atomic::AtomicCell; -use lwip_rs::udp::{UdpSocketRead, UdpSocketWrite}; -use parking_lot::Mutex; -use std::collections::HashMap; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::net::UdpSocket; -use vnt::handle::CurrentDeviceInfo; - -pub async fn udp_mapping_start( - lwip_udp_write: UdpSocketWrite, - mut lwip_udp_read: UdpSocketRead, - current_device: Arc>, - in_udp_map: Arc< - Mutex< - HashMap< - (SocketAddr, SocketAddr), - (Arc, Option, Arc>), - >, - >, - >, -) { - loop { - let (buf, src, dest) = match lwip_udp_read.recv().await { - Ok(rs) => rs, - Err(e) => { - log::warn!("udp_mapping err {:?}", e); - break; - } - }; - if let Err(e) = handle( - ¤t_device, - &lwip_udp_write, - &in_udp_map, - buf, - src, - dest, - ) - .await - { - log::warn!("udp_mapping err {}->{} {:?}", src, dest, e) - } - } -} - -async fn handle( - current_device: &AtomicCell, - lwip_udp_write: &UdpSocketWrite, - map: &Arc< - Mutex< - HashMap< - (SocketAddr, SocketAddr), - (Arc, Option, Arc>), - >, - >, - >, - buf: Vec, - src: SocketAddr, - dest: SocketAddr, -) -> anyhow::Result<()> { - let option = map.lock().get(&(src, dest)).cloned(); - - if let Some((dest_udp, addr, time)) = option { - time.store(Instant::now()); - if let Some(addr) = addr { - dest_udp.send_to(&buf, addr).await?; - } else { - dest_udp.send(&buf).await?; - } - } else { - let mut real_dest = dest; - let peer_udp_socket = match UdpSocket::bind(format!("0.0.0.0:{}", src.port())).await { - Ok(udp) => udp, - Err(_) => UdpSocket::bind("0.0.0.0:0").await?, - }; - if let IpAddr::V4(ip) = dest.ip() { - let device_info = current_device.load(); - if ip.is_unspecified() - || ip.is_broadcast() - || ip.is_multicast() - || ip == device_info.virtual_ip - || ip == device_info.broadcast_ip - { - //是自己 - real_dest.set_ip(IpAddr::V4(Ipv4Addr::LOCALHOST)); - } - } - peer_udp_socket.connect(real_dest).await?; - peer_udp_socket.send(&buf).await?; - let peer_udp_socket = Arc::new(peer_udp_socket); - let time = Arc::new(AtomicCell::new(Instant::now())); - let map = map.clone(); - map.lock() - .insert((src, dest), (peer_udp_socket.clone(), None, time.clone())); - let lwip_udp_write = lwip_udp_write.clone(); - tokio::spawn(async move { - peer_udp_handle(peer_udp_socket, lwip_udp_write, src, dest, time).await; - map.lock().remove(&(src, dest)); - }); - } - Ok(()) -} - -async fn peer_udp_handle( - peer_udp_socket: Arc, - lwip_udp_write: UdpSocketWrite, - src: SocketAddr, - dest: SocketAddr, - time: Arc>, -) { - let mut buf = [0u8; 65536]; - loop { - match tokio::time::timeout(Duration::from_secs(600), peer_udp_socket.recv(&mut buf)).await { - Ok(rs) => match rs { - Ok(len) => match lwip_udp_write.send(&buf[..len], &dest, &src) { - Ok(_) => {} - Err(e) => { - log::warn!("udp proxy {}->{} {:?}", dest, src, e); - break; - } - }, - Err(e) => { - log::warn!("udp proxy {}->{} {:?}", dest, src, e); - break; - } - }, - Err(_) => { - if time.load().elapsed() > Duration::from_secs(580) { - //超时关闭 - log::warn!("udp proxy timeout {}->{}", dest, src,); - break; - } - } - } - } -} diff --git a/vn-link/src/vnt_link/mod.rs b/vn-link/src/vnt_link/mod.rs deleted file mode 100644 index c009efd..0000000 --- a/vn-link/src/vnt_link/mod.rs +++ /dev/null @@ -1,217 +0,0 @@ -use std::collections::HashMap; -use std::net::SocketAddr; -use std::sync::Arc; -use std::time::Instant; - -use anyhow::Context; -use crossbeam_utils::atomic::AtomicCell; -use parking_lot::Mutex; -use tokio::net::{TcpListener, UdpSocket}; -use tokio::sync::watch::{channel, Sender}; - -use lwip_rs::stack::{NetStack, NetStackWrite}; -use lwip_rs::tcp_listener::TcpListener as LwIPTcpListener; -use lwip_rs::udp::{UdpSocket as LwIpUdpSocket, UdpSocketWrite}; -use vnt::channel::BUFFER_SIZE; -use vnt::core::{Config, Vnt}; -use vnt::packet::ip::ipv4::packet::IpV4Packet; -use vnt::protocol::HEAD_LEN; -use vnt::vnt_device::DeviceWrite; -use vnt::VntCallback; - -use crate::config::{LinkItem, LinkProtocol, VnLinkConfig}; -use crate::{in_mapping, out_mapping}; - -pub struct VnLink { - vnt: Vnt, - in_udp_map: Arc< - Mutex< - HashMap< - (SocketAddr, SocketAddr), - (Arc, Option, Arc>), - >, - >, - >, - lwip_udp_write: UdpSocketWrite, - shutdown_tx: Sender, -} - -impl VnLink { - pub async fn new( - vnt_config: Config, - vn_link_config: VnLinkConfig, - callback: Call, - ) -> anyhow::Result { - let stack = NetStack::new(HEAD_LEN, 1024, vnt_config.mtu.unwrap_or(1420) as u16).await; - let udp = LwIpUdpSocket::new()?; - let tcp_listener = LwIPTcpListener::new()?; - let (shutdown_tx, shutdown_rx) = channel(false); - let (net_stack_write, mut net_stack_read) = stack.into_split(); - let vnt = Vnt::new_device(vnt_config, callback, VntDevice { net_stack_write })?; - let shutdown_tx_ = shutdown_tx.clone(); - let w = vnt.add_stop_listener("vnt-link".into(), move || { - let _ = shutdown_tx_.send(true); - })?; - let ip_sender = vnt.ipv4_packet_sender().unwrap(); - let mut shutdown_rx_ = shutdown_rx.clone(); - tokio::spawn(async move { - let mut extend = [0; BUFFER_SIZE]; - loop { - tokio::select! { - _ = shutdown_rx_.changed() => { - break; - } - rs = net_stack_read.recv_ip() => { - match rs{ - Ok((mut buf, start_index, len)) => { - let ipv4_packet = if let Ok(packet) = - IpV4Packet::new(&buf[start_index..len]) - { - packet - } else { - continue; - }; - let destination_ip = ipv4_packet.destination_ip(); - let source_ip = ipv4_packet.source_ip(); - - if let Err(e) = ip_sender.send_ip(&mut buf, len, &mut extend, destination_ip) { - log::warn!("{}->{},{}", source_ip, destination_ip, e); - } - }, - Err(e) => { - log::error!("net_stack_read {:?}", e); - break; - } - }; - } - } - } - w.stop_all(); - }); - - let (lwip_udp_write, lwip_udp_read) = udp.into_split(); - let in_udp_map: Arc< - Mutex< - HashMap< - (SocketAddr, SocketAddr), - (Arc, Option, Arc>), - >, - >, - > = Arc::new(Mutex::new(HashMap::new())); - - let current_device_info = vnt.current_device_info(); - let in_udp_map_ = in_udp_map.clone(); - let lwip_udp_write_ = lwip_udp_write.clone(); - let vnt_ = vnt.clone(); - let mut shutdown_rx_ = shutdown_rx.clone(); - tokio::spawn(async move { - tokio::select! { - _ = shutdown_rx_.changed() => {} - _ = out_mapping::udp::udp_mapping_start( - lwip_udp_write_, - lwip_udp_read, - current_device_info, - in_udp_map_, - ) => {} - } - - vnt_.stop(); - }); - let current_device_info = vnt.current_device_info(); - let vnt_ = vnt.clone(); - let mut shutdown_rx_ = shutdown_rx.clone(); - - tokio::spawn(async move { - tokio::select! { - _ = shutdown_rx_.changed() => {} - _ = out_mapping::tcp::tcp_mapping_listen(tcp_listener, current_device_info) => {} - } - vnt_.stop(); - }); - let link = Self { - vnt, - in_udp_map, - lwip_udp_write, - shutdown_tx, - }; - link.add_mapping(vn_link_config.mapping).await?; - Ok(link) - } - pub async fn add_mapping(&self, mapping: Vec) -> anyhow::Result<()> { - for item in mapping { - let current_device_info = self.vnt.current_device_info(); - if item.dest.ip().is_unspecified() { - Err(anyhow::anyhow!("dest_address {:?} is_unspecified", item))? - } - let mut shutdown_rx_ = self.shutdown_tx.subscribe(); - if *shutdown_rx_.borrow() { - Err(anyhow::anyhow!("mapping stop"))? - } - if item.protocol == LinkProtocol::Udp { - let lwip_udp_write = self.lwip_udp_write.clone(); - let in_udp_map = self.in_udp_map.clone(); - //只能本机访问,不然不同IP的相同来源端口会有问题 - let udp = UdpSocket::bind(format!("127.0.0.1:{}", item.src_port)) - .await - .with_context(|| format!("udp bind failed {}", item.src_port))?; - tokio::spawn(async move { - tokio::select! { - _ = shutdown_rx_.changed() => {} - _ = in_mapping::udp::udp_mapping_start( - udp, - lwip_udp_write, - current_device_info, - &in_udp_map, - item.dest, - ) => {} - } - }); - } else { - let listener = TcpListener::bind(format!("127.0.0.1:{}", item.src_port)) - .await - .with_context(|| format!("tcp bind failed {}", item.src_port))?; - tokio::spawn(async move { - tokio::select! { - _ = shutdown_rx_.changed() => {} - _ = in_mapping::tcp::tcp_mapping_listen( - listener, - current_device_info, - item.dest, - ) => {} - } - }); - } - } - Ok(()) - } - pub fn stop(&self) { - self.as_vnt().stop() - } - pub async fn wait(&self) { - loop { - let mut receiver = self.shutdown_tx.subscribe(); - if *receiver.borrow() { - return; - } - if receiver.changed().await.is_err() { - return; - } - } - } - - pub fn as_vnt(&self) -> &Vnt { - &self.vnt - } -} - -#[derive(Clone)] -pub struct VntDevice { - net_stack_write: NetStackWrite, -} - -impl DeviceWrite for VntDevice { - fn write(&self, buf: &[u8]) -> std::io::Result { - self.net_stack_write.send_ip(buf)?; - Ok(buf.len()) - } -} diff --git a/vnt-cli/Cargo.toml b/vnt-cli/Cargo.toml deleted file mode 100644 index 4de7503..0000000 --- a/vnt-cli/Cargo.toml +++ /dev/null @@ -1,51 +0,0 @@ -[package] -name = "vnt-cli" -version = "1.2.16" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -vnt = { path = "../vnt", package = "vnt", default-features = false, features = ["integrated_tun"] } -common = { path = "../common", default-features = false, features = ["integrated_tun"] } -log = "0.4.17" -anyhow = "1.0.82" -console = "0.15.2" - -[target.'cfg(any(target_os = "linux",target_os = "macos"))'.dependencies] -sudo = "0.6.0" -signal-hook = "0.3.17" - -[target.'cfg(target_os = "windows")'.dependencies] -winapi = { version = "0.3.9", features = ["handleapi", "processthreadsapi", "winnt", "securitybaseapi", "impl-default"] } - - -[features] -default = ["default-feature"] -default-feature = ["server_encrypt", "aes_gcm", "aes_cbc", "aes_ecb", "sm4_cbc", "chacha20_poly1305", "ip_proxy", "port_mapping", "log", "command", "file_config", "lz4", "ws"] - -openssl = ["vnt/openssl", "common/openssl"] -openssl-vendored = ["vnt/openssl-vendored", "common/openssl-vendored"] -ring-cipher = ["vnt/ring-cipher", "common/ring-cipher"] -aes_cbc = ["vnt/aes_cbc", "common/aes_cbc"] -aes_ecb = ["vnt/aes_ecb", "common/aes_ecb"] -sm4_cbc = ["vnt/sm4_cbc", "common/sm4_cbc"] -aes_gcm = ["vnt/aes_gcm", "common/aes_gcm"] -chacha20_poly1305 = ["vnt/chacha20_poly1305", "common/chacha20_poly1305"] -server_encrypt = ["vnt/server_encrypt", "common/server_encrypt"] -port_mapping = ["vnt/port_mapping", "common/port_mapping"] -lz4 = ["vnt/lz4_compress", "common/lz4"] -zstd = ["vnt/zstd_compress", "common/zstd"] -ip_proxy = ["vnt/ip_proxy", "common/ip_proxy"] -upnp = ["vnt/upnp", "common/upnp"] -ws = ["vnt/ws", "common/ws"] -wss = ["vnt/wss", "common/wss"] -log = ["common/log"] -command = ["common/command"] -file_config = ["common/file_config"] -[build-dependencies] -rand = "0.8.5" -chrono = "0.4.23" - -[target.'cfg(windows)'.build-dependencies] -thunk-rs = { version = "0.3.3", features = ["win7"] } diff --git a/vnt-cli/README.md b/vnt-cli/README.md deleted file mode 100644 index d6b74ee..0000000 --- a/vnt-cli/README.md +++ /dev/null @@ -1,256 +0,0 @@ -## 模块介绍 - -体积小,可以在服务器、路由器等环境使用 - -## 详细参数说明 - -### -k `` - -一个虚拟局域网的标识,在同一服务器下,相同token的设备会组建一个局域网 - -### -n `` - -设备名称,方便区分不同设备 - -### -d `` - -设备id,每台设备的唯一标识,注意不要重复 - -### -c - -关闭控制台交互式命令,后台运行时可以加此参数 - -### -s `` - -注册和中继服务器地址,注册和转发数据,以'TXT:'开头表示解析TXT记录,TXT记录内容必须是'host:port'形式的服务器地址 - -### -e `` - -使用stun服务探测客户端NAT类型,不同类型有不同的打洞策略 - -### -a - -加了此参数表示使用tap网卡,默认使用tun网卡,tun网卡效率更高 - -注意:仅在windows上支持使用tap,用于兼容低版本windows系统(低版本windows不支持wintun) - -使用tap模式需要手动创建tap网卡,使用--nic参数指定已经创建好的tap网卡名称 - -### --nic `` - -指定虚拟网卡名称,默认tun模式使用vnt-tun,tap模式使用vnt-tap - -### -i ``、-o `` - -配置点对网(IP代理)时使用,例如A(虚拟ip:10.26.0.2)通过B(虚拟ip:10.26.0.3,本地出口ip:192.168.0.10)访问C( -目标网段192.168.0.0/24), - -则在A配置 **'-i 192.168.0.0/24,10.26.0.3'** ,表示将192.168.0.0/24网段的数据都转发到10.26.0.3节点 - -在B配置 **'-o 192.168.0.0/24'** ,表示允许将数据转发到 192.168.0.0/24 ,允许转发所有网段可以使用 **'-o 0.0.0.0/0'** - --i和-o参数均可使用多次,来指定不同网段,例如 **'-o 192.168.1.0/24 -o 192.168.2.0/24'** -表示允许转发目标为192.168.1.0/24或192.168.2.0/24这两个网段的数据 - -### -w `` - -提升通信安全性,使用该密码生成的密钥对客户端数据进行加密,并且服务端无法解密(包括中继数据)。使用相同密码的客户端才能通信 - -| 密码位数 | 加密算法 | -|-------|------------| -| 小于8 | AES128-GCM | -| 大于等于8 | AES256-GCM | - -### -W - -开启和服务端通信的数据加密,采用rsa+aes256gcm加密客户端和服务端之间通信的数据,可以避免token泄漏、中间人攻击 - -注意: - -1. -w ``是用于客户端-客户端之间的加密,password不会传递到服务端,只添加这个参数不会加密客户端-服务端通信的数据 -2. -W 用于开启客户端-服务端之间的加密 - -### -u `` - -设置虚拟网卡的mtu值,大多数情况下使用默认值效率会更高,也可根据实际情况微调这个值,不加密默认为1450,加密默认为1410 - -### ~~--tcp~~ - -~~和服务端使用tcp通信。有些网络提供商对UDP限制比较大,这个时候可以选择使用TCP模式,提高稳定性。一般来说udp延迟和消耗更低~~ - -新版本使用 `-s tcp://`的形式使用tcp - -### --ip `` - -指定虚拟ip,指定的ip不能和其他设备重复,必须有效并且在服务端所属网段下,默认情况由服务端分配 - -### --par `` - -任务并行度(必须为正整数),默认值为1,该值表示处理网卡读写的任务数,组网设备数较多、处理延迟较大时可适当调大此值 - -### --model `` - -加密模式,可选值 -aes_gcm/aes_cbc/aes_ecb/sm4_cbc/chacha20_poly1305/chacha20/xor,默认使用aes_gcm,通常情况aes_gcm和chacha20_poly1305安全性高。 -各种加密模式的安全性和速度都不相同,请按需选取 - -特别说明:xor只是对数据进行简单异或,仅仅避免了明文传输,安全性很差,同时对性能影响也极小; - -| 密码位数 | model | 加密算法 | -|--------|-------------------|-------------------| -| `< 8` | aes_gcm | AES128-GCM | -| `>= 8` | aes_gcm | AES256-GCM | -| `< 8` | aes_cbc | AES128-CBC | -| `>= 8` | aes_cbc | AES256-CBC | -| `< 8` | aes_ecb | AES128-ECB | -| `>= 8` | aes_ecb | AES256-ECB | -| `> 0` | sm4_cbc | SM4-CBC | -| `> 0` | chacha20_poly1305 | ChaCha20-Poly1305 | -| `> 0` | chacha20 | ChaCha20 | -| `> 0` | xor | 简单异或混淆 | - -### --finger - -开启数据指纹校验,可增加安全性,如果服务端开启指纹校验,则客户端也必须开启,开启会损耗一部分性能 - -注意:默认情况下服务端不会对中转的数据做校验,如果要对中转的数据做校验,则需要客户端、服务端都开启此参数 - -### --punch `` - -取值ipv4/ipv6,选择只使用ipv4打洞或者只使用ipv6打洞,默认两者都会使用 - -### --ports `` - -指定本地监听的端口组,多个端口使用逗号分隔,多个端口可以分摊流量,增加并发、减缓流量限制,tcp会监听端口组的第一个端口,用于tcp直连 - -- 例1:‘--ports 12345,12346,12347’ 表示udp监听12345、12346、12347这三个端口,tcp监听12345端口 -- 例2:‘--ports 0,0’ 表示udp监听两个未使用的端口,tcp监听一个未使用的端口 - -### --cmd - -开启交互式命令,开启后可以直接在窗口下输入命令,如需后台运行请勿开启 - -### --first_latency - -优先使用低延迟通道,默认情况下优先使用p2p通道,某些情况下可能p2p比客户端中继延迟更高,可使用此参数进行优化传输 - -### --no-proxy - -关闭内置的ip代理,内置的代理较为简单,而且一般来说直接使用网卡NAT转发性能会更高, -有需要可以自行配置NAT转发,[可参考‘编译’小节中的NAT配置](https://github.com/vnt-dev/vnt#%E7%BC%96%E8%AF%91) - -### --dns `<223.5.5.5>` - -设置域名解析服务器地址,可以设置多个。如果使用TXT记录的域名,则dns默认使用223.5.5.5和114.114.114.114,端口省略值为53 - -当地址解析失败时,会依次尝试后面的dns,直到有A记录、AAAA记录(或TXT记录)的解析结果 - -### --mapping `` - -端口映射,可以设置多个映射地址,例如 '--mapping udp:0.0.0.0:80-10.26.0.10:80 --mapping tcp:0.0.0.0:80-10.26.0.11:81' -表示将本地udp 80端口的数据转发到10.26.0.10:80,将本地tcp 80端口的数据转发到10.26.0.11:81,转发的目的地址可以使用域名+端口 - -### --compressor `` - -启用压缩,默认仅支持lz4压缩,开启压缩后,如果数据包长度大于等于128,则会使用压缩,否则还是会按原数据发送 - -也支持开启zstd压缩,但是需要自行编译,编译时加入参数--features zstd - -如果宽度速度比较慢,可以考虑使用高级别的压缩 - -### -f `` - -指定配置文件 -配置文件采用yaml格式,可参考: - -```yaml -# 全部参数 -tap: false #是否使用tap 仅在windows上支持使用tap -token: xxx #组网token -device_id: xxx #当前设备id -name: windows 11 #当前设备名称 -server_address: ip:port #注册和中继服务器 -stun_server: #stun服务器 - - stun1.l.google.com:19302 - - stun2.l.google.com:19302 -in_ips: #代理ip入站 - - 192.168.1.0/24,10.26.0.3 -out_ips: #代理ip出站 - - 0.0.0.0/0 -password: xxx #密码 -mtu: 1420 #mtu -tcp: false #tcp模式 -ip: 10.26.0.2 #指定虚拟ip -use_channel: relay #relay:仅中继模式.p2p:仅直连模式 -server_encrypt: true #服务端加密 -parallel: 1 #任务并行度 -cipher_model: aes_gcm #客户端加密算法 -finger: false #关闭数据指纹 -punch_model: ipv4 #打洞模式,表示只使用ipv4地址打洞,默认会同时使用v6和v4 -ports: - - 0 #使用随机端口,tcp监听此端口 - - 0 -cmd: false #关闭控制台输入 -no_proxy: false #是否关闭内置代理,true为关闭 -first_latency: false #是否优先低延迟通道,默认为false,表示优先使用p2p通道 -device_name: vnt-tun #网卡名称 -packet_loss: 0 #指定丢包率 取值0~1之间的数 用于模拟弱网 -packet_delay: 0 #指定延迟 单位毫秒 用于模拟弱网 -dns: - - 223.5.5.5 # 首选dns - - 8.8.8.8 # 备选dns -mapping: - - udp:0.0.0.0:80-10.26.0.10:80 # 映射udp数据 - - tcp:0.0.0.0:80-10.26.0.10:81 # 映射tcp数据 - - tcp:0.0.0.0:82-localhost:83 # 映射tcp数据 -disable_stats: false # 为true表示关闭统计 -allow_wire_guard: false # 为true则表示允许接入wg -``` - -或者需要哪个配置就加哪个,当然token是必须的 - -```yaml -# 部分参数 -token: xxx #组网token -``` - -### --use-channel `` - -- relay:仅中继模式,会禁止打洞/p2p直连,只使用服务器转发 -- p2p:仅直连模式,会禁止网络数据从服务器/客户端转发,只会使用服务器转发控制包 - -### --packet-loss `<0>` - -模拟丢包,取值0~1之间的小数,程序会按设定的概率主动丢包。在模拟弱网环境时会有帮助。 - -### --packet-delay `<0>` - -模拟延迟,整数,单位毫秒(ms),程序会按设定的值延迟发包,可用于模拟弱网 - -### --disable-stats - -关闭流量统计 - -### --allow-wg - -允许接入WireGuard客户端,和wg混用时必须开启此参数 - -### --list - -在后台运行时,查看其他设备列表 - -### --all - -在后台运行时,查看其他设备完整信息 - -### --info - -在后台运行时,查看当前设备信息 - -### --route - -在后台运行时,查看数据转发路径 - -### --stop - -停止后台运行 diff --git a/vnt-cli/build.rs b/vnt-cli/build.rs deleted file mode 100644 index 4d63de1..0000000 --- a/vnt-cli/build.rs +++ /dev/null @@ -1,5 +0,0 @@ -fn main() { - // 配置 thunk-rs 来链接 Windows 7 兼容库,并自动设置链接参数 - #[cfg(target_os = "windows")] - thunk::thunk(); -} diff --git a/vnt-cli/src/main.rs b/vnt-cli/src/main.rs deleted file mode 100644 index 77809b9..0000000 --- a/vnt-cli/src/main.rs +++ /dev/null @@ -1,104 +0,0 @@ -use common::callback; -use console::style; -use vnt::core::{Config, Vnt}; -mod root_check; -fn main() { - let (config, _vnt_link_config, cmd) = match common::cli::parse_args_config() { - Ok(rs) => { - if let Some(rs) = rs { - rs - } else { - return; - } - } - Err(e) => { - log::error!( - "parse error={:?} cmd={:?}", - e, - std::env::args().collect::>() - ); - println!("{}", style(format!("Error {:?}", e)).red()); - return; - } - }; - main0(config, cmd) -} -fn main0(config: Config, _show_cmd: bool) { - if !root_check::is_app_elevated() { - println!("Please run it with administrator or root privileges"); - #[cfg(any(target_os = "linux", target_os = "macos"))] - sudo::escalate_if_needed().unwrap(); - return; - } - #[cfg(feature = "port_mapping")] - for (is_tcp, addr, dest) in config.port_mapping_list.iter() { - if *is_tcp { - println!("TCP port mapping {}->{}", addr, dest) - } else { - println!("UDP port mapping {}->{}", addr, dest) - } - } - let vnt_util = match Vnt::new(config, callback::VntHandler {}) { - Ok(vnt) => vnt, - Err(e) => { - log::error!("vnt create error {:?}", e); - println!("error: {:?}", e); - std::process::exit(1); - } - }; - #[cfg(any(target_os = "linux", target_os = "macos"))] - { - let vnt_c = vnt_util.clone(); - let mut signals = signal_hook::iterator::Signals::new(&[ - signal_hook::consts::SIGINT, - signal_hook::consts::SIGTERM, - ]) - .unwrap(); - let handle = signals.handle(); - std::thread::spawn(move || { - for sig in signals.forever() { - match sig { - signal_hook::consts::SIGINT | signal_hook::consts::SIGTERM => { - println!("Received SIGINT, {}", sig); - vnt_c.stop(); - handle.close(); - break; - } - _ => {} - } - } - }); - } - #[cfg(feature = "command")] - { - let vnt_c = vnt_util.clone(); - std::thread::Builder::new() - .name("CommandServer".into()) - .spawn(move || { - if let Err(e) = common::command::server::CommandServer::new().start(vnt_c) { - log::warn!("cmd:{:?}", e); - } - }) - .expect("CommandServer"); - if _show_cmd { - let mut cmd = String::new(); - loop { - cmd.clear(); - println!("======== input:list,info,route,all,stop,chart_a,chart_b[:ip] ========"); - match std::io::stdin().read_line(&mut cmd) { - Ok(len) => { - if !common::command::command_str(&cmd[..len], &vnt_util) { - break; - } - } - Err(e) => { - println!("input err:{}", e); - break; - } - } - } - } - } - - vnt_util.wait() -} diff --git a/vnt-cli/src/root_check/mod.rs b/vnt-cli/src/root_check/mod.rs deleted file mode 100644 index 3cf4643..0000000 --- a/vnt-cli/src/root_check/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[cfg(target_os = "windows")] -mod windows; - -#[cfg(target_os = "windows")] -pub use windows::is_app_elevated; - -#[cfg(any(target_os = "linux", target_os = "macos"))] -mod unix; - -#[cfg(any(target_os = "linux", target_os = "macos"))] -pub use unix::is_app_elevated; diff --git a/vnt-cli/src/root_check/unix.rs b/vnt-cli/src/root_check/unix.rs deleted file mode 100644 index 3f24a49..0000000 --- a/vnt-cli/src/root_check/unix.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub fn is_app_elevated() -> bool { - sudo::RunningAs::Root == sudo::check() -} diff --git a/vnt-cli/src/root_check/windows.rs b/vnt-cli/src/root_check/windows.rs deleted file mode 100644 index c3531bf..0000000 --- a/vnt-cli/src/root_check/windows.rs +++ /dev/null @@ -1,76 +0,0 @@ -/// 使用 https://github.com/spa5k/is_sudo/blob/main/src/window.rs -use std::io::Error; -use std::ptr; - -use winapi::um::handleapi::CloseHandle; -use winapi::um::processthreadsapi::{GetCurrentProcess, OpenProcessToken}; -use winapi::um::securitybaseapi::GetTokenInformation; -use winapi::um::winnt::{TokenElevation, HANDLE, TOKEN_ELEVATION, TOKEN_QUERY}; - -// Use std::io::Error::last_os_error for errors. -// NOTE: For this example I'm simple passing on the OS error. -// However, customising the error could provide more context - -/// Returns true if the current process has admin rights, otherwise false. -pub fn is_app_elevated() -> bool { - _is_app_elevated().unwrap_or(false) -} - -/// On success returns a bool indicating if the current process has admin rights. -/// Otherwise returns an OS error. -/// -/// This is unlikely to fail but if it does it's even more unlikely that you have admin permissions anyway. -/// Therefore the public function above simply eats the error and returns a bool. -fn _is_app_elevated() -> Result { - let token = QueryAccessToken::from_current_process()?; - token.is_elevated() -} - -/// A safe wrapper around querying Windows access tokens. -pub struct QueryAccessToken(HANDLE); - -impl QueryAccessToken { - pub fn from_current_process() -> Result { - unsafe { - let mut handle: HANDLE = ptr::null_mut(); - let result = OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut handle); - - if result != 0 { - Ok(Self(handle)) - } else { - Err(Error::last_os_error()) - } - } - } - - /// On success returns a bool indicating if the access token has elevated privilidges. - /// Otherwise returns an OS error. - pub fn is_elevated(&self) -> Result { - unsafe { - let mut elevation = TOKEN_ELEVATION::default(); - let size = std::mem::size_of::() as u32; - let mut ret_size = size; - // The weird looking repetition of `as *mut _` is casting the reference to a c_void pointer. - if GetTokenInformation( - self.0, - TokenElevation, - &mut elevation as *mut _ as *mut _, - size, - &mut ret_size, - ) != 0 - { - Ok(elevation.TokenIsElevated != 0) - } else { - Err(Error::last_os_error()) - } - } - } -} - -impl Drop for QueryAccessToken { - fn drop(&mut self) { - if !self.0.is_null() { - unsafe { CloseHandle(self.0) }; - } - } -} diff --git a/vnt-core/Cargo.toml b/vnt-core/Cargo.toml new file mode 100644 index 0000000..69db3bf --- /dev/null +++ b/vnt-core/Cargo.toml @@ -0,0 +1,58 @@ +[package] +name = "vnt-core" +version = "2.0.0" +edition = "2024" + +[dependencies] + + +tokio = { version = "1", features = ["full"] } +tokio-util = { version = "0.7", features = ["codec"] } +futures = "0.3" + +tun-rs = { version = "2", features = ["async", "async_framed"] } + +rust-p2p-core = { version="0.4" } +tcp_ip = { git = "https://github.com/rustp2p/tcp_ip" } + +anyhow = "1" +parking_lot = "0.12" + +quinn = { version = "0.11", default-features = false, features = ["rustls", "runtime-tokio"] } + +tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } +rustls = { version = "0.23", default-features = false, features = ["ring"] } +hex = "0.4" +sha2 = "0.10" +rustls-native-certs = "0.8.2" +log = "0.4" +bytes = "1.11.0" +rand = "0.9" +time = { version = "0.3", features = ["macros", "formatting", "local-offset"] } +pnet_packet = "0.35" +ring = "0.17.14" + +prost = "0.14" +tokio-tungstenite = "0.28.0" +tungstenite = "0.28.0" +uuid = { version = "1.18.1", features = ["v4"] } +ipnet = { version = "2.11", features = ["serde"] } +getifaddrs = "0.6.0" +dns-parser = "0.8" + +lz4_flex = "0.12.0" +reed-solomon-erasure = "6.0" + +serde = { version = "1.0.228", features = ["derive"] } +rcgen = "0.14.6" +machine-uid = "0.5.4" + +zerocopy = { version = "0.8.31", features = ["derive"] } +socket2 = { version = "0.6.1", features = ["all"] } + +[target.'cfg(target_os = "windows")'.dependencies] +winapi = { version = "0.3.9", features = ["winreg"] } +widestring = "1.2" + +[build-dependencies] +prost-build = "0.14" diff --git a/vnt-core/build.rs b/vnt-core/build.rs new file mode 100644 index 0000000..b31a0f5 --- /dev/null +++ b/vnt-core/build.rs @@ -0,0 +1,15 @@ +fn main() { + let mut config = prost_build::Config::new(); + config.protoc_arg("--experimental_allow_proto3_optional"); + config + .compile_protos( + &[ + "proto/control_message.proto", + "proto/rpc.proto", + "proto/client.proto", + "proto/fec.proto", + ], + &["proto"], + ) + .unwrap(); +} diff --git a/vnt-core/proto/client.proto b/vnt-core/proto/client.proto new file mode 100644 index 0000000..f5d9515 --- /dev/null +++ b/vnt-core/proto/client.proto @@ -0,0 +1,62 @@ +syntax = "proto3"; + +package protocol.client; + +message QuicProxyHandshake{ + oneof handshake{ + TcpProxyHandshake tcp = 1; + IpProxyHandshake ip = 2; + PortProxyHandshake tcp_port_mapping = 3; + PortProxyHandshake udp_port_mapping = 4; + } + +} + + +message TcpProxyHandshake{ + fixed32 src_ip = 1; + uint32 src_port = 2; + fixed32 dst_ip = 3; + uint32 dst_port = 4; +} +message PortProxyHandshake{ + string src_ip = 1; + uint32 src_port = 2; + string dst_host = 3; + uint32 dst_port = 4; +} + +message IpProxyHandshake{ + uint32 ip_next_header_protocol = 1; + fixed32 src_ip = 2; + fixed32 dst_ip = 3; +} + +enum NatType { + NAT_TYPE_CONE = 0; + NAT_TYPE_SYMMETRIC = 1; +} +message NatInfo { + + NatType nat_type = 1; + + repeated fixed32 public_ips = 2; + + repeated uint32 public_udp_ports = 3; + + uint32 public_port_range = 4; + + repeated fixed32 local_ipv4s = 5; + + optional bytes ipv6 = 6; + + repeated uint32 local_udp_ports = 7; + + uint32 local_tcp_port = 8; + + uint32 public_tcp_port = 9; +} + +message PunchInfo{ + NatInfo nat_info = 1; +} \ No newline at end of file diff --git a/vnt-core/proto/control_message.proto b/vnt-core/proto/control_message.proto new file mode 100644 index 0000000..a309e86 --- /dev/null +++ b/vnt-core/proto/control_message.proto @@ -0,0 +1,72 @@ +syntax = "proto3"; + +package protocol.control_message; + +enum RegistrationMode { + NORMAL = 0; + PRE_REGISTER = 1; +} + +message RegRequestMsg { + string network_code = 1; + string device_id = 2; + optional fixed32 ip = 3; + string name = 4; + string version = 5; + optional string key_sign = 6; + bool ip_variable = 7; + fixed32 server_id = 8; + RegistrationMode registration_mode = 9; +} + + +message RegResponseMsg { + fixed32 ip = 1; + uint32 prefix_len = 2; + fixed32 gateway = 3; + string server_version = 4; +} + +message ConfirmRegMsg { +} + +message ConfirmRegResponseMsg { + bool success = 1; +} + +message ErrorResponseMsg { + uint32 code = 1; + string message = 2; +} + +message RequestMessage{ + oneof request_payload{ + RegRequestMsg reg = 1; + ConfirmRegMsg confirm_reg = 2; + } +} +message ResponseMessage{ + oneof response_payload{ + RegResponseMsg reg = 1; + ErrorResponseMsg error = 2; + ConfirmRegResponseMsg confirm_reg = 3; + } +} + +message SelectiveBroadcast { + repeated fixed32 ips = 1; + bytes data = 2; +} + +message ClientSimpleInfo{ + fixed32 ip = 1; + bool online = 2; +} + +message ClientSimpleInfoList{ + uint64 data_version = 1; + repeated ClientSimpleInfo list = 2; + bool is_all = 3; + int64 time = 4; +} + diff --git a/vnt-core/proto/fec.proto b/vnt-core/proto/fec.proto new file mode 100644 index 0000000..a320f96 --- /dev/null +++ b/vnt-core/proto/fec.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package protocol.fec; + +message FecPacket { + uint64 group_id = 1; // FEC 组号(按目标IP分组) + uint32 packet_index = 2; // 包在组内的序号(0-based) + bytes payload = 3; + optional ParityData parity_data = 4; +} + + + +// 冗余包(携带FEC恢复所需的元数据) +message ParityData { + uint32 data_shards = 1; // 原始包数量 + uint32 parity_shards = 2; // 冗余包数量 +} diff --git a/vnt-core/proto/rpc.proto b/vnt-core/proto/rpc.proto new file mode 100644 index 0000000..e6077e7 --- /dev/null +++ b/vnt-core/proto/rpc.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; + +package protocol.rpc; + +message RpcMessageRequest{ + uint64 id = 1; + oneof rpc_req_payload{ + ClientListRequest client_list_req = 2; + } +} + +message ClientListRequest{ + +} + + + +message RpcMessageResponse{ + uint64 id = 1; + oneof rpc_res_payload{ + ClientListResponse client_list_res = 2; + } +} + +message ClientInfo{ + string name = 1; + string version = 2; + fixed32 ip = 3; + optional string key_sign = 4; + bool online = 5; + int64 last_connected_time = 6; + string id = 7; +} + +message ClientListResponse{ + repeated ClientInfo list = 1; +} \ No newline at end of file diff --git a/vnt-core/src/api/mod.rs b/vnt-core/src/api/mod.rs new file mode 100644 index 0000000..a551b9f --- /dev/null +++ b/vnt-core/src/api/mod.rs @@ -0,0 +1,94 @@ +use crate::context::config::Config; +use crate::context::{AppState, NetworkAddr, PacketLossInfo, ServerNodeInfo, TrafficInfo}; +use crate::protocol::control_message::ClientSimpleInfo; +use crate::tunnel_core::p2p::route_table::Route; +use crate::tunnel_core::server::rpc::ServerRPC; +use rust_p2p_core::nat::NatInfo; +use std::net::Ipv4Addr; + +#[derive(Clone)] +pub struct VntApi { + app_state: AppState, + server_rpc: ServerRPC, +} + +impl VntApi { + pub(crate) fn new(app_state: AppState, server_rpc: ServerRPC) -> Self { + Self { + app_state, + server_rpc, + } + } + pub fn server_rpc(&self) -> &ServerRPC { + &self.server_rpc + } + /// 获取启动配置 + pub fn get_config(&self) -> Option> { + self.app_state.get_config() + } + /// 获取所有客户端ip + pub fn client_ips(&self) -> Vec { + self.app_state.client_ips() + } + /// 判断目标IP是否直连 + pub fn is_direct(&self, ip: &Ipv4Addr) -> bool { + self.app_state.route_table.p2p_num(ip) > 0 + } + /// 查找路由 + pub fn find_route(&self, ip: &Ipv4Addr) -> Option { + self.app_state.route_table.get_route_by_id(ip).ok() + } + pub fn get_rtt(&self, ip: &Ipv4Addr) -> Option { + if let Some(route) = self.find_route(ip) { + Some(route.rtt()) + } else { + self.server_node_rtt(ip).map(|v| v * 2) + } + } + /// 获取所有路由 + pub fn route_table(&self) -> Vec<(Ipv4Addr, Vec)> { + self.app_state.route_table.route_table() + } + /// 获取服务器节点 + pub fn server_node_list(&self) -> Vec { + self.app_state.server_info_collection.server_node_list() + } + pub fn server_node_rtt(&self, ip: &Ipv4Addr) -> Option { + self.app_state.server_info_collection.get_server_rtt(ip) + } + /// 获取网络配置 + pub fn network(&self) -> Option { + self.app_state.get_network() + } + /// 获取当前的nat信息 + pub fn nat_info(&self) -> Option { + self.app_state.get_nat_info() + } + pub fn peer_nat_info(&self, ip: &Ipv4Addr) -> Option { + self.app_state.get_peer_info(ip).and_then(|v| v.nat_info) + } + pub fn packet_loss_info(&self, ip: &Ipv4Addr) -> Option { + self.app_state.packet_loss_stats.get_loss_info(ip) + } + pub fn all_packet_loss_info(&self) -> Vec { + self.app_state.packet_loss_stats.get_all_loss_info() + } + pub fn reset_packet_loss(&self, ip: &Ipv4Addr) { + self.app_state.packet_loss_stats.reset(ip) + } + pub fn reset_all_packet_loss(&self) { + self.app_state.packet_loss_stats.reset_all() + } + pub fn traffic_info(&self, ip: &Ipv4Addr) -> Option { + self.app_state.traffic_stats.get_traffic_info(ip) + } + pub fn all_traffic_info(&self) -> Vec { + self.app_state.traffic_stats.get_all_traffic_info() + } + pub fn reset_traffic(&self, ip: &Ipv4Addr) { + self.app_state.traffic_stats.reset(ip) + } + pub fn reset_all_traffic(&self) { + self.app_state.traffic_stats.reset_all() + } +} diff --git a/vnt-core/src/compression/lz4_compression.rs b/vnt-core/src/compression/lz4_compression.rs new file mode 100644 index 0000000..10f98e7 --- /dev/null +++ b/vnt-core/src/compression/lz4_compression.rs @@ -0,0 +1,132 @@ +use crate::protocol::ip_packet_protocol::{HEAD_LENGTH, NetPacket}; +use crate::protocol::transmission::TransmissionBytes; +use std::io; + +#[derive(Clone)] +pub struct LZ4Compression { + min_size: usize, // 只压缩大于此大小的数据包 +} + +impl LZ4Compression { + pub fn new() -> Self { + Self::with_min_size(256) + } + pub fn with_min_size(min_size: usize) -> Self { + Self { min_size } + } + + /// 压缩数据包,返回新的压缩后的数据包 + /// reserve: 尾部预留空间(用于后续加密等操作) + pub fn compress( + &self, + pkt: NetPacket, + reserve: usize, + ) -> io::Result> { + let payload = pkt.payload(); + if payload.len() < self.min_size { + return Ok(pkt); + } + let compressed = lz4_flex::compress_prepend_size(payload); + if compressed.len() >= payload.len() { + return Ok(pkt); + } + let total_len = HEAD_LENGTH + compressed.len(); + let mut buf = TransmissionBytes::zeroed_size(total_len, reserve); + + buf[..HEAD_LENGTH].copy_from_slice(&pkt.buffer()[..HEAD_LENGTH]); + buf[HEAD_LENGTH..total_len].copy_from_slice(&compressed); + + let mut packet = NetPacket::new(buf)?; + packet.set_compressed_flag(true); + Ok(packet) + } + + /// 解压缩数据包,返回新的解压后的数据包 + /// reserve: 尾部预留空间(用于后续加密等操作) + pub fn decompress( + &self, + pkt: NetPacket, + ) -> io::Result> { + if !pkt.is_compressed() { + return Ok(pkt); + } + let payload = pkt.payload(); + + let decompressed = lz4_flex::decompress_size_prepended(payload).map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("decompress failed: {}", e), + ) + })?; + + let total_len = HEAD_LENGTH + decompressed.len(); + let mut buf = TransmissionBytes::zeroed(total_len); + + buf[..HEAD_LENGTH].copy_from_slice(&pkt.buffer()[..HEAD_LENGTH]); + buf[HEAD_LENGTH..total_len].copy_from_slice(&decompressed); + + let mut packet = NetPacket::new(buf)?; + packet.set_compressed_flag(false); + Ok(packet) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::protocol::ip_packet_protocol::{HEAD_LENGTH, NetPacket}; + use crate::protocol::transmission::TransmissionBytes; + + fn make_packet(data: &[u8]) -> NetPacket { + let mut buf = TransmissionBytes::zeroed(HEAD_LENGTH + data.len()); + buf[HEAD_LENGTH..HEAD_LENGTH + data.len()].copy_from_slice(data); + NetPacket::new(buf).unwrap() + } + + #[test] + fn test_lz4_compress_and_decompress() { + let lz = LZ4Compression::with_min_size(10); + + // --- 构造原始包 --- + let payload = vec![1u8; 200]; + let original = make_packet(&payload); + + // --- 压缩 --- + let compressed = lz.compress(original, 0).unwrap(); + assert!(compressed.is_compressed()); + + // 压缩后的 payload 应变小 + assert!( + compressed.payload().len() < payload.len(), + "压缩后 payload 应该更小" + ); + + // --- 解压 --- + let decompressed = lz.decompress(compressed).unwrap(); + + // 标志应清除 + assert!(!decompressed.is_compressed()); + + // HEAD 不变 + assert_eq!( + &decompressed.buffer()[..HEAD_LENGTH], + &[0u8; HEAD_LENGTH][..], + "HEAD 必须保持不变" + ); + + // payload 必须等于原始 payload + assert_eq!(decompressed.payload(), &payload[..]); + } + + #[test] + fn test_no_compress_when_small() { + let lz = LZ4Compression::with_min_size(100); + + let pkt = make_packet(&[7; 20]); + let compressed = lz.compress(pkt, 0).unwrap(); + + assert!(!compressed.is_compressed(), "小包不应该被压缩"); + + assert_eq!(compressed.payload(), &[7; 20][..]); + } +} diff --git a/vnt-core/src/compression/mod.rs b/vnt-core/src/compression/mod.rs new file mode 100644 index 0000000..92c19b6 --- /dev/null +++ b/vnt-core/src/compression/mod.rs @@ -0,0 +1,45 @@ +use crate::compression::lz4_compression::LZ4Compression; +use crate::protocol::ip_packet_protocol::NetPacket; +use crate::protocol::transmission::TransmissionBytes; +use std::io; + +mod lz4_compression; + +#[derive(Clone)] +pub(crate) struct PacketCompression { + compression: Option, +} + +impl PacketCompression { + pub(crate) fn new(enabled: bool) -> Self { + Self { + compression: if enabled { + Some(LZ4Compression::new()) + } else { + None + }, + } + } + + pub(crate) fn compress( + &self, + pkt: NetPacket, + reserve: usize, + ) -> io::Result> { + if let Some(compression) = self.compression.as_ref() { + return compression.compress(pkt, reserve); + } + + Ok(pkt) + } + + pub(crate) fn decompress( + &self, + pkt: NetPacket, + ) -> io::Result> { + if let Some(compression) = self.compression.as_ref() { + return compression.decompress(pkt); + } + Ok(pkt) + } +} diff --git a/vnt-core/src/context/config.rs b/vnt-core/src/context/config.rs new file mode 100644 index 0000000..e0a1f99 --- /dev/null +++ b/vnt-core/src/context/config.rs @@ -0,0 +1,101 @@ +use crate::crypto::PacketCrypto; +use crate::nat::NetInput; +use crate::port_mapping::PortMapping; +use crate::tls::verifier::CertValidationMode; +use crate::tunnel_core::server::transport::config::{ConnectRegConfig, ProtocolAddress}; +use anyhow::bail; +use ipnet::Ipv4Net; +use std::collections::HashSet; +use std::net::Ipv4Addr; + +pub const MAX_NETWORK_CODE_LEN: usize = 32; +pub const MAX_DEVICE_ID_LEN: usize = 64; +pub const MAX_NAME_LEN: usize = 128; +pub const MAX_VERSION_LEN: usize = 32; +pub const MAX_MTU: u16 = 1500; + +#[derive(Debug, Clone, Default)] +pub struct Config { + pub server_addr: Vec, + pub cert_mode: CertValidationMode, + pub network_code: String, + pub device_id: String, + pub device_name: String, + pub tun_name: Option, + pub ip: Option, + pub password: Option, + pub no_punch: bool, + pub compress: bool, + pub rtx: bool, + pub fec: bool, + pub input: Vec, + pub output: Vec, + pub no_nat: bool, + pub no_tun: bool, + pub mtu: Option, + pub port_mapping: Vec, + pub allow_port_mapping: bool, + pub udp_stun: Vec, + pub tcp_stun: Vec, +} +impl Config { + pub fn check(&self) -> anyhow::Result<()> { + if self.server_addr.is_empty() { + bail!("服务器地址不能为空"); + } + if self.server_addr.len() > 1 { + let mut set = HashSet::new(); + + for a in self.server_addr.iter() { + if !set.insert(a.address.as_str()) { + bail!("服务器地址不能相同") + } + } + } + + if self.network_code.len() > MAX_NETWORK_CODE_LEN { + bail!( + "network_code length exceeds {} characters (current: {})", + MAX_NETWORK_CODE_LEN, + self.network_code.len() + ) + } + + if self.device_id.len() > MAX_DEVICE_ID_LEN { + bail!( + "device_id length exceeds {} characters (current: {})", + MAX_DEVICE_ID_LEN, + self.device_id.len() + ) + } + + if self.device_name.len() > MAX_NAME_LEN { + bail!( + "name length exceeds {} characters (current: {})", + MAX_NAME_LEN, + self.device_name.len() + ) + } + if let Some(mtu) = self.mtu + && mtu > MAX_MTU + { + bail!("MTU is too large (Maximum mtu: {MAX_MTU})",) + } + Ok(()) + } + pub fn key_sign(&self) -> Option { + self.password.as_ref().map(|p| PacketCrypto::key_sign(p)) + } + pub(crate) fn to_connect_config(&self, index: usize) -> ConnectRegConfig { + ConnectRegConfig { + server_addr: self.server_addr[index].clone(), + cert_mode: self.cert_mode.clone(), + network_code: self.network_code.clone(), + device_id: self.device_id.clone(), + device_name: self.device_name.clone(), + ip: self.ip, + key_sign: self.key_sign(), + ip_variable: self.ip.is_none(), + } + } +} diff --git a/vnt-core/src/context/mod.rs b/vnt-core/src/context/mod.rs new file mode 100644 index 0000000..a65cc47 --- /dev/null +++ b/vnt-core/src/context/mod.rs @@ -0,0 +1,632 @@ +use crate::context::config::Config; +use crate::context::nat::{MyNatInfo, PunchBackoff}; +use crate::nat::SubnetExternalRoute; +use crate::protocol::client_message::PunchInfo; +use crate::protocol::control_message::{ClientSimpleInfo, ClientSimpleInfoList}; +use crate::tunnel_core::p2p::route_table::RouteTable; +use crate::tunnel_core::server::transport::config::ProtocolAddress; +use ipnet::Ipv4Net; +use parking_lot::{Mutex, RwLock}; +use rust_p2p_core::nat::NatInfo; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::net::Ipv4Addr; +use std::sync::Arc; +#[derive(Default)] +struct PingStats { + sent: u64, + received: u64, +} + +#[derive(Default)] +struct TrafficCounter { + tx_bytes: u64, + rx_bytes: u64, +} + +#[derive(Clone, Default)] +pub struct TrafficStats { + inner: Arc>>>>, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TrafficInfo { + pub ip: Ipv4Addr, + pub tx_bytes: u64, + pub rx_bytes: u64, +} + +impl TrafficStats { + fn get_or_create(&self, ip: Ipv4Addr) -> Arc> { + { + let read = self.inner.read(); + if let Some(counter) = read.get(&ip) { + return counter.clone(); + } + } + let mut write = self.inner.write(); + write + .entry(ip) + .or_insert_with(|| Arc::new(Mutex::new(TrafficCounter::default()))) + .clone() + } + + pub fn record_tx(&self, ip: Ipv4Addr, bytes: u64) { + let counter = self.get_or_create(ip); + counter.lock().tx_bytes += bytes; + } + + pub fn record_rx(&self, ip: Ipv4Addr, bytes: u64) { + let counter = self.get_or_create(ip); + counter.lock().rx_bytes += bytes; + } + + pub fn get_traffic_info(&self, ip: &Ipv4Addr) -> Option { + let read = self.inner.read(); + read.get(ip).map(|counter| { + let guard = counter.lock(); + TrafficInfo { + ip: *ip, + tx_bytes: guard.tx_bytes, + rx_bytes: guard.rx_bytes, + } + }) + } + + pub fn get_all_traffic_info(&self) -> Vec { + let read = self.inner.read(); + read.iter() + .map(|(ip, counter)| { + let guard = counter.lock(); + TrafficInfo { + ip: *ip, + tx_bytes: guard.tx_bytes, + rx_bytes: guard.rx_bytes, + } + }) + .collect() + } + + pub fn reset(&self, ip: &Ipv4Addr) { + let read = self.inner.read(); + if let Some(counter) = read.get(ip) { + *counter.lock() = TrafficCounter::default(); + } + } + + pub fn reset_all(&self) { + let read = self.inner.read(); + for counter in read.values() { + *counter.lock() = TrafficCounter::default(); + } + } + + pub fn clear(&self) { + self.inner.write().clear(); + } +} + +#[derive(Clone, Default)] +pub struct PacketLossStats { + inner: Arc>>>>, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PacketLossInfo { + pub ip: Ipv4Addr, + pub sent: u64, + pub received: u64, + pub loss_rate: f64, +} + +impl PacketLossStats { + fn get_or_create(&self, ip: Ipv4Addr) -> Arc> { + { + let read = self.inner.read(); + if let Some(stats) = read.get(&ip) { + return stats.clone(); + } + } + let mut write = self.inner.write(); + write + .entry(ip) + .or_insert_with(|| Arc::new(Mutex::new(PingStats::default()))) + .clone() + } + + pub fn record_sent(&self, ip: Ipv4Addr) { + let stats = self.get_or_create(ip); + stats.lock().sent += 1; + } + + pub fn record_received(&self, ip: Ipv4Addr) { + let stats = self.get_or_create(ip); + stats.lock().received += 1; + } + + pub fn get_loss_info(&self, ip: &Ipv4Addr) -> Option { + let read = self.inner.read(); + read.get(ip).map(|stats| { + let guard = stats.lock(); + let loss_rate = if guard.sent > 0 { + 1.0 - (guard.received as f64 / guard.sent as f64) + } else { + 0.0 + }; + PacketLossInfo { + ip: *ip, + sent: guard.sent, + received: guard.received, + loss_rate, + } + }) + } + + pub fn get_all_loss_info(&self) -> Vec { + let read = self.inner.read(); + read.iter() + .map(|(ip, stats)| { + let guard = stats.lock(); + let loss_rate = if guard.sent > 0 { + 1.0 - (guard.received as f64 / guard.sent as f64) + } else { + 0.0 + }; + PacketLossInfo { + ip: *ip, + sent: guard.sent, + received: guard.received, + loss_rate, + } + }) + .collect() + } + + pub fn reset(&self, ip: &Ipv4Addr) { + let read = self.inner.read(); + if let Some(stats) = read.get(ip) { + *stats.lock() = PingStats::default(); + } + } + + pub fn reset_all(&self) { + let read = self.inner.read(); + for stats in read.values() { + *stats.lock() = PingStats::default(); + } + } + + pub fn clear(&self) { + self.inner.write().clear(); + } +} + +pub mod config; +pub(crate) mod nat; + +#[derive(Clone, Default)] +pub(crate) struct AppState { + config: Arc>>>, + pub(crate) network: SharedNetworkAddr, + pub(crate) server_info_collection: ServerInfoCollection, + pub(crate) peer_map: PeerInfoMap, + pub(crate) route_table: RouteTable, + pub(crate) subnet_route: SubnetExternalRoute, + pub(crate) nat_info: MyNatInfo, + pub(crate) punch_backoff: PunchBackoff, + pub(crate) packet_loss_stats: PacketLossStats, + pub(crate) traffic_stats: TrafficStats, +} +#[derive(Clone, Default)] +pub(crate) struct SharedNetworkAddr { + inner: Arc>>, +} +impl SharedNetworkAddr { + pub fn network(&self) -> Option { + self.inner.lock().as_ref().map(|v| v.network()) + } + pub fn ip(&self) -> Option { + self.inner.lock().map(|v| v.ip) + } + pub fn get(&self) -> Option { + *self.inner.lock() + } + pub fn set(&self, addr: NetworkAddr) { + *self.inner.lock() = Some(addr); + } + pub fn clear(&self) { + *self.inner.lock() = None; + } +} + +/// 网络路由封装,包含本地网络信息和子网路由 +#[derive(Clone)] +pub(crate) struct NetworkRoute { + pub network: SharedNetworkAddr, + pub subnet_route: SubnetExternalRoute, +} + +impl NetworkRoute { + pub fn new(network: SharedNetworkAddr, subnet_route: SubnetExternalRoute) -> Self { + Self { + network, + subnet_route, + } + } + + /// 检查 IP 是否在本地网络或子网路由中 + pub fn network_contains(&self, ip: &Ipv4Addr) -> bool { + if let Some(network) = self.network.network() + && network.contains(ip) + { + return true; + } + self.subnet_route.route(ip).is_some() + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PeerClientInfo { + pub nat_info: Option, +} + +#[derive(Clone, Default)] +pub(crate) struct PeerInfoMap { + inner: Arc>>, +} + +impl PeerInfoMap { + pub fn get(&self, ip: &Ipv4Addr) -> Option { + self.inner.lock().get(ip).cloned() + } + + pub fn update_nat_info(&self, ip: Ipv4Addr, nat_info: NatInfo) { + let mut guard = self.inner.lock(); + if let Some(v) = guard.get_mut(&ip) { + v.nat_info = Some(nat_info); + return; + } + guard.insert( + ip, + PeerClientInfo { + nat_info: Some(nat_info), + }, + ); + } + + pub fn clear(&self) { + self.inner.lock().clear(); + } +} + +#[derive(Clone, Default)] +pub(crate) struct ServerInfoCollection { + client_simple_list: Arc>>, + server_node_map: Arc>>, +} +#[derive(Clone, Default)] +pub struct ServerNodeInfo { + pub server_id: u32, + pub server_addr: ProtocolAddress, + pub connected: bool, + pub rtt: Option, + pub data_version: u64, + pub client_map: HashMap, + pub last_connected_time: Option, + pub disconnected_time: Option, + pub server_version: Option, +} +impl ServerInfoCollection { + pub fn server_client_ip_map(&self) -> HashMap, u32)> { + self.server_node_map + .read() + .iter() + .map(|(k, v)| { + ( + *k, + ( + v.client_map + .iter() + .filter(|(_, v)| v.online) + .map(|(k, _)| *k) + .collect(), + v.rtt.unwrap_or(500), + ), + ) + }) + .collect() + } + pub fn server_node_list(&self) -> Vec { + self.server_node_map.read().values().cloned().collect() + } + pub fn update_server(&self, addr: Vec<(u32, ProtocolAddress)>) { + let mut server_node_map_guard = self.server_node_map.write(); + let mut client_simple_list_guard = self.client_simple_list.write(); + server_node_map_guard.clear(); + client_simple_list_guard.clear(); + for (server_id, server_addr) in addr { + let server_node = ServerNodeInfo { + server_id, + server_addr, + ..Default::default() + }; + server_node_map_guard.insert(server_id, server_node); + } + } + pub fn find_connected_server(&self, server_ids: &[u32]) -> Option { + let map = self.server_node_map.read(); + + server_ids + .iter() + .filter_map(|id| { + let server = map.get(id)?; + + if !server.connected { + return None; + } + + let rtt = server.rtt.unwrap_or(u32::MAX); + + Some((*id, rtt)) + }) + .min_by_key(|(_, rtt)| *rtt) + .map(|(id, _)| id) + } + pub fn find_ip_to_server(&self, server_ids: &[u32], ip: &Ipv4Addr) -> Option { + let map = self.server_node_map.read(); + + server_ids + .iter() + .filter_map(|id| { + let server = map.get(id)?; + + if !server.connected { + return None; + } + + let client = server.client_map.get(ip)?; + if !client.online { + return None; + } + + let rtt = server.rtt.unwrap_or(u32::MAX); + + Some((*id, rtt)) + }) + .min_by_key(|(_, rtt)| *rtt) + .map(|(id, _)| id) + } + pub fn client_online_ips(&self) -> Vec { + self.client_simple_list + .read() + .iter() + .filter(|v| v.online) + .map(|c| c.ip) + .collect() + } + pub fn client_ips(&self) -> Vec { + self.client_simple_list.read().clone() + } + pub fn data_version(&self, server_id: u32) -> u64 { + self.server_node_map + .read() + .get(&server_id) + .map(|v| v.data_version) + .unwrap_or(0) + } + pub fn update_client_simple_list( + &self, + server_id: u32, + self_ip: Ipv4Addr, + client_simple_list: ClientSimpleInfoList, + now: i64, + ) { + let mut guard = self.server_node_map.write(); + let server_node = guard.entry(server_id).or_default(); + if now > client_simple_list.time { + server_node.rtt = Some((now - client_simple_list.time) as u32); + } + server_node.data_version = client_simple_list.data_version; + let map: HashMap = client_simple_list + .list + .into_iter() + .filter(|v| v.ip != self_ip) + .map(|info| (info.ip, info)) + .collect(); + + if client_simple_list.is_all { + server_node.client_map = map; + } else { + server_node.client_map.extend(map); + } + let mut client_simple_map = HashMap::::new(); + for (_, server_node) in guard.iter() { + for (_, x) in server_node.client_map.iter() { + if let Some(v) = client_simple_map.get_mut(&x.ip) { + if x.online { + v.online = true; + } + } else { + client_simple_map.insert(x.ip, x.clone()); + } + } + } + + let mut guard = self.client_simple_list.write(); + *guard = client_simple_map.into_values().collect() + } + pub fn set_server_connected(&self, server_id: u32, val: bool) -> bool { + let mut mutex_guard = self.server_node_map.write(); + let server_node = mutex_guard.entry(server_id).or_default(); + let old = server_node.connected; + server_node.connected = val; + old + } + pub fn is_any_server_connected(&self, server_ids: Option<&[u32]>) -> bool { + let guard = self.server_node_map.read(); + if let Some(server_ids) = server_ids { + for id in server_ids { + if guard.get(id).map(|v| v.connected).unwrap_or(false) { + return true; + } + } + } else { + for (_, server_node) in guard.iter() { + if server_node.connected { + return true; + } + } + } + + false + } + pub fn is_server_connected(&self, server_id: u32) -> bool { + self.server_node_map + .read() + .get(&server_id) + .map(|v| v.connected) + .unwrap_or(false) + } + pub fn set_last_connected_time(&self, server_id: u32, last_connected_time: Option) { + self.server_node_map + .write() + .entry(server_id) + .or_default() + .last_connected_time = last_connected_time; + } + pub fn set_disconnected_time(&self, server_id: u32, last_connected_time: Option) { + self.server_node_map + .write() + .entry(server_id) + .or_default() + .disconnected_time = last_connected_time; + } + pub fn set_server_rtt(&self, server_id: u32, rtt: u32) { + if let Some(v) = self.server_node_map.write().get_mut(&server_id) { + v.rtt = Some(rtt); + } + } + pub fn set_server_version(&self, server_id: u32, version: String) { + if let Some(v) = self.server_node_map.write().get_mut(&server_id) { + v.server_version = Some(version); + } + } + pub fn get_server_rtt(&self, ip: &Ipv4Addr) -> Option { + let server_node_map_guard = self.server_node_map.read(); + for (_, server_node) in server_node_map_guard.iter() { + if !server_node.connected { + continue; + } + if let Some(v) = server_node.client_map.get(ip) + && v.online + { + return server_node.rtt; + } + } + None + } + pub fn exists_online_client_ip(&self, ip: &Ipv4Addr) -> bool { + self.client_simple_list + .read() + .iter() + .any(|v| v.ip == *ip && v.online) + } + pub fn clear(&self) { + self.client_simple_list.write().clear(); + let mut guard = self.server_node_map.write(); + for server_node in guard.values_mut() { + server_node.connected = false; + server_node.rtt = None; + server_node.data_version = 0; + server_node.client_map.clear(); + server_node.last_connected_time = None; + server_node.disconnected_time = None; + } + } +} +#[derive(Copy, Clone, Debug)] +pub struct NetworkAddr { + pub gateway: Ipv4Addr, + pub broadcast: Ipv4Addr, + pub ip: Ipv4Addr, + pub prefix_len: u8, +} +impl NetworkAddr { + pub fn network(&self) -> Ipv4Net { + Ipv4Net::new_assert(self.ip, self.prefix_len) + } +} + +impl AppState { + pub fn stop_network(&self) { + self.network.clear(); + self.server_info_collection.clear(); + self.peer_map.clear(); + // route_table 来自外部 crate,会在任务停止后自动失效 + self.nat_info.clear(); + self.punch_backoff.clear(); + self.packet_loss_stats.clear(); + self.traffic_stats.clear(); + } + + fn network(&self) -> Option { + self.network.network() + } + pub fn get_network(&self) -> Option { + self.network.get() + } + fn network_contains(&self, ip: &Ipv4Addr) -> bool { + let Some(network) = self.network() else { + return false; + }; + if network.contains(ip) { + return true; + } + self.subnet_route.route(ip).is_some() + } + pub fn client_ips(&self) -> Vec { + self.server_info_collection.client_ips() + } + pub fn get_peer_info(&self, ip: &Ipv4Addr) -> Option { + self.peer_map.get(ip) + } + + pub fn set_config(&self, config: Box) { + *self.config.lock() = Some(config); + } + + pub fn get_config(&self) -> Option> { + self.config.lock().clone() + } + pub(crate) fn udp_stun(&self) -> Vec { + self.config + .lock() + .as_ref() + .map(|v| v.udp_stun.clone()) + .unwrap_or_default() + } + pub(crate) fn tcp_stun(&self) -> Vec { + self.config + .lock() + .as_ref() + .map(|v| v.tcp_stun.clone()) + .unwrap_or_default() + } +} +impl AppState { + pub fn get_punch_info(&self) -> Option { + self.nat_info.get().map(|info| PunchInfo { + nat_info: self.filter_ip(info), + }) + } + pub fn get_nat_info(&self) -> Option { + self.nat_info.get().map(|info| self.filter_ip(info)) + } + pub fn filter_ip(&self, mut info: NatInfo) -> NatInfo { + if self.network_contains(&info.local_ipv4) { + info.local_ipv4 = Ipv4Addr::UNSPECIFIED; + } + info.local_ipv4s.retain(|ip| !self.network_contains(ip)); + info + } +} diff --git a/vnt-core/src/context/nat.rs b/vnt-core/src/context/nat.rs new file mode 100644 index 0000000..224b9ff --- /dev/null +++ b/vnt-core/src/context/nat.rs @@ -0,0 +1,147 @@ +use parking_lot::RwLock; +use rust_p2p_core::nat::NatInfo; +use rust_p2p_core::route::Index; +use rust_p2p_core::tunnel::udp::UDPIndex; +use std::collections::HashMap; +use std::net::{Ipv4Addr, SocketAddr}; +use std::sync::Arc; + +#[derive(Clone, Default)] +pub struct MyNatInfo { + nat_info: Arc>>, +} +impl MyNatInfo { + pub fn get(&self) -> Option { + self.nat_info.read().clone() + } + pub fn update_public_addr(&self, index: Index, addr: SocketAddr) { + let (ip, port) = if let Some(r) = mapping_addr(addr) { + r + } else { + return; + }; + log::debug!("public_addr:{},{},index={index:?}", ip, port); + let mut nat_info = self.nat_info.write(); + let Some(nat_info) = nat_info.as_mut() else { + return; + }; + if rust_p2p_core::extend::addr::is_ipv4_global(&ip) { + if !nat_info.public_ips.contains(&ip) { + nat_info.public_ips.push(ip); + } + match index { + Index::Udp(index) => { + let index = match index { + UDPIndex::MainV4(index) => index, + UDPIndex::MainV6(index) => index, + UDPIndex::SubV4(_) => return, + }; + if let Some(p) = nat_info.public_udp_ports.get_mut(index) { + *p = port; + } + } + Index::Tcp(_) => { + nat_info.public_tcp_port = port; + } + _ => {} + } + } else { + log::debug!("not public addr: {addr:?}") + } + } + pub fn update_tcp_public_addr(&self, addr: SocketAddr) { + let SocketAddr::V4(addr) = addr else { + return; + }; + let ip = *addr.ip(); + let port = addr.port(); + log::info!("tcp_public_addr, {}:{}", ip, port); + let mut nat_info = self.nat_info.write(); + let Some(nat_info) = nat_info.as_mut() else { + return; + }; + if ip.is_unspecified() && port == 0 { + nat_info.public_tcp_port = 0; + return; + } + if rust_p2p_core::extend::addr::is_ipv4_global(&ip) { + if !nat_info.public_ips.contains(&ip) { + nat_info.public_ips.push(ip); + } + nat_info.public_tcp_port = port; + } else { + log::debug!("not public addr: {addr:?}") + } + } + pub fn replace_nat_info(&self, nat_info: NatInfo) { + self.nat_info.write().replace(nat_info); + } + pub fn clear(&self) { + *self.nat_info.write() = None; + } +} +fn mapping_addr(addr: SocketAddr) -> Option<(Ipv4Addr, u16)> { + match addr { + SocketAddr::V4(addr) => Some((*addr.ip(), addr.port())), + SocketAddr::V6(addr) => addr.ip().to_ipv4_mapped().map(|ip| (ip, addr.port())), + } +} + +#[derive(Copy, Clone, Debug)] +pub struct PunchState { + pub count: i64, + pub last_ts: i64, +} + +#[derive(Clone, Default)] +pub struct PunchBackoff { + inner: Arc>>, +} + +impl PunchBackoff { + const MAX_BACKOFF_MS: i64 = 3_600_000; // 1h + const BASE_MS: i64 = 3000; + + fn now() -> i64 { + crate::utils::time::now_ts_ms() + } + + pub fn record(&self, ip: Ipv4Addr) { + let mut map = self.inner.write(); + let entry = map.entry(ip).or_insert(PunchState { + count: 0, + last_ts: 0, + }); + entry.count += 1; + entry.last_ts = Self::now(); + } + #[allow(dead_code)] + pub fn reset(&self, ip: Ipv4Addr) { + self.inner.write().remove(&ip); + } + #[allow(dead_code)] + pub fn get(&self, ip: &Ipv4Addr) -> Option { + self.inner.read().get(ip).copied() + } + + pub fn should_punch(&self, ip: Ipv4Addr) -> bool { + let map = self.inner.read(); + let Some(state) = map.get(&ip) else { + return true; + }; + + let now = Self::now(); + let elapsed = now - state.last_ts; + + let mut backoff = Self::BASE_MS * state.count; + if backoff > Self::MAX_BACKOFF_MS { + backoff = Self::MAX_BACKOFF_MS; + } + + elapsed >= backoff + } + #[allow(dead_code)] + pub fn clear(&self) { + self.inner.write().clear(); + } +} diff --git a/vnt-core/src/core/mod.rs b/vnt-core/src/core/mod.rs new file mode 100644 index 0000000..e2b562f --- /dev/null +++ b/vnt-core/src/core/mod.rs @@ -0,0 +1,353 @@ +use crate::api::VntApi; +use crate::compression::PacketCompression; +use crate::context::config::Config; +use crate::context::{AppState, NetworkAddr, NetworkRoute}; +use crate::crypto::PacketCrypto; +use crate::enhanced_tunnel::enhanced_ipv4_tunnel; +use crate::enhanced_tunnel::inbound::EnhancedInbound; +use crate::enhanced_tunnel::outbound::EnhancedOutbound; +use crate::fec::{FecDecoder, FecEncoder}; +use crate::nat::internal_nat::{InternalNatInbound, PortMappingManager}; +use crate::nat::{AllowSubnetExternalRoute, SubnetExternalRoute}; +use crate::tun::enhanced_tun::EnhancedTunInbound; +use crate::tun::{DeviceConfig, DeviceIOManager, TunDataInbound, TunReceiver, tun_channel}; +use crate::tunnel_core::outbound::{BasicOutbound, HybridOutbound}; +use crate::tunnel_core::p2p::inbound::{P2pInboundConfig, P2pInboundHandler}; +use crate::tunnel_core::p2p::transport::punch::NatPuncher; +use crate::tunnel_core::p2p::transport::task::init_tunnel; +use crate::tunnel_core::server::connection_manager::{ + InboundHandlerConfig, ServerTurnManager, coordinated_registration, create_server_tunnel, +}; +use crate::tunnel_core::server::rpc::ServerRPC; +use crate::utils::task_control::TaskGroup; +use anyhow::bail; +use ipnet::Ipv4Net; +use std::net::Ipv4Addr; + +pub const DEFAULT_MTU: u16 = 1380; + +/// Context for deferred registration +struct RegistrationContext { + server_managers: Vec, + subnet_external_route: SubnetExternalRoute, + puncher: NatPuncher, + packet_crypto: PacketCrypto, + packet_compression: PacketCompression, + enhanced_inbound: EnhancedInbound, + fec_decoder: FecDecoder, +} + +pub struct NetworkManager { + config: Box, + app_state: AppState, + task_group: TaskGroup, + device_io_manager: DeviceIOManager, + enhanced_outbound: Option, + server_rpc: ServerRPC, + tun_receiver: Option, + registration_context: Option>, +} + +impl NetworkManager { + pub async fn create_network( + config: Box, + task_group: TaskGroup, + ) -> anyhow::Result { + let app_state = AppState::default(); + config.check()?; + let mtu = config.mtu.unwrap_or(DEFAULT_MTU); + let packet_crypto = PacketCrypto::new_from_str(config.password.as_deref()); + let packet_compression = PacketCompression::new(config.compress); + let (server_manager_list, tunnel_to_server, server_rpc) = + create_server_tunnel(app_state.clone(), &config, packet_crypto.clone()); + let device_io_manager = DeviceIOManager::new(task_group.clone()); + let allow_subnet = AllowSubnetExternalRoute::new(config.output.clone()); + + let (puncher, p2p_socket, p2p_task) = if !config.no_punch { + let (puncher, p2p_socket_manager, p2p_task) = init_tunnel( + task_group.clone(), + app_state.clone(), + tunnel_to_server.clone(), + packet_crypto.clone(), + ) + .await?; + + (Some(puncher), Some(p2p_socket_manager), Some(p2p_task)) + } else { + (None, None, None) + }; + let puncher = NatPuncher::new( + app_state.network.clone(), + app_state.punch_backoff.clone(), + puncher, + packet_crypto.clone(), + ); + let subnet_external_route = app_state.subnet_route.clone(); + subnet_external_route.set_route_table(config.input.clone()); + + let fec_decoder = FecDecoder::new(); + let basic_outbound = BasicOutbound::new( + tunnel_to_server.clone(), + p2p_socket.clone(), + packet_crypto.clone(), + ); + let fec_encoder = if config.fec { + Some(FecEncoder::new(&task_group, basic_outbound.clone())) + } else { + None + }; + + let hybrid_outbound = HybridOutbound::new( + app_state.network.clone(), + app_state.server_info_collection.clone(), + app_state.traffic_stats.clone(), + basic_outbound, + packet_compression.clone(), + subnet_external_route.clone(), + fec_encoder, + ); + let port_mapping_manager = PortMappingManager::new( + config.no_tun, + config.allow_port_mapping, + app_state.network.clone(), + ); + let internal_nat_inbound = if config.no_nat && !config.no_tun { + None + } else { + let nat_inbound = InternalNatInbound::create( + &task_group, + mtu, + hybrid_outbound.clone(), + allow_subnet.clone(), + app_state.network.clone(), + config.no_tun, + ) + .await?; + Some(nat_inbound) + }; + + let (enhanced_tun_inbound, tun_receiver) = if config.no_tun { + ( + EnhancedTunInbound::Nat( + internal_nat_inbound + .clone() + .expect("internal_nat_inbound must be Some when no_tun is true"), + ), + None, + ) + } else { + let (tun_inbound, tun_receiver) = tun_channel(); + let tun_data_sender = TunDataInbound::new(tun_inbound, allow_subnet.clone()); + (EnhancedTunInbound::Tun(tun_data_sender), Some(tun_receiver)) + }; + + let (enhanced_inbound, enhanced_outbound) = enhanced_ipv4_tunnel( + app_state.clone(), + task_group.clone(), + enhanced_tun_inbound, + crate::enhanced_tunnel::TunnelConfig { + mtu, + password: config.password.clone(), + open_quic_client: config.rtx, + port_mapping: config.port_mapping.clone(), + }, + crate::enhanced_tunnel::TunnelComponents { + hybrid_outbound: hybrid_outbound.clone(), + external_route: subnet_external_route.clone(), + internal_nat_inbound, + port_mapping_manager, + }, + ) + .await?; + + if let Some(p2p_task) = p2p_task { + let handler = P2pInboundHandler::new(P2pInboundConfig { + network_route: NetworkRoute::new( + app_state.network.clone(), + subnet_external_route.clone(), + ), + route_table: app_state.route_table.clone(), + packet_loss_stats: app_state.packet_loss_stats.clone(), + packet_crypto: packet_crypto.clone(), + packet_compression: packet_compression.clone(), + enhanced_inbound: enhanced_inbound.clone(), + fec_decoder: fec_decoder.clone(), + }); + p2p_task.start(handler); + } + + let registration_context = Box::new(RegistrationContext { + server_managers: server_manager_list, + subnet_external_route, + puncher, + packet_crypto, + packet_compression, + enhanced_inbound, + fec_decoder, + }); + + app_state.set_config(config.clone()); + Ok(Self { + config, + app_state, + task_group, + device_io_manager, + enhanced_outbound, + server_rpc, + tun_receiver, + registration_context: Some(registration_context), + }) + } + + /// Register with server(s) and start data handling tasks. + /// This method can only be called once. + /// Returns the registration response on success. + pub async fn register(&mut self) -> anyhow::Result { + let Some(mut ctx) = self.registration_context.take() else { + bail!("register can only be called once"); + }; + + let is_multi_server = ctx.server_managers.len() > 1; + + let reg_response = if is_multi_server { + // Multi-server: coordinated pre-registration + log::info!( + "Multi-server mode: performing coordinated registration for {} servers", + ctx.server_managers.len() + ); + let reg_response = coordinated_registration(&mut ctx.server_managers).await?; + log::info!( + "Coordinated registration completed, IP: {}, prefix_len: {}", + reg_response.ip, + reg_response.prefix_len + ); + reg_response + } else { + // Single-server: normal registration + log::info!("Single-server mode: performing normal registration"); + let response = ctx.server_managers[0] + .connect_and_reg(crate::protocol::control_message::RegistrationMode::Normal) + .await?; + match response { + crate::protocol::control_message::ResponseMessage::Reg(reg) => { + log::info!( + "Registration completed, IP: {}, prefix_len: {}", + reg.ip, + reg.prefix_len + ); + reg + } + crate::protocol::control_message::ResponseMessage::Error(e) => { + bail!("Registration failed: {}", e.message); + } + crate::protocol::control_message::ResponseMessage::ConfirmReg(_) => { + bail!("Unexpected ConfirmReg response"); + } + } + }; + let network_addr = NetworkAddr { + gateway: reg_response.gateway, + broadcast: Ipv4Net::new(reg_response.ip, reg_response.prefix_len)?.broadcast(), + ip: reg_response.ip, + prefix_len: reg_response.prefix_len, + }; + self.app_state.network.set(network_addr); + + // 保存服务器版本信息 + if !reg_response.server_version.is_empty() { + for (index, _) in ctx.server_managers.iter().enumerate() { + self.app_state.server_info_collection.set_server_version( + index as u32, + reg_response.server_version.clone(), + ); + } + } + + // Start data handling tasks for all servers + for turn_manager in ctx.server_managers { + let handler_config = Box::new(InboundHandlerConfig { + network_route: NetworkRoute::new( + self.app_state.network.clone(), + ctx.subnet_external_route.clone(), + ), + server_info: self.app_state.server_info_collection.clone(), + nat_info: self.app_state.nat_info.clone(), + peer_map: self.app_state.peer_map.clone(), + punch_backoff: self.app_state.punch_backoff.clone(), + puncher: ctx.puncher.clone(), + packet_crypto: ctx.packet_crypto.clone(), + packet_compression: ctx.packet_compression.clone(), + enhanced_inbound: ctx.enhanced_inbound.clone(), + fec_decoder: ctx.fec_decoder.clone(), + }); + turn_manager.data_handle_task_connected(&self.task_group, handler_config, network_addr); + } + + Ok(network_addr) + } + + pub fn is_no_tun(&self) -> bool { + self.config.no_tun + } + + pub async fn start_tun(&mut self) -> anyhow::Result<()> { + let Some(receiver) = self.tun_receiver.take() else { + bail!("start_tun can only be called once"); + }; + let Some(enhanced_outbound) = self.enhanced_outbound.take() else { + bail!("start_tun can only be called once"); + }; + let mut config = DeviceConfig::default(); + config = config.set_mtu(self.config.mtu.unwrap_or(DEFAULT_MTU)); + if let Some(tun_name) = self.config.tun_name.clone() { + config = config.set_tun_name(tun_name); + } + self.device_io_manager + .start_task(config, receiver, enhanced_outbound) + .await + } + #[cfg(unix)] + pub async fn start_tun_fd(&mut self, tun_fd: Option) -> anyhow::Result<()> { + let Some(receiver) = self.tun_receiver.take() else { + bail!("start_tun_fd can only be called once"); + }; + let Some(enhanced_outbound) = self.enhanced_outbound.take() else { + bail!("start_tun_fd can only be called once"); + }; + let mut config = DeviceConfig::default(); + if let Some(tun_fd) = tun_fd { + config = config.set_tun_fd(tun_fd); + } + if let Some(tun_name) = self.config.tun_name.clone() { + config = config.set_tun_name(tun_name); + } + self.device_io_manager + .start_task(config, receiver, enhanced_outbound) + .await + } + #[cfg(not(target_os = "android"))] + pub async fn set_network_ip(&self, ip: Ipv4Addr, prefix_len: u8) -> anyhow::Result<()> { + self.device_io_manager.set_network(ip, prefix_len).await?; + Ok(()) + } + + fn stop_network(&mut self) { + self.task_group.stop(); + self.app_state.stop_network(); + } + #[cfg(not(target_os = "android"))] + pub async fn tun_if_index(&self) -> anyhow::Result { + self.device_io_manager.tun_if_index().await + } + pub async fn wait_all_stopped(&mut self) { + self.task_group.wait_all_stopped().await; + } + pub fn vnt_api(&self) -> VntApi { + VntApi::new(self.app_state.clone(), self.server_rpc.clone()) + } +} +impl Drop for NetworkManager { + fn drop(&mut self) { + self.stop_network(); + } +} diff --git a/vnt-core/src/crypto/chacha20_poly1305.rs b/vnt-core/src/crypto/chacha20_poly1305.rs new file mode 100644 index 0000000..633a4d0 --- /dev/null +++ b/vnt-core/src/crypto/chacha20_poly1305.rs @@ -0,0 +1,169 @@ +use crate::protocol::ip_packet_protocol::{HEAD_LENGTH, NetPacket}; +use ring::aead::{Aad, CHACHA20_POLY1305, LessSafeKey, Nonce, UnboundKey}; +use std::io; + +pub const TAG_LEN: usize = 16; + +#[derive(Clone)] +pub struct PacketCrypto { + key: LessSafeKey, +} + +impl PacketCrypto { + pub fn key_sign(s: &str) -> String { + use ring::digest::{Context, SHA256}; + + const PREFIX: &[u8] = b"KEY-BEGIN"; + const SUFFIX: &[u8] = b"KEY-END"; + + let mut ctx = Context::new(&SHA256); + ctx.update(PREFIX); + ctx.update(s.as_bytes()); + ctx.update(SUFFIX); + let digest = ctx.finish(); + let mut key_bytes = [0u8; 16]; + key_bytes.copy_from_slice(&digest.as_ref()[..16]); + key_bytes + .iter() + .map(|b| format!("{:02x}", b)) + .collect::() + } + pub fn new(key_bytes: [u8; 32]) -> Self { + let unbound = UnboundKey::new(&CHACHA20_POLY1305, &key_bytes).unwrap(); + let key = LessSafeKey::new(unbound); + Self { key } + } + pub fn new_from_str(s: &str) -> Self { + let hash = ring::digest::digest(&ring::digest::SHA256, s.as_bytes()); + let mut key_bytes = [0u8; 32]; + key_bytes.copy_from_slice(hash.as_ref()); + Self::new(key_bytes) + } + /// 根据包头生成 12 字节 nonce + pub fn make_nonce>(&self, pkt: &NetPacket) -> io::Result<[u8; 12]> { + let buf = pkt.buffer(); + + if buf.len() < HEAD_LENGTH { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "buffer too small", + )); + } + let msg_type = buf[0]; + let seq = &buf[4..8]; + let src = &buf[8..12]; + let dst = &buf[12..16]; + + let mut nonce12 = [0u8; 12]; + nonce12[0..4].copy_from_slice(seq); + nonce12[4..8].copy_from_slice(dst); + nonce12[8..12].copy_from_slice(src); + nonce12[0] = msg_type; + + Ok(nonce12) + } + + /// 原地加密(in-place) + /// payload 后需要预留16字节用于存放 tag + pub fn encrypt_in_place + AsMut<[u8]>>( + &self, + pkt: &mut NetPacket, + ) -> io::Result<()> { + let nonce = Nonce::assume_unique_for_key(self.make_nonce(pkt)?); + + let payload = pkt.payload_mut(); + let payload_len = payload.len() - TAG_LEN; // 实际 payload 长度(不含 tag 预留空间) + + // 只加密实际的 payload 部分 + let tag = self + .key + .seal_in_place_separate_tag(nonce, Aad::empty(), &mut payload[..payload_len]) + .map_err(|_| io::Error::other("encrypt failed"))?; + + // 将 tag 写入 payload 后的预留空间 + payload[payload_len..payload_len + TAG_LEN].copy_from_slice(tag.as_ref()); + + Ok(()) + } + + /// 原地解密(in-place) + pub fn decrypt_in_place + AsMut<[u8]>>( + &self, + pkt: &mut NetPacket, + ) -> io::Result { + let nonce = Nonce::assume_unique_for_key(self.make_nonce(pkt)?); + + let payload_with_tag = pkt.payload_mut(); + + let plaintext = self + .key + .open_in_place(nonce, Aad::empty(), payload_with_tag) + .map_err(|_| io::Error::other("decrypt failed"))?; + Ok(plaintext.len()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bytes::BytesMut; + + // 用于构造一个简单的 NetPacket,包含头 16 字节 + payload + 16 字节 TAG 预留 + fn build_test_packet(payload_len: usize) -> NetPacket { + // 16 字节 head + payload + 16 字节预留 TAG + let total_len = HEAD_LENGTH + payload_len + TAG_LEN; + let mut buf = BytesMut::zeroed(total_len); + + // 构造一个头(16 字节) + buf[0] = 4; // MsgType::Ping + buf[4..8].copy_from_slice(&12345u32.to_be_bytes()); + buf[8..12].copy_from_slice(&111u32.to_be_bytes()); + buf[12..16].copy_from_slice(&222u32.to_be_bytes()); + + // 构造 payload(明文) + let payload_plain = &mut buf[HEAD_LENGTH..HEAD_LENGTH + payload_len]; + for (i, p) in payload_plain.iter_mut().enumerate() { + *p = (i as u8) ^ 0xAB; + } + + NetPacket::new(buf).unwrap() + } + + #[test] + fn test_encrypt_decrypt_in_place() { + let key = [7u8; 32]; + let crypto = PacketCrypto::new(key); + + let payload_len = 20; + let mut pkt = build_test_packet(payload_len); + + // 备份原 payload + let original_payload: Vec = + pkt.buffer()[HEAD_LENGTH..HEAD_LENGTH + payload_len].to_vec(); + + // 加密 + crypto.encrypt_in_place(&mut pkt).expect("encrypt failed"); + + let encrypted_buf = pkt.buffer(); + let tag_start = HEAD_LENGTH + payload_len; + let tag_end = tag_start + TAG_LEN; + + // TAG 不应该是全 0 + assert_ne!(&encrypted_buf[tag_start..tag_end], &[0u8; TAG_LEN]); + + // payload 已被加密,不等于明文 + assert_ne!( + &encrypted_buf[HEAD_LENGTH..HEAD_LENGTH + payload_len], + &original_payload[..] + ); + + // 解密 + crypto.decrypt_in_place(&mut pkt).expect("decrypt failed"); + + let decrypted_buf = pkt.buffer(); + let decrypted_payload = &decrypted_buf[HEAD_LENGTH..HEAD_LENGTH + payload_len]; + + // 解密后与原文一致 + assert_eq!(decrypted_payload, &original_payload[..]); + } +} diff --git a/vnt-core/src/crypto/mod.rs b/vnt-core/src/crypto/mod.rs new file mode 100644 index 0000000..bdf6666 --- /dev/null +++ b/vnt-core/src/crypto/mod.rs @@ -0,0 +1,47 @@ +use crate::crypto::chacha20_poly1305::TAG_LEN; +use crate::protocol::ip_packet_protocol::NetPacket; +use std::io; +use std::sync::Arc; + +mod chacha20_poly1305; + +use crate::protocol::transmission::{ExtendEnd, ShrinkEnd}; + +#[derive(Clone)] +pub(crate) struct PacketCrypto { + crypto: Option>, +} +impl PacketCrypto { + pub(crate) fn key_sign(s: &str) -> String { + chacha20_poly1305::PacketCrypto::key_sign(s) + } + + pub(crate) fn new_from_str(s: Option<&str>) -> Self { + Self { + crypto: s.map(chacha20_poly1305::PacketCrypto::new_from_str).map(Arc::new), + } + } + pub(crate) fn encrypt_reserve(&self) -> usize { + if self.crypto.is_some() { TAG_LEN } else { 0 } + } + pub(crate) fn encrypt_in_place + AsMut<[u8]> + ExtendEnd>( + &self, + pkt: &mut NetPacket, + ) -> io::Result<()> { + if let Some(crypto) = self.crypto.as_ref() { + pkt.source_buf_mut().extend_end(TAG_LEN); + return crypto.encrypt_in_place(pkt); + } + Ok(()) + } + pub(crate) fn decrypt_in_place + AsMut<[u8]> + ShrinkEnd>( + &self, + pkt: &mut NetPacket, + ) -> io::Result<()> { + if let Some(crypto) = self.crypto.as_ref() { + let _ = crypto.decrypt_in_place(pkt)?; + pkt.source_buf_mut().shrink_end(TAG_LEN); + } + Ok(()) + } +} diff --git a/vnt-core/src/enhanced_tunnel/inbound.rs b/vnt-core/src/enhanced_tunnel/inbound.rs new file mode 100644 index 0000000..9f97926 --- /dev/null +++ b/vnt-core/src/enhanced_tunnel/inbound.rs @@ -0,0 +1,72 @@ +use crate::context::{NetworkAddr, TrafficStats}; +use crate::enhanced_tunnel::quic_over::quic_inbound::EnhancedQuicInbound; +use crate::nat::internal_nat::InternalNatInbound; +use crate::protocol::ip_packet_protocol::{HEAD_LENGTH, MsgType, NetPacket}; +use crate::protocol::transmission::TransmissionBytes; +use crate::tun::enhanced_tun::EnhancedTunInbound; +use anyhow::{Context, bail}; +use pnet_packet::ipv4::Ipv4Packet; +use std::net::Ipv4Addr; + +#[derive(Clone)] +pub(crate) struct EnhancedInbound { + tun_data_inbound: EnhancedTunInbound, + quic_inbound: EnhancedQuicInbound, + internal_nat_inbound: Option, + traffic_stats: TrafficStats, +} + +impl EnhancedInbound { + pub fn new( + tun_data_inbound: EnhancedTunInbound, + quic_inbound: EnhancedQuicInbound, + internal_nat_inbound: Option, + traffic_stats: TrafficStats, + ) -> Self { + Self { + tun_data_inbound, + quic_inbound, + internal_nat_inbound, + traffic_stats, + } + } + pub async fn inbound( + &self, + network_addr: &NetworkAddr, + msg_type: MsgType, + src: Ipv4Addr, + packet: NetPacket, + ) -> anyhow::Result<()> { + let mut buf = packet.into_buffer(); + self.traffic_stats.record_rx(src, buf.len() as u64); + buf.advance_head(HEAD_LENGTH)?; + + match msg_type { + MsgType::Turn => { + if let Some(internal_nat_inbound) = self.internal_nat_inbound.as_ref() { + let Some(ipv4) = Ipv4Packet::new(&buf) else { + bail!("EnhancedInbound not ipv4") + }; + let dest = ipv4.get_destination(); + if dest != network_addr.ip && !network_addr.network().contains(&dest) { + internal_nat_inbound.send(&buf, network_addr).await?; + return Ok(()); + } + } + self.tun_data_inbound.inbound(buf, network_addr).await?; + } + MsgType::Broadcast | MsgType::ExcludeBroadcast => { + self.tun_data_inbound.inbound(buf, network_addr).await?; + } + MsgType::Quic => { + let payload = buf.into_bytes().freeze(); + self.quic_inbound + .inbound(payload, src) + .await + .context("inbound quic")?; + } + _ => {} + } + Ok(()) + } +} diff --git a/vnt-core/src/enhanced_tunnel/mod.rs b/vnt-core/src/enhanced_tunnel/mod.rs new file mode 100644 index 0000000..3021bc3 --- /dev/null +++ b/vnt-core/src/enhanced_tunnel/mod.rs @@ -0,0 +1,75 @@ +use crate::context::AppState; +use crate::enhanced_tunnel::inbound::EnhancedInbound; +use crate::enhanced_tunnel::outbound::EnhancedOutbound; +use crate::nat::SubnetExternalRoute; +use crate::nat::internal_nat::{InternalNatInbound, PortMappingManager}; +use crate::port_mapping::PortMapping; +use crate::tun::enhanced_tun::EnhancedTunInbound; +use crate::tunnel_core::outbound::HybridOutbound; +use crate::utils::task_control::TaskGroup; + +pub(crate) mod quic_over; + +pub(crate) mod inbound; +pub(crate) mod outbound; + +pub(crate) struct TunnelConfig { + pub mtu: u16, + pub password: Option, + pub open_quic_client: bool, + pub port_mapping: Vec, +} + +pub(crate) struct TunnelComponents { + pub hybrid_outbound: HybridOutbound, + pub external_route: SubnetExternalRoute, + pub internal_nat_inbound: Option, + pub port_mapping_manager: PortMappingManager, +} + +pub(crate) async fn enhanced_ipv4_tunnel( + app_state: AppState, + task_group: TaskGroup, + tun_data_sender: EnhancedTunInbound, + config: TunnelConfig, + components: TunnelComponents, +) -> anyhow::Result<(EnhancedInbound, Option)> { + let password = config.password.unwrap_or_else(|| "password".to_string()); + let tun = match &tun_data_sender { + EnhancedTunInbound::Tun(tun) => Some(tun.clone()), + EnhancedTunInbound::Nat(_) => None, + }; + let (inbound, outbound) = quic_over::boot::quic_tunnel_start( + app_state.clone(), + task_group, + tun, + quic_over::boot::QuicTunnelConfig { + mtu: config.mtu, + password, + open_quic_client: config.open_quic_client, + port_mapping: config.port_mapping, + }, + quic_over::boot::QuicTunnelComponents { + hybrid_outbound: components.hybrid_outbound.clone(), + external_route: components.external_route, + internal_nat_manager: components.internal_nat_inbound.clone(), + port_mapping_manager: components.port_mapping_manager, + }, + ) + .await?; + let enhanced_inbound = EnhancedInbound::new( + tun_data_sender, + inbound, + components.internal_nat_inbound, + app_state.traffic_stats.clone(), + ); + + let enhanced_outbound = outbound.map(|outbound| { + EnhancedOutbound::new( + app_state.network.clone(), + outbound, + components.hybrid_outbound, + ) + }); + Ok((enhanced_inbound, enhanced_outbound)) +} diff --git a/vnt-core/src/enhanced_tunnel/outbound.rs b/vnt-core/src/enhanced_tunnel/outbound.rs new file mode 100644 index 0000000..8059374 --- /dev/null +++ b/vnt-core/src/enhanced_tunnel/outbound.rs @@ -0,0 +1,68 @@ +use crate::context::SharedNetworkAddr; +use crate::enhanced_tunnel::quic_over::quic_outbound::EnhancedQuicOutbound; +use crate::protocol::transmission::TransmissionBytes; +use crate::tunnel_core::outbound::HybridOutbound; +use pnet_packet::ipv4::Ipv4Packet; + +pub struct EnhancedOutbound { + network: SharedNetworkAddr, + enhanced_quic_outbound: EnhancedQuicOutbound, + hybrid_outbound: HybridOutbound, +} + +impl EnhancedOutbound { + pub fn new( + network: SharedNetworkAddr, + enhanced_quic_outbound: EnhancedQuicOutbound, + hybrid_outbound: HybridOutbound, + ) -> Self { + Self { + network, + enhanced_quic_outbound, + hybrid_outbound, + } + } + pub async fn ipv4_outbound(&self, data: TransmissionBytes) { + if data.is_empty() || data[0] >> 4 != 4 { + return; + } + if let Err(e) = self.ipv4_outbound_impl(data).await { + log::warn!("EnhancedOutbound error: {:?}", e); + } + } + async fn ipv4_outbound_impl(&self, data: TransmissionBytes) -> anyhow::Result<()> { + let Some(ipv4) = Ipv4Packet::new(data.as_ref()) else { + return Ok(()); + }; + let Some(net) = self.network.get() else { + return Ok(()); + }; + let src = ipv4.get_source(); + + let dest = ipv4.get_destination(); + if dest == src || dest.is_unspecified() { + return Ok(()); + } + if dest == net.gateway { + // 发送到网关 + return self.hybrid_outbound.ipv4_gateway_outbound(net, data).await; + } + if dest.is_multicast() || dest == net.broadcast || dest.is_broadcast() { + // 广播 + return self + .hybrid_outbound + .ipv4_broadcast_outbound(net, data) + .await; + } + if self + .enhanced_quic_outbound + .outbound(&net, data.as_ref()) + .await + { + // 使用quic 通道传输 + return Ok(()); + } + // 使用通用通道传输 + self.hybrid_outbound.ipv4_outbound(net, data).await + } +} diff --git a/vnt-core/src/enhanced_tunnel/quic_over/boot.rs b/vnt-core/src/enhanced_tunnel/quic_over/boot.rs new file mode 100644 index 0000000..ad2a297 --- /dev/null +++ b/vnt-core/src/enhanced_tunnel/quic_over/boot.rs @@ -0,0 +1,200 @@ +use crate::context::AppState; +use crate::enhanced_tunnel::quic_over::enhanced_io::enhanced_inbound::{ + QuicDataInbound, create_enhanced_inbound, +}; +use crate::enhanced_tunnel::quic_over::enhanced_io::enhanced_outbound::create_enhanced_outbound; +use crate::enhanced_tunnel::quic_over::enhanced_io::socket::ExtendedQuicSocket; +use crate::enhanced_tunnel::quic_over::quic_client::QuicTunnelClient; +use crate::enhanced_tunnel::quic_over::quic_inbound::EnhancedQuicInbound; +use crate::enhanced_tunnel::quic_over::quic_outbound::EnhancedQuicOutbound; +use crate::enhanced_tunnel::quic_over::{quic_client, quic_server}; +use crate::nat::SubnetExternalRoute; +use crate::nat::internal_nat::{InternalNatInbound, PortMappingManager}; +use crate::port_mapping::PortMapping; +use crate::tls; +use crate::tun::TunDataInbound; +use crate::tunnel_core::outbound::HybridOutbound; +use crate::utils::task_control::TaskGroup; +use anyhow::Context; +use quinn::congestion::BbrConfig; +use quinn::crypto::rustls::QuicServerConfig; +use quinn::{ClientConfig, Endpoint, EndpointConfig, TransportConfig, default_runtime}; +use rustls::ServerConfig; +use sha2::{Digest, Sha256}; +use std::io; +use std::sync::Arc; +use std::time::Duration; +use tcp_ip::{IpStackConfig, IpStackRecv}; + +pub(crate) struct QuicTunnelConfig { + pub mtu: u16, + pub password: String, + pub open_quic_client: bool, + pub port_mapping: Vec, +} + +pub(crate) struct QuicTunnelComponents { + pub hybrid_outbound: HybridOutbound, + pub external_route: SubnetExternalRoute, + pub internal_nat_manager: Option, + pub port_mapping_manager: PortMappingManager, +} + +pub(crate) async fn quic_tunnel_start( + app_state: AppState, + task_group: TaskGroup, + tun_data_sender: Option, + config: QuicTunnelConfig, + components: QuicTunnelComponents, +) -> anyhow::Result<(EnhancedQuicInbound, Option)> { + let ip_stack_config = IpStackConfig { + mtu: config.mtu, + ..Default::default() + }; + let (ip_stack, ip_socket, quic_outbound) = if let Some(tun_data_sender) = tun_data_sender { + let (ip_stack, ip_stack_send, ip_stack_recv) = tcp_ip::ip_stack(ip_stack_config)?; + let ip_socket = tcp_ip::ip::IpSocket::bind_all(None, ip_stack.clone()).await?; + let ip_socket = Arc::new(ip_socket); + task_group.spawn(ip_stack_recv_task( + ip_stack_recv, + app_state.clone(), + tun_data_sender, + )); + let quic_outbound = + EnhancedQuicOutbound::new(config.open_quic_client, ip_stack_send, ip_stack.clone()); + + (Some(ip_stack), Some(ip_socket), Some(quic_outbound)) + } else { + (None, None, None) + }; + + let (inbound, endpoint) = create_quic_endpoint( + config.password, + task_group.clone(), + components.hybrid_outbound, + ) + .await?; + quic_server::server_listen( + &task_group, + endpoint.clone(), + ip_socket.clone(), + ip_stack.clone(), + components.internal_nat_manager, + components.port_mapping_manager, + ) + .await; + if config.open_quic_client { + let quic_client = + QuicTunnelClient::new(app_state.clone(), endpoint, components.external_route); + + // 客户端使用指纹验证 + if let (Some(ip_stack), Some(ip_socket)) = (ip_stack, ip_socket) { + quic_client::create_client( + quic_client.clone(), + task_group.clone(), + ip_stack.clone(), + ip_socket, + ) + .await; + } + if !config.port_mapping.is_empty() { + crate::port_mapping::port_mapping_start(&task_group, config.port_mapping, quic_client) + .await?; + } + } else if !config.port_mapping.is_empty() { + let quic_client = + QuicTunnelClient::new(app_state.clone(), endpoint, components.external_route); + + crate::port_mapping::port_mapping_start(&task_group, config.port_mapping, quic_client) + .await?; + } + + let quic_inbound = EnhancedQuicInbound::new(inbound); + Ok((quic_inbound, quic_outbound)) +} +async fn create_quic_endpoint( + password: String, + task_group: TaskGroup, + hybrid_outbound: HybridOutbound, +) -> anyhow::Result<(QuicDataInbound, Endpoint)> { + let (cert, private_key) = crate::tls::cert::generate_deterministic_cert(&password)?; + let mut hasher = Sha256::new(); + hasher.update(cert.as_ref()); + let calculated_hash: [u8; 32] = hasher.finalize().into(); + log::info!("QUIC Cert Fingerprint: {}", hex::encode(calculated_hash)); + + let outbound = create_enhanced_outbound(task_group.clone(), hybrid_outbound).await; + let (inbound, inbound_receiver) = create_enhanced_inbound(); + let socket = ExtendedQuicSocket::new(inbound_receiver, outbound); + + let server_config = ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(vec![cert], private_key) + .context("TLS config error")?; + + let server_crypto = QuicServerConfig::try_from(server_config) + .map_err(|e| anyhow::anyhow!("QUIC TLS config error: {:?}", e))?; + let server_config = quinn::ServerConfig::with_crypto(Arc::new(server_crypto)); + // 替换运行时 + let runtime = default_runtime().ok_or_else(|| io::Error::other("no async runtime found"))?; + let fingerprint_verifier = tls::verifier::FingerprintVerifier::new(calculated_hash); + + let client_config = rustls::ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(fingerprint_verifier)) + .with_no_client_auth(); + let mut client_config = ClientConfig::new(Arc::new( + quinn::crypto::rustls::QuicClientConfig::try_from(client_config) + .context("Failed to create QUIC client config")?, + )); + client_config.transport_config(build_transport_config()); + let mut endpoint_config = EndpointConfig::default(); + endpoint_config.max_udp_payload_size(1300)?; + let mut endpoint = quinn::Endpoint::new_with_abstract_socket( + endpoint_config, + Some(server_config), + Arc::new(socket), + runtime, + ) + .context("quic server create failed")?; + endpoint.set_default_client_config(client_config); + Ok((inbound, endpoint)) +} + +fn build_transport_config() -> Arc { + let mut transport = TransportConfig::default(); + transport.congestion_controller_factory(Arc::new(BbrConfig::default())); + transport.keep_alive_interval(Some(Duration::from_secs(5))); + + transport.max_idle_timeout(Some(Duration::from_secs(10).try_into().unwrap())); + + Arc::new(transport) +} + +async fn ip_stack_recv_task( + mut ip_stack_recv: IpStackRecv, + app_state: AppState, + tun_data_sender: TunDataInbound, +) { + let mut buf = vec![0u8; 1500]; + loop { + let len = match ip_stack_recv.recv(&mut buf).await { + Ok(len) => len, + Err(e) => { + log::error!("IP stack recv error: {:?}", e); + break; + } + }; + let Some(net) = app_state.get_network() else { + log::error!("not network"); + break; + }; + match tun_data_sender.send((&buf[..len]).into(), &net).await { + Ok(_) => {} + Err(e) => { + log::error!("IP stack send error: {:?}", e); + break; + } + } + } +} diff --git a/vnt-core/src/enhanced_tunnel/quic_over/enhanced_io/enhanced_inbound.rs b/vnt-core/src/enhanced_tunnel/quic_over/enhanced_io/enhanced_inbound.rs new file mode 100644 index 0000000..ae6183c --- /dev/null +++ b/vnt-core/src/enhanced_tunnel/quic_over/enhanced_io/enhanced_inbound.rs @@ -0,0 +1,93 @@ +use anyhow::anyhow; +use bytes::Bytes; +use parking_lot::Mutex; +use quinn::udp::RecvMeta; +use std::fmt::{Debug, Formatter}; +use std::io::IoSliceMut; +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::sync::Arc; +use std::task::{Context, Poll}; +use tokio::sync::mpsc::{Receiver, Sender}; + +#[derive(Clone)] +pub struct QuicInnerInboundReceiver { + receiver: Arc>>, +} +#[derive(Clone)] +pub struct QuicDataInbound { + sender: Sender<(Bytes, Ipv4Addr)>, +} +impl QuicDataInbound { + pub async fn send(&self, data: Bytes, addr: Ipv4Addr) -> anyhow::Result<()> { + self.sender + .send((data, addr)) + .await + .map_err(|_e| anyhow!("quic data inbound error")) + } +} +impl Debug for QuicInnerInboundReceiver { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EnhancedInbound").finish() + } +} +pub fn create_enhanced_inbound() -> (QuicDataInbound, QuicInnerInboundReceiver) { + let (sender, receiver) = tokio::sync::mpsc::channel(256); + ( + QuicDataInbound { sender }, + QuicInnerInboundReceiver::new(receiver), + ) +} +impl QuicInnerInboundReceiver { + pub fn new(receiver: Receiver<(Bytes, Ipv4Addr)>) -> Self { + Self { + receiver: Arc::new(Mutex::new(receiver)), + } + } + + pub fn poll_recv( + &self, + cx: &mut Context, + bufs: &mut [IoSliceMut<'_>], + meta: &mut [RecvMeta], + ) -> Poll> { + let mut guard = self.receiver.lock(); + let rs = guard.poll_recv(cx); + drop(guard); + match rs { + Poll::Ready(Some((buf, ip))) => { + let (buf_mut, meta) = match (bufs.get_mut(0), meta.get_mut(0)) { + (Some(b), Some(m)) => (b, m), + _ => { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "no buffer available", + ))); + } + }; + + if buf_mut.len() < buf.len() { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!( + "buffer too small: need {}, got {}", + buf.len(), + buf_mut.len() + ), + ))); + } + + buf_mut[..buf.len()].copy_from_slice(&buf); + + meta.len = buf.len(); + meta.stride = buf.len(); + meta.addr = SocketAddr::V4(SocketAddrV4::new(ip, 10000)); + Poll::Ready(Ok(1)) + } + Poll::Ready(None) => Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "inbound channel closed", + ))), + Poll::Pending => Poll::Pending, + } + } +} diff --git a/vnt-core/src/enhanced_tunnel/quic_over/enhanced_io/enhanced_outbound.rs b/vnt-core/src/enhanced_tunnel/quic_over/enhanced_io/enhanced_outbound.rs new file mode 100644 index 0000000..a37f1f6 --- /dev/null +++ b/vnt-core/src/enhanced_tunnel/quic_over/enhanced_io/enhanced_outbound.rs @@ -0,0 +1,89 @@ +use crate::protocol::ip_packet_protocol::{HEAD_LENGTH, MsgType, NetPacket}; +use crate::protocol::transmission::TransmissionBytes; +use crate::tunnel_core::outbound::HybridOutbound; +use crate::utils::task_control::TaskGroup; +use quinn::UdpPoller; +use std::fmt::{Debug, Formatter}; +use std::io; +use std::net::Ipv4Addr; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tokio::sync::mpsc::{Sender, error::TrySendError}; +use tokio_util::sync::PollSender; + +#[derive(Clone)] +pub struct QuicInnerOutbound { + sender: Sender<(Ipv4Addr, NetPacket)>, +} + +pub async fn create_enhanced_outbound( + task_group: TaskGroup, + hybrid_outbound: HybridOutbound, +) -> QuicInnerOutbound { + let (s, mut r) = tokio::sync::mpsc::channel(256); + + task_group.spawn(async move { + while let Some((dst, packet)) = r.recv().await { + if let Err(e) = hybrid_outbound.outbound_raw(dst, packet).await { + log::debug!("outbound error: {e:?}, dst={dst}"); + } + } + }); + + QuicInnerOutbound { sender: s } +} + +impl QuicInnerOutbound { + pub fn try_outbound(&self, buf: &[u8], dest: Ipv4Addr) -> io::Result<()> { + let send = match self.sender.try_reserve() { + Ok(send) => send, + Err(TrySendError::Full(_)) => { + return Err(io::Error::new( + io::ErrorKind::WouldBlock, + "outbound channel full", + )); + } + Err(TrySendError::Closed(_)) => { + return Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "outbound channel closed", + )); + } + }; + let mut packet = NetPacket::new(TransmissionBytes::zeroed(HEAD_LENGTH + buf.len()))?; + packet.set_ttl(5); + packet.set_msg_type(MsgType::Quic); + packet.set_dest_id(dest.into()); + packet.set_payload(buf)?; + send.send((dest, packet)); + Ok(()) + } + pub fn create_io_poller(&self) -> Pin> { + Box::pin(EnhancedOutboundPoller { + sender: PollSender::new(self.sender.clone()), + }) + } +} +pub struct EnhancedOutboundPoller { + sender: PollSender<(Ipv4Addr, NetPacket)>, +} +impl Debug for EnhancedOutboundPoller { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EnhancedOutboundPoller").finish() + } +} +impl UdpPoller for EnhancedOutboundPoller { + fn poll_writable(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + match self.sender.poll_reserve(cx) { + Poll::Ready(Ok(_)) => { + self.sender.abort_send(); + Poll::Ready(Ok(())) + } + Poll::Ready(Err(_e)) => Poll::Ready(Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "outbound channel closed", + ))), + Poll::Pending => Poll::Pending, + } + } +} diff --git a/vnt-core/src/enhanced_tunnel/quic_over/enhanced_io/mod.rs b/vnt-core/src/enhanced_tunnel/quic_over/enhanced_io/mod.rs new file mode 100644 index 0000000..1f9ab80 --- /dev/null +++ b/vnt-core/src/enhanced_tunnel/quic_over/enhanced_io/mod.rs @@ -0,0 +1,3 @@ +pub mod enhanced_inbound; +pub mod enhanced_outbound; +pub mod socket; diff --git a/vnt-core/src/enhanced_tunnel/quic_over/enhanced_io/socket.rs b/vnt-core/src/enhanced_tunnel/quic_over/enhanced_io/socket.rs new file mode 100644 index 0000000..d466614 --- /dev/null +++ b/vnt-core/src/enhanced_tunnel/quic_over/enhanced_io/socket.rs @@ -0,0 +1,55 @@ +use crate::enhanced_tunnel::quic_over::enhanced_io::enhanced_inbound::QuicInnerInboundReceiver; +use crate::enhanced_tunnel::quic_over::enhanced_io::enhanced_outbound::QuicInnerOutbound; +use quinn::udp::{RecvMeta, Transmit}; +use quinn::{AsyncUdpSocket, UdpPoller}; +use std::fmt::{Debug, Formatter}; +use std::io::IoSliceMut; +use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; + +pub struct ExtendedQuicSocket { + inbound: QuicInnerInboundReceiver, + outbound: QuicInnerOutbound, +} +impl Debug for ExtendedQuicSocket { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("QuicSocket").finish() + } +} +impl ExtendedQuicSocket { + pub fn new(inbound: QuicInnerInboundReceiver, outbound: QuicInnerOutbound) -> Self { + Self { inbound, outbound } + } +} + +impl AsyncUdpSocket for ExtendedQuicSocket { + fn create_io_poller(self: Arc) -> Pin> { + self.outbound.create_io_poller() + } + + fn try_send(&self, transmit: &Transmit) -> std::io::Result<()> { + let IpAddr::V4(dest) = transmit.destination.ip() else { + return Ok(()); + }; + + self.outbound.try_outbound(transmit.contents, dest) + } + + fn poll_recv( + &self, + cx: &mut Context, + bufs: &mut [IoSliceMut<'_>], + meta: &mut [RecvMeta], + ) -> Poll> { + self.inbound.poll_recv(cx, bufs, meta) + } + + fn local_addr(&self) -> std::io::Result { + Ok(SocketAddr::V4(SocketAddrV4::new( + Ipv4Addr::new(127, 0, 0, 1), + 10000, + ))) + } +} diff --git a/vnt-core/src/enhanced_tunnel/quic_over/mod.rs b/vnt-core/src/enhanced_tunnel/quic_over/mod.rs new file mode 100644 index 0000000..71031bd --- /dev/null +++ b/vnt-core/src/enhanced_tunnel/quic_over/mod.rs @@ -0,0 +1,7 @@ +mod enhanced_io; +pub(crate) mod quic_client; +pub(crate) mod quic_inbound; +pub(crate) mod quic_outbound; +mod quic_server; + +pub(crate) mod boot; diff --git a/vnt-core/src/enhanced_tunnel/quic_over/quic_client.rs b/vnt-core/src/enhanced_tunnel/quic_over/quic_client.rs new file mode 100644 index 0000000..cc3feca --- /dev/null +++ b/vnt-core/src/enhanced_tunnel/quic_over/quic_client.rs @@ -0,0 +1,309 @@ +use crate::context::AppState; + +use crate::nat::SubnetExternalRoute; +use crate::protocol::client_message::{ + IpProxyHandshake, QuicProxyHandshake, TcpProxyHandshake, quic_proxy_handshake, +}; +use crate::utils::task_control::TaskGroup; +use anyhow::{Context, bail}; +use bytes::Bytes; +use futures::SinkExt; +use parking_lot::Mutex; +use pnet_packet::ip::IpNextHeaderProtocol; +use prost::Message; +use quinn::{Connection, Endpoint, RecvStream, SendStream}; +use std::collections::HashMap; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::sync::Arc; +use tcp_ip::IpStack; +use tcp_ip::ip::IpSocket; +use tcp_ip::tcp::TcpStream; +use tokio::io::AsyncWriteExt; +use tokio::sync::OnceCell; +use tokio::sync::mpsc::Sender; +use tokio::sync::mpsc::error::TrySendError; +use tokio_util::codec::{FramedWrite, LengthDelimitedCodec}; + +#[derive(Clone)] +pub struct QuicTunnelClient { + app_state: AppState, + endpoint: Endpoint, + connection_map: Arc>>>>, + external_route: SubnetExternalRoute, +} + +impl QuicTunnelClient { + pub fn new( + app_state: AppState, + endpoint: Endpoint, + external_route: SubnetExternalRoute, + ) -> QuicTunnelClient { + Self { + app_state, + endpoint, + connection_map: Arc::new(Default::default()), + external_route, + } + } + + pub async fn open_bi(&self, mut dest: Ipv4Addr) -> anyhow::Result<(SendStream, RecvStream)> { + let Some(net) = self.app_state.get_network() else { + bail!("no network found"); + }; + if !net.network().contains(&dest) { + if let Some(v) = self.external_route.route(&dest) { + dest = v; + } else { + bail!("invalid route found:{dest}"); + } + } + let mut count = 0; + loop { + count += 1; + let cell = self + .connection_map + .lock() + .entry(dest) + .or_insert_with(|| Arc::new(OnceCell::new())) + .clone(); + let connection = cell + .get_or_try_init(|| async { + self.endpoint + .connect(SocketAddr::new(dest.into(), 10000), "localhost")? + .await + .context("connect failed") + }) + .await?; + + return match connection.open_bi().await { + Ok(rs) => Ok(rs), + Err(e) => { + self.connection_map.lock().remove(&dest); + if count == 1 { + continue; + } + Err(e.into()) + } + }; + } + } + pub async fn open_uni(&self, mut dest: Ipv4Addr) -> anyhow::Result { + let Some(net) = self.app_state.get_network() else { + bail!("no network found"); + }; + if !net.network().contains(&dest) { + if let Some(v) = self.external_route.route(&dest) { + dest = v; + } else { + bail!("invalid route found:{dest}"); + } + } + let mut count = 0; + loop { + count += 1; + let cell = self + .connection_map + .lock() + .entry(dest) + .or_insert_with(|| Arc::new(OnceCell::new())) + .clone(); + let connection = cell + .get_or_try_init(|| async { + self.endpoint + .connect(SocketAddr::new(dest.into(), 10000), "localhost")? + .await + .context("connect failed") + }) + .await?; + return match connection.open_uni().await { + Ok(rs) => Ok(rs), + Err(e) => { + self.connection_map.lock().remove(&dest); + if count == 1 { + continue; + } + Err(e.into()) + } + }; + } + } +} +pub(crate) async fn send_handshake( + send_stream: &mut SendStream, + handshake: QuicProxyHandshake, +) -> anyhow::Result<()> { + let handshake = handshake.encode_to_vec(); + send_stream.write_u16(handshake.len() as u16).await?; + send_stream.write_all(&handshake).await?; + Ok(()) +} + +pub async fn create_client( + quic_client: QuicTunnelClient, + task_group: TaskGroup, + ip_stack: IpStack, + ip_socket: Arc, +) { + task_group.spawn(tcp_listen( + task_group.clone(), + ip_stack.clone(), + quic_client.clone(), + )); + task_group.spawn(ip_listen(task_group.clone(), ip_socket, quic_client)); +} + +async fn tcp_listen( + task_group: TaskGroup, + ip_stack: IpStack, + quic_tunnel_client: QuicTunnelClient, +) { + if let Err(e) = tcp_listen_impl(task_group, ip_stack, quic_tunnel_client).await { + log::error!("tcp_listen {e:?}"); + } +} + +async fn tcp_listen_impl( + task_group: TaskGroup, + ip_stack: IpStack, + quic_tunnel_client: QuicTunnelClient, +) -> anyhow::Result<()> { + let mut listener = tcp_ip::tcp::TcpListener::bind_all(ip_stack).await?; + loop { + let (tcp_stream, addr) = listener.accept().await?; + let quic_tunnel_client = quic_tunnel_client.clone(); + task_group.spawn(async move { + if let Err(e) = tcp_stream_handle(tcp_stream, quic_tunnel_client).await { + log::error!("TCP stream handle failed with error: {e:?},addr={addr}"); + } + }); + } +} + +async fn tcp_stream_handle( + tcp_stream: TcpStream, + quic_tunnel_client: QuicTunnelClient, +) -> anyhow::Result<()> { + // 连接方向是反过来的,因为自己充当目标做了tcp卸载 + let SocketAddr::V4(peer_addr) = tcp_stream.local_addr()? else { + bail!("invalid IP address"); + }; + let SocketAddr::V4(local_addr) = tcp_stream.peer_addr()? else { + bail!("invalid IP address"); + }; + log::debug!("connect TCP stream {}->{}", local_addr, peer_addr); + + let (mut send_stream, mut recv_stream) = quic_tunnel_client.open_bi(*peer_addr.ip()).await?; + let handshake = QuicProxyHandshake { + handshake: Some(quic_proxy_handshake::Handshake::Tcp(TcpProxyHandshake { + src_ip: (*local_addr.ip()).into(), + src_port: local_addr.port().into(), + dst_ip: (*peer_addr.ip()).into(), + dst_port: peer_addr.port().into(), + })), + }; + send_handshake(&mut send_stream, handshake).await?; + let (mut tcp_w, mut tcp_r) = tcp_stream.split()?; + tokio::select! { + _ = tokio::io::copy(&mut recv_stream, &mut tcp_w) => {}, + _ = tokio::io::copy(&mut tcp_r, &mut send_stream) => {}, + } + log::debug!("disconnect TCP stream {}->{}", local_addr, peer_addr); + Ok(()) +} + +async fn ip_listen( + task_group: TaskGroup, + ip_socket: Arc, + quic_tunnel_client: QuicTunnelClient, +) { + if let Err(e) = ip_listen_impl(task_group, ip_socket, quic_tunnel_client).await { + log::error!("ip_listen {e:?}"); + } +} +#[derive(Eq, PartialEq, Hash, Copy, Clone, Debug)] +struct IpKey { + protocol: IpNextHeaderProtocol, + src: Ipv4Addr, + dest: Ipv4Addr, +} +async fn ip_listen_impl( + task_group: TaskGroup, + ip_socket: Arc, + quic_tunnel_client: QuicTunnelClient, +) -> anyhow::Result<()> { + let mut buf = vec![0u8; 65536]; + let dest_map = Arc::new(Mutex::new(HashMap::>::new())); + + loop { + let (len, protocol, src, dest) = ip_socket.recv_protocol_from_to(&mut buf).await?; + let (IpAddr::V4(src), IpAddr::V4(dest)) = (src, dest) else { + continue; + }; + let key = IpKey { + protocol, + src, + dest, + }; + let bytes = Bytes::copy_from_slice(&buf[..len]); + + let tx = { + let mut map = dest_map.lock(); + if let Some(tx) = map.get(&key) { + tx.clone() + } else { + let (tx, rx) = tokio::sync::mpsc::channel::(128); + + spawn_dest_sender(task_group.clone(), key, rx, quic_tunnel_client.clone()); + + map.insert(key, tx.clone()); + tx + } + }; + + if let Err(err) = tx.try_send(bytes) { + match err { + TrySendError::Full(_) => {} + TrySendError::Closed(_) => { + let mut map = dest_map.lock(); + map.remove(&key); + } + } + } + } +} + +fn spawn_dest_sender( + task_group: TaskGroup, + key: IpKey, + mut rx: tokio::sync::mpsc::Receiver, + quic_tunnel_client: QuicTunnelClient, +) { + log::info!("send ip({}) packet {}->{}", key.protocol, key.src, key.dest); + task_group.spawn(async move { + let result = async { + let mut send_stream = quic_tunnel_client.open_uni(key.dest).await?; + + let handshake = QuicProxyHandshake { + handshake: Some(quic_proxy_handshake::Handshake::Ip(IpProxyHandshake { + ip_next_header_protocol: key.protocol.0 as _, + src_ip: key.src.into(), + dst_ip: key.dest.into(), + })), + }; + send_handshake(&mut send_stream, handshake).await?; + + let mut framed = FramedWrite::new(send_stream, LengthDelimitedCodec::new()); + + while let Some(pkt) = rx.recv().await { + framed.send(pkt).await?; + } + + Ok::<(), anyhow::Error>(()) + } + .await; + + if let Err(e) = result { + log::error!("key {:?} sender task exit: {:?}", key, e); + } + }); +} diff --git a/vnt-core/src/enhanced_tunnel/quic_over/quic_inbound.rs b/vnt-core/src/enhanced_tunnel/quic_over/quic_inbound.rs new file mode 100644 index 0000000..7aa8736 --- /dev/null +++ b/vnt-core/src/enhanced_tunnel/quic_over/quic_inbound.rs @@ -0,0 +1,18 @@ +use crate::enhanced_tunnel::quic_over::enhanced_io::enhanced_inbound::QuicDataInbound; +use bytes::Bytes; +use std::net::Ipv4Addr; + +#[derive(Clone)] +pub struct EnhancedQuicInbound { + quic_data_inbound: QuicDataInbound, +} + +impl EnhancedQuicInbound { + pub fn new(quic_data_inbound: QuicDataInbound) -> Self { + Self { quic_data_inbound } + } + pub async fn inbound(&self, data: Bytes, src: Ipv4Addr) -> anyhow::Result<()> { + self.quic_data_inbound.send(data, src).await?; + Ok(()) + } +} diff --git a/vnt-core/src/enhanced_tunnel/quic_over/quic_outbound.rs b/vnt-core/src/enhanced_tunnel/quic_over/quic_outbound.rs new file mode 100644 index 0000000..11c212f --- /dev/null +++ b/vnt-core/src/enhanced_tunnel/quic_over/quic_outbound.rs @@ -0,0 +1,94 @@ +use crate::context::NetworkAddr; +use pnet_packet::Packet; +use pnet_packet::ip::IpNextHeaderProtocols; +use pnet_packet::ipv4::{Ipv4Flags, Ipv4Packet}; +use pnet_packet::tcp::TcpFlags::{ACK, SYN}; +use pnet_packet::tcp::TcpPacket; +use std::net::SocketAddr; +use tcp_ip::{IpStack, IpStackSend}; + +pub struct EnhancedQuicOutbound { + open_quic_client: bool, + ip_stack_send: IpStackSend, + ip_stack: IpStack, +} + +impl EnhancedQuicOutbound { + pub fn new(open_quic_client: bool, ip_stack_send: IpStackSend, ip_stack: IpStack) -> Self { + Self { + open_quic_client, + ip_stack_send, + ip_stack, + } + } + pub async fn outbound(&self, _net: &NetworkAddr, data: &[u8]) -> bool { + let Some(ipv4) = Ipv4Packet::new(data) else { + return true; + }; + + if self.open_quic_client { + // 针对tcp 如果不是从IpStack建立的连接,则不使用IpStack解析 + if ipv4.get_next_level_protocol() == IpNextHeaderProtocols::Tcp { + let more_fragments = + ipv4.get_flags() & Ipv4Flags::MoreFragments == Ipv4Flags::MoreFragments; + let offset = ipv4.get_fragment_offset(); + let segmented = more_fragments || offset > 0; + if !segmented { + let Some(tcp) = TcpPacket::new(ipv4.payload()) else { + return true; + }; + // 不是第一个包 + if !(tcp.get_flags() & SYN == SYN && tcp.get_flags() & ACK != ACK) { + let local_addr = + SocketAddr::new(ipv4.get_source().into(), tcp.get_source()); + let peer_addr = + SocketAddr::new(ipv4.get_destination().into(), tcp.get_destination()); + // 在IpStack中找不到连接 + if !self + .ip_stack + .has_tcp_connection(local_addr, peer_addr) + .unwrap_or(false) + && !self + .ip_stack + .has_tcp_connection(peer_addr, local_addr) + .unwrap_or(false) + && !self + .ip_stack + .has_tcp_half_open(peer_addr, local_addr) + .unwrap_or(false) + { + return false; + } + } + } + } + _ = self.ip_stack_send.send_ip_packet(data).await; + return true; + } + // 判断tcp流 + if ipv4.get_next_level_protocol() == IpNextHeaderProtocols::Tcp { + let more_fragments = + ipv4.get_flags() & Ipv4Flags::MoreFragments == Ipv4Flags::MoreFragments; + let offset = ipv4.get_fragment_offset(); + let segmented = more_fragments || offset > 0; + if !segmented && let Some(tcp) = TcpPacket::new(ipv4.payload()) { + // 如果对端使用IpStack连接了自己,则也需要原路回复 + // 这是连接回复,所以方向是和流方向相反的 + let peer_addr = SocketAddr::new(ipv4.get_source().into(), tcp.get_source()); + let local_addr = + SocketAddr::new(ipv4.get_destination().into(), tcp.get_destination()); + + if self + .ip_stack + .has_tcp_connection(local_addr, peer_addr) + .unwrap_or(false) + { + _ = self.ip_stack_send.send_ip_packet(data).await; + return true; + } + } + } + + false + } +} diff --git a/vnt-core/src/enhanced_tunnel/quic_over/quic_server.rs b/vnt-core/src/enhanced_tunnel/quic_over/quic_server.rs new file mode 100644 index 0000000..90dfc6e --- /dev/null +++ b/vnt-core/src/enhanced_tunnel/quic_over/quic_server.rs @@ -0,0 +1,245 @@ +use crate::nat::internal_nat::{InternalNatInbound, PortMappingManager}; +use crate::protocol::client_message::QuicProxyHandshake; +use crate::protocol::client_message::quic_proxy_handshake::Handshake; +use crate::utils::task_control::TaskGroup; +use anyhow::{Context, bail}; +use futures::StreamExt; +use pnet_packet::ip::IpNextHeaderProtocol; +use prost::Message; +use quinn::{Connection, Endpoint, RecvStream, SendStream}; +use std::net::{Ipv4Addr, SocketAddr}; +use std::sync::Arc; +use tcp_ip::IpStack; +use tcp_ip::ip::IpSocket; +use tokio::io::AsyncReadExt; +use tokio_util::codec::{FramedRead, LengthDelimitedCodec}; + +pub async fn server_listen( + task_group: &TaskGroup, + endpoint: Endpoint, + ip_socket: Option>, + ip_stack: Option, + internal_nat_manager: Option, + port_mapping_manager: PortMappingManager, +) { + task_group.spawn(quic_endpoint_accept( + ip_stack, + task_group.clone(), + endpoint, + ip_socket, + internal_nat_manager, + port_mapping_manager, + )); +} + +async fn quic_endpoint_accept( + ip_stack: Option, + task_group: TaskGroup, + endpoint: Endpoint, + ip_socket: Option>, + internal_nat_manager: Option, + port_mapping_manager: PortMappingManager, +) { + while let Some(connecting) = endpoint.accept().await { + let remote_addr = connecting.remote_address(); + let task_group_clone = task_group.clone(); + let ip_socket = ip_socket.clone(); + let ip_stack = ip_stack.clone(); + let internal_nat_manager = internal_nat_manager.clone(); + let port_mapping_manager = port_mapping_manager.clone(); + task_group.spawn(async move { + match connecting.await { + Ok(connection) => { + log::info!("QUIC connection: {}", remote_addr); + if let Err(e) = quic_accept( + ip_stack, + task_group_clone, + connection, + ip_socket, + internal_nat_manager, + port_mapping_manager, + ) + .await + { + log::info!("quic close: {remote_addr},{e:?}",); + } + } + Err(e) => { + log::error!("connect: {:?},remote_addr={remote_addr}", e); + } + } + }); + } + log::warn!("quic server closed"); +} + +async fn quic_accept( + ip_stack: Option, + task_group_clone: TaskGroup, + connection: Connection, + ip_socket: Option>, + internal_nat_manager: Option, + port_mapping_manager: PortMappingManager, +) -> anyhow::Result<()> { + loop { + tokio::select! { + rs = connection.accept_bi()=>{ + let (send_stream, recv_stream) = rs?; + let ip_stack = ip_stack.clone(); + let internal_nat_manager = internal_nat_manager.clone(); + let port_mapping_manager = port_mapping_manager.clone(); + task_group_clone.spawn(async move { + if let Err(e) = quic_stream_bi_handle(ip_stack,send_stream, recv_stream,&internal_nat_manager,port_mapping_manager).await{ + log::error!("quic_stream_bi_handle: {e:?}"); + } + }); + } + rs = connection.accept_uni()=>{ + let recv_stream = rs?; + let ip_socket = ip_socket.clone(); + let internal_nat_manager = internal_nat_manager.clone(); + task_group_clone.spawn(async move { + if let Err(e) = quic_stream_uni_handle(recv_stream, ip_socket,&internal_nat_manager).await{ + log::error!("quic_stream_uni_handle: {e:?}"); + } + }); + } + } + } +} + +async fn quic_stream_bi_handle( + ip_stack: Option, + mut send_stream: SendStream, + mut recv_stream: RecvStream, + internal_nat_manager: &Option, + port_mapping_manager: PortMappingManager, +) -> anyhow::Result<()> { + let handshake = recv_handshake(&mut recv_stream).await?; + let Some(handshake) = handshake.handshake else { + return Ok(()); + }; + match handshake { + Handshake::Tcp(handshake) => { + let src = SocketAddr::new( + Ipv4Addr::from(handshake.src_ip).into(), + handshake.src_port as _, + ); + let dst_ip = Ipv4Addr::from(handshake.dst_ip); + let dst = SocketAddr::new(dst_ip.into(), handshake.dst_port as _); + if src == dst { + bail!("tcp handshake failed, ip: {}", src); + } + log::debug!("accept TCP stream {src}->{dst}"); + // 如果不是网段内的,并且启用了内置nat,则直接转发 + if let Some(internal_nat_manager) = internal_nat_manager { + if internal_nat_manager.use_nat(&dst_ip) { + internal_nat_manager + .tcp_nat(recv_stream, send_stream, dst_ip, dst.port()) + .await?; + return Ok(()); + } + if internal_nat_manager.no_tun() { + return Ok(()); + } + } + if let Some(ip_stack) = ip_stack { + let stream = tcp_ip::tcp::TcpStream::bind(ip_stack, src)? + .connect_to(dst) + .await?; + let (mut tcp_w, mut tcp_r) = stream.split()?; + tokio::select! { + _ = tokio::io::copy(&mut recv_stream, &mut tcp_w) => {}, + _ = tokio::io::copy(&mut tcp_r, &mut send_stream) => {}, + } + log::debug!("accept close TCP stream {src}->{dst}"); + } + } + Handshake::Ip(_) => {} + Handshake::TcpPortMapping(handshake) => { + port_mapping_manager + .tcp_mapping( + recv_stream, + send_stream, + handshake.dst_host, + handshake.dst_port as _, + ) + .await?; + } + Handshake::UdpPortMapping(handshake) => { + port_mapping_manager + .udp_mapping( + recv_stream, + send_stream, + handshake.dst_host, + handshake.dst_port as _, + ) + .await?; + } + } + Ok(()) +} + +async fn recv_handshake(recv_stream: &mut RecvStream) -> anyhow::Result { + let len = recv_stream.read_u16().await?; + let mut buf = vec![0u8; len as usize]; + recv_stream.read_exact(&mut buf).await?; + let handshake = QuicProxyHandshake::decode(&buf[..])?; + Ok(handshake) +} +async fn quic_stream_uni_handle( + mut recv_stream: RecvStream, + ip_socket: Option>, + internal_nat_manager: &Option, +) -> anyhow::Result<()> { + let handshake = recv_handshake(&mut recv_stream).await?; + let Some(handshake) = handshake.handshake else { + return Ok(()); + }; + match handshake { + Handshake::Tcp(_) => {} + Handshake::Ip(handshake) => { + let ip_next_header_protocol = + IpNextHeaderProtocol::new(handshake.ip_next_header_protocol as _); + let src_ip = Ipv4Addr::from(handshake.src_ip); + let dest_ip = Ipv4Addr::from(handshake.dst_ip); + log::debug!("recv IP({ip_next_header_protocol}) packet {src_ip}->{dest_ip}"); + let mut framed_read = FramedRead::new(recv_stream, LengthDelimitedCodec::new()); + // 如果不是网段内的,并且启用了内置nat,则直接转发 + if let Some(internal_nat_manager) = internal_nat_manager { + if internal_nat_manager.use_nat(&dest_ip) { + loop { + let buf = framed_read + .next() + .await + .context("receive quic stream failed")??; + internal_nat_manager + .send_ipv4_payload(ip_next_header_protocol, src_ip, dest_ip, buf) + .await?; + } + } + if internal_nat_manager.no_tun() { + return Ok(()); + } + } + let Some(ip_socket) = ip_socket else { + return Ok(()); + }; + let src_ip = src_ip.into(); + let dest_ip = dest_ip.into(); + loop { + let buf = framed_read + .next() + .await + .context("receive quic stream failed")??; + + ip_socket + .send_protocol_from_to(&buf, ip_next_header_protocol, src_ip, dest_ip) + .await?; + } + } + Handshake::TcpPortMapping(_) => {} + Handshake::UdpPortMapping(_) => {} + } + Ok(()) +} diff --git a/vnt-core/src/fec/decoder.rs b/vnt-core/src/fec/decoder.rs new file mode 100644 index 0000000..576a4f5 --- /dev/null +++ b/vnt-core/src/fec/decoder.rs @@ -0,0 +1,302 @@ +use crate::fec::encoder::FecPacket; +use crate::protocol::ip_packet_protocol::{HEAD_LENGTH, NetPacket}; +use crate::protocol::transmission::TransmissionBytes; +use anyhow::{Result, bail}; +use prost::Message; +use reed_solomon_erasure::galois_8::ReedSolomon; +use std::collections::HashMap; +use std::net::Ipv4Addr; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +const GROUP_TIMEOUT: Duration = Duration::from_secs(3); +const MAX_GROUPS: usize = 1000; +const MAX_NUM: usize = 50; + +#[derive(Clone)] +pub struct FecDecoder { + inner: Arc>, +} + +struct FecDecoderInner { + groups: HashMap<(Ipv4Addr, u64), FecGroup>, + last_cleanup: Instant, +} + +struct FecGroup { + data_shards: usize, + parity_shards: usize, + received_original_count: usize, + received_shards: Vec>>, + last_update: Instant, +} +impl FecGroup { + fn is_done(&self) -> bool { + self.data_shards != 0 && self.received_original_count == self.data_shards + } + fn done(&mut self) { + self.received_original_count = self.data_shards; + self.received_shards = vec![]; + } +} +impl Default for FecGroup { + fn default() -> Self { + Self { + data_shards: 0, + parity_shards: 0, + received_original_count: 0, + received_shards: Vec::with_capacity(16), + last_update: Instant::now(), + } + } +} + +impl FecDecoder { + pub fn new() -> Self { + Self { + inner: Arc::new(parking_lot::Mutex::new(FecDecoderInner { + groups: HashMap::new(), + last_cleanup: Instant::now(), + })), + } + } + + /// 接收FEC包并尝试恢复丢失的包 + pub fn receive( + &self, + net_packet: NetPacket, + ) -> Result>>> { + let mut inner = self.inner.lock(); + let src_ip = Ipv4Addr::from(net_packet.src_id()); + let fec_packet = FecPacket::decode(net_packet.payload())?; + + let group_id = fec_packet.group_id; + let packet_index = fec_packet.packet_index as usize; + let payload = fec_packet.payload; + + if packet_index > MAX_NUM { + log::warn!( + "packet_index overflow, src={src_ip},group_id={group_id}, packet_index={packet_index}", + ); + bail!("packet_index overflow {src_ip}"); + } + let mut packet = None; + let group = inner.groups.entry((src_ip, group_id)).or_default(); + if group.is_done() { + return Ok(None); + } + if group + .received_shards + .get(packet_index) + .is_some_and(|v| v.is_some()) + { + return Ok(None); + } + if let Some(parity_data) = fec_packet.parity_data { + let data_shards = parity_data.data_shards as usize; + let parity_shards = parity_data.parity_shards as usize; + if data_shards > MAX_NUM { + bail!("data_shards overflow {src_ip}"); + } + if parity_shards > MAX_NUM { + bail!("parity_shards overflow {src_ip}"); + } + + if data_shards + parity_shards <= packet_index { + log::warn!( + "packet_index overflow in parity, src={},group_id={}, packet_index={}, total_shards={}", + src_ip, + group_id, + packet_index, + data_shards + parity_shards + ); + bail!("packet_index overflow {src_ip}"); + } + if group.data_shards != 0 && group.data_shards != data_shards { + bail!("group data_shards!=data_shards {src_ip}"); + } + if group.parity_shards != 0 && group.parity_shards != parity_shards { + bail!("group parity_shards!=parity_shards {src_ip}"); + } + group.data_shards = data_shards; + group.parity_shards = parity_shards; + if group.received_shards.len() < data_shards + parity_shards { + group + .received_shards + .resize(data_shards + parity_shards, None); + } + + group.received_shards[packet_index] = Some(payload); + } else { + let buffer = TransmissionBytes::zeroed(HEAD_LENGTH + payload.len()); + let mut result_packet = NetPacket::new(buffer)?; + result_packet.head_mut().copy_from_slice(net_packet.head()); + result_packet.set_fec_flag(false); + result_packet.set_payload(&payload)?; + packet = Some(result_packet); + + if group.received_shards.len() <= packet_index { + group.received_shards.resize(packet_index + 1, None); + } + + // 保存FEC数据: [type_byte, flags_byte, payload_len(u16), payload...] + let type_byte = net_packet.head()[0]; + let flags_byte = net_packet.head()[2]; + let mut batch_data = vec![0u8; 4 + payload.len()]; + batch_data[0] = type_byte; + batch_data[1] = flags_byte; + batch_data[2..4].copy_from_slice(&(payload.len() as u16).to_be_bytes()); + batch_data[4..].copy_from_slice(&payload); + group.received_shards[packet_index] = Some(batch_data); + group.received_original_count += 1; + } + group.last_update = Instant::now(); + + if group.is_done() { + group.done(); + if inner.last_cleanup.elapsed() > Duration::from_secs(1) { + Self::cleanup_old_groups(&mut inner.groups); + inner.last_cleanup = Instant::now(); + } + return Ok(packet.map(|v| vec![v])); + } + + let result = Self::try_decode(group, (src_ip, group_id), &net_packet)?; + + if inner.last_cleanup.elapsed() > Duration::from_secs(1) { + Self::cleanup_old_groups(&mut inner.groups); + inner.last_cleanup = Instant::now(); + } + match (packet, result) { + (Some(packet), Some(mut result)) => { + result.push(packet); + Ok(Some(result)) + } + (Some(packet), None) => Ok(Some(vec![packet])), + (None, Some(result)) => Ok(Some(result)), + (None, None) => Ok(None), + } + } + + /// 检查是否可以恢复丢失的包 + fn try_decode( + group: &mut FecGroup, + key: (Ipv4Addr, u64), + net_packet: &NetPacket, + ) -> Result>>> { + if group.data_shards == 0 { + return Ok(None); + } + if group.received_original_count == group.data_shards { + return Ok(None); + } + let received_count = group.received_shards.iter().filter(|s| s.is_some()).count(); + + if received_count < group.data_shards { + return Ok(None); + } + + Self::decode_with_rs(group, key, net_packet) + } + + /// Reed-Solomon解码恢复丢失的包 + fn decode_with_rs( + group: &mut FecGroup, + key: (Ipv4Addr, u64), + net_packet: &NetPacket, + ) -> Result>>> { + let (src_ip, group_id) = key; + + if group.received_shards.len() != group.data_shards + group.parity_shards { + bail!( + "received_shards.len()({}) != data_shards({})+parity_shards({}) src_ip={src_ip},group_id={group_id}", + group.received_shards.len(), + group.data_shards, + group.parity_shards, + ) + } + + let mut delivered_packets = vec![false; group.data_shards]; + for (index, x) in group.received_shards[..group.data_shards] + .iter() + .enumerate() + { + if x.is_some() { + delivered_packets[index] = true; + } + } + + let rs = ReedSolomon::new(group.data_shards, group.parity_shards)?; + rs.reconstruct(&mut group.received_shards)?; + + let mut result = Vec::new(); + for (i, shard) in group + .received_shards + .iter() + .enumerate() + .take(group.data_shards) + { + if let Some(shard) = shard { + if delivered_packets[i] { + continue; + } + + let net_packet = Self::rebuild_net_packet(net_packet, shard)?; + result.push(net_packet); + } + } + + group.done(); + + Ok(Some(result)) + } + + /// 从恢复的数据重建NetPacket + fn rebuild_net_packet( + current_packet: &NetPacket, + recovered_data: &[u8], + ) -> Result> { + if recovered_data.len() < 4 { + bail!("recovered_data too short"); + } + + let type_byte = recovered_data[0]; + let flags_byte = recovered_data[1]; + let payload_len = u16::from_be_bytes([recovered_data[2], recovered_data[3]]) as usize; + + if payload_len + 4 > recovered_data.len() { + bail!("invalid payload_len in recovered_data"); + } + + let payload = &recovered_data[4..4 + payload_len]; + + let buffer = TransmissionBytes::zeroed(HEAD_LENGTH + payload.len()); + let mut net_packet = NetPacket::new(buffer)?; + + net_packet.head_mut()[0] = type_byte; + net_packet.head_mut()[2] = flags_byte; + net_packet.set_src_id(current_packet.src_id()); + net_packet.set_dest_id(current_packet.dest_id()); + net_packet.set_ttl(current_packet.ttl()); + net_packet.set_fec_flag(false); + net_packet.set_payload(payload)?; + + Ok(net_packet) + } + + fn cleanup_old_groups(groups: &mut HashMap<(Ipv4Addr, u64), FecGroup>) { + let now = Instant::now(); + + groups.retain(|_, group| now.duration_since(group.last_update) < GROUP_TIMEOUT); + + if groups.len() > MAX_GROUPS { + let mut group_ids: Vec<_> = groups.iter().map(|(id, g)| (*id, g.last_update)).collect(); + group_ids.sort_by_key(|(_, last_update)| *last_update); + + let to_remove = group_ids.len() - MAX_GROUPS; + for (group_id, _) in group_ids.iter().take(to_remove) { + groups.remove(group_id); + } + } + } +} diff --git a/vnt-core/src/fec/encoder.rs b/vnt-core/src/fec/encoder.rs new file mode 100644 index 0000000..0f7eeab --- /dev/null +++ b/vnt-core/src/fec/encoder.rs @@ -0,0 +1,247 @@ +use crate::protocol::ip_packet_protocol::{HEAD_LENGTH, MsgType, NetPacket}; +use crate::protocol::transmission::TransmissionBytes; +use crate::tunnel_core::outbound::BasicOutbound; +use anyhow::{Result, bail}; +use parking_lot::Mutex; +use prost::Message; +use reed_solomon_erasure::galois_8::ReedSolomon; +use std::collections::HashMap; +use std::net::Ipv4Addr; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::sync::mpsc; + +mod fec_proto { + include!(concat!(env!("OUT_DIR"), "/protocol.fec.rs")); +} +use crate::utils::task_control::TaskGroup; +pub use fec_proto::FecPacket; + +const BATCH_SIZE: usize = 10; +const REDUNDANCY_RATE: f32 = 0.2; +const BATCH_TIMEOUT_MS: u64 = 20; +const MIN_PARITY: usize = 1; +const BATCH_CHANNEL_SIZE: usize = 1024; + +#[derive(Clone)] +pub struct FecEncoder { + batch_states: Arc>>, + batch_tx: mpsc::Sender<(Ipv4Addr, Ipv4Addr, u64, Vec)>, +} + +struct DestBatchState { + group_id: u64, + current_batch: Vec, + deadline: Instant, + src_ip: Ipv4Addr, +} + +impl FecEncoder { + pub fn new(task_group: &TaskGroup, basic_outbound: BasicOutbound) -> Self { + let (batch_tx, batch_rx) = mpsc::channel(BATCH_CHANNEL_SIZE); + let batch_states = Arc::new(Mutex::new(HashMap::new())); + let encoder = Self { + batch_states: batch_states.clone(), + batch_tx, + }; + + task_group.spawn(fec_encoder_worker(batch_rx, basic_outbound, batch_states)); + + encoder + } + + /// 将数据包加入FEC批次并返回包装后的包 + pub fn encode( + &self, + mut packet: NetPacket, + ) -> Result> { + let src_ip = Ipv4Addr::from(packet.src_id()); + let dest = Ipv4Addr::from(packet.dest_id()); + if packet.payload().len() > u16::MAX as usize { + bail!("Payload too big"); + } + let original_payload = packet.payload().to_vec(); + let original_payload_len = original_payload.len(); + + let type_byte = packet.head()[0]; + let flags_byte = packet.head()[2]; + + // 组装FEC数据: [type_byte, flags_byte, payload_len(u16), payload...] + let batch_len = 4 + original_payload_len; + let mut batch_buffer = TransmissionBytes::zeroed(batch_len); + batch_buffer[0] = type_byte; + batch_buffer[1] = flags_byte; + batch_buffer[2..4].copy_from_slice(&(original_payload_len as u16).to_be_bytes()); + batch_buffer[4..batch_len].copy_from_slice(&original_payload); + + let (group_id, packet_index) = { + let mut states = self.batch_states.lock(); + let state = states.entry(dest).or_insert_with(|| DestBatchState { + group_id: 0, + current_batch: Vec::with_capacity(BATCH_SIZE), + deadline: Instant::now() + Duration::from_millis(BATCH_TIMEOUT_MS), + src_ip, + }); + + let group_id = state.group_id; + let packet_index = state.current_batch.len(); + + state.current_batch.push(batch_buffer); + + if state.current_batch.len() >= BATCH_SIZE { + let batch = std::mem::take(&mut state.current_batch); + state.group_id += 1; + state.deadline = Instant::now() + Duration::from_millis(BATCH_TIMEOUT_MS); + + if self + .batch_tx + .try_send((src_ip, dest, group_id, batch)) + .is_err() + { + log::warn!( + "failed to send batch to worker (channel full), dest={}, group_id={}", + dest, + group_id + ); + } + } + + (group_id, packet_index) + }; + + let fec_packet = FecPacket { + group_id, + packet_index: packet_index as u32, + payload: original_payload, + parity_data: None, + }; + + let fec_payload = fec_packet.encode_to_vec(); + packet + .source_buf_mut() + .resize(HEAD_LENGTH + fec_payload.len(), 0); + packet.set_payload(&fec_payload)?; + packet.set_fec_flag(true); + + Ok(packet) + } +} + +/// 后台worker,处理满批次和超时批次 +async fn fec_encoder_worker( + mut batch_rx: mpsc::Receiver<(Ipv4Addr, Ipv4Addr, u64, Vec)>, + basic_outbound: BasicOutbound, + batch_states: Arc>>, +) { + let mut timer = tokio::time::interval(Duration::from_millis(5)); + + loop { + tokio::select! { + Some((src,dest, group_id, mut items)) = batch_rx.recv() => { + if let Err(e) = encode_and_send_parity(src,dest, group_id, &mut items, &basic_outbound).await { + log::warn!("encode_and_send_parity error for {} group {}: {:?}", dest, group_id, e); + } + } + + _ = timer.tick() => { + let now = Instant::now(); + + let timeout_batches = { + let mut states = batch_states.lock(); + let mut batches = Vec::new(); + for (dest, state) in states.iter_mut() { + if !state.current_batch.is_empty() && now >= state.deadline { + let items = std::mem::take(&mut state.current_batch); + let group_id = state.group_id; + state.group_id += 1; + state.deadline = Instant::now() + Duration::from_millis(BATCH_TIMEOUT_MS); + batches.push((state.src_ip,*dest, group_id, items)); + } + } + batches + }; + + for (src,dest, group_id, mut items) in timeout_batches { + if let Err(e) = encode_and_send_parity(src, dest, group_id, &mut items, &basic_outbound).await { + log::warn!("encode_and_send_parity timeout error for {} group {}: {:?}", dest, group_id, e); + } + } + } + } + } +} + +/// Reed-Solomon编码并发送冗余包 +async fn encode_and_send_parity( + src: Ipv4Addr, + dest: Ipv4Addr, + group_id: u64, + items: &mut Vec, + basic_outbound: &BasicOutbound, +) -> Result<()> { + if items.is_empty() { + return Ok(()); + } + + let data_shards = items.len(); + let parity_shards = (data_shards as f32 * REDUNDANCY_RATE).ceil() as usize; + let parity_shards = parity_shards.max(MIN_PARITY); + + let max_len = items.iter().map(|buf| buf.len()).max().unwrap_or(0); + + if max_len == 0 { + log::warn!("max_len is 0, dest={}, group_id={}", dest, group_id); + return Ok(()); + } + + for buf in items.iter_mut() { + if buf.len() < max_len { + let padding = max_len - buf.len(); + buf.extend_end(padding); + } + } + + for _ in 0..parity_shards { + items.push(TransmissionBytes::zeroed(max_len)); + } + + let rs = ReedSolomon::new(data_shards, parity_shards)?; + let mut shard_refs: Vec<&mut [u8]> = items.iter_mut().map(|buf| buf.as_mut()).collect(); + rs.encode(&mut shard_refs)?; + + for (i, parity_buf) in items[data_shards..].iter().enumerate() { + let packet_index = (data_shards + i) as u32; + let fec_packet = FecPacket { + group_id, + packet_index, + payload: parity_buf.as_ref().to_vec(), + parity_data: Some(fec_proto::ParityData { + data_shards: data_shards as u32, + parity_shards: parity_shards as u32, + }), + }; + + let fec_payload = fec_packet.encode_to_vec(); + + let buffer = TransmissionBytes::zeroed(HEAD_LENGTH + fec_payload.len()); + let mut net_packet = NetPacket::new(buffer)?; + net_packet.set_msg_type(MsgType::Turn); + net_packet.set_src_id(src.into()); + net_packet.set_dest_id(dest.into()); + net_packet.set_ttl(5); + net_packet.set_payload(&fec_payload)?; + net_packet.set_fec_flag(true); + + if let Err(e) = basic_outbound.send_encrypted_packet(dest, net_packet).await { + log::warn!( + "failed to send parity packet {}: {:?}, dest={}, group_id={}", + packet_index, + e, + dest, + group_id + ); + } + } + + Ok(()) +} diff --git a/vnt-core/src/fec/mod.rs b/vnt-core/src/fec/mod.rs new file mode 100644 index 0000000..5640696 --- /dev/null +++ b/vnt-core/src/fec/mod.rs @@ -0,0 +1,5 @@ +mod decoder; +mod encoder; + +pub(crate) use decoder::FecDecoder; +pub(crate) use encoder::FecEncoder; diff --git a/vnt-core/src/lib.rs b/vnt-core/src/lib.rs new file mode 100644 index 0000000..33f4c45 --- /dev/null +++ b/vnt-core/src/lib.rs @@ -0,0 +1,15 @@ +pub(crate) mod compression; +pub mod context; +pub mod core; +pub mod crypto; +pub(crate) mod fec; +pub mod nat; +pub mod protocol; +pub mod tls; +pub(crate) mod tun; +pub mod tunnel_core; +pub mod utils; + +pub mod api; +pub(crate) mod enhanced_tunnel; +pub mod port_mapping; diff --git a/vnt-core/src/nat/internal_nat/icmp_nat.rs b/vnt-core/src/nat/internal_nat/icmp_nat.rs new file mode 100644 index 0000000..826895f --- /dev/null +++ b/vnt-core/src/nat/internal_nat/icmp_nat.rs @@ -0,0 +1,139 @@ +use crate::context::SharedNetworkAddr; +use crate::utils::task_control::TaskGroup; +use anyhow::Context; +use pnet_packet::Packet; +use pnet_packet::icmp::echo_reply::{Identifier, SequenceNumber}; +use pnet_packet::icmp::{IcmpPacket, IcmpTypes}; +use pnet_packet::ipv4::Ipv4Packet; +use std::collections::HashMap; +use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}; +use tcp_ip::IpStack; +use tcp_ip::icmp::IcmpSocket; +use tokio::net::UdpSocket; + +pub async fn start_icmp_nat( + task_group: &TaskGroup, + ip_stack: &IpStack, + no_tun: bool, + network: SharedNetworkAddr, +) -> anyhow::Result<()> { + let net_icmp_socket = socket2::Socket::new( + socket2::Domain::IPV4, + socket2::Type::RAW, + Some(socket2::Protocol::ICMPV4), + ) + .context("new Socket RAW ICMPV4 failed")?; + let addr: SocketAddrV4 = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0); + net_icmp_socket + .bind(&socket2::SockAddr::from(addr)) + .context("bind Socket ICMPV4 failed")?; + net_icmp_socket.set_nonblocking(true)?; + + let std_socket: std::net::UdpSocket = net_icmp_socket.into(); + + let tokio_icmp_socket = UdpSocket::from_std(std_socket)?; + + let inner_icmp_socket = IcmpSocket::bind_all(ip_stack.clone()).await?; + task_group.spawn(async move { + if let Err(e) = task(tokio_icmp_socket, inner_icmp_socket, no_tun, network).await { + log::error!("icmp task failed: {:?}", e); + } + }); + Ok(()) +} +async fn task( + tokio_icmp_socket: UdpSocket, + inner_icmp_socket: IcmpSocket, + no_tun: bool, + network: SharedNetworkAddr, +) -> anyhow::Result<()> { + let mut buf1 = vec![0u8; 65536]; + let mut buf2 = vec![0u8; 65536]; + let mut map = HashMap::new(); + loop { + tokio::select! { + rs = tokio_icmp_socket.recv(&mut buf1) => { + let len = rs?; + tokio_icmp_socket_recv(&buf1[..len],&inner_icmp_socket,&map,no_tun,&network).await?; + } + rs = inner_icmp_socket.recv_from_to(&mut buf2) => { + let (len,src,dst) = rs?; + inner_icmp_socket_recv(&buf2[..len],src,dst,&tokio_icmp_socket,&mut map,no_tun,&network).await?; + } + } + } +} +async fn tokio_icmp_socket_recv( + buf: &[u8], + inner_icmp_socket: &IcmpSocket, + map: &HashMap<(Ipv4Addr, Identifier, SequenceNumber), Ipv4Addr>, + no_tun: bool, + network: &SharedNetworkAddr, +) -> anyhow::Result<()> { + let Some(ipv4) = Ipv4Packet::new(buf) else { + return Ok(()); + }; + let Some(icmp) = IcmpPacket::new(ipv4.payload()) else { + return Ok(()); + }; + if icmp.get_icmp_type() != IcmpTypes::EchoReply + && icmp.get_icmp_type() != IcmpTypes::EchoRequest + { + return Ok(()); + } + let payload = icmp.payload(); + if payload.len() < 4 { + return Ok(()); + } + let mut src = ipv4.get_source(); + let identifier = Identifier::new(u16::from_be_bytes([payload[0], payload[1]])); + let sequence_number = SequenceNumber::new(u16::from_be_bytes([payload[2], payload[3]])); + let Some(dst) = map.get(&(src, identifier, sequence_number)) else { + return Ok(()); + }; + if no_tun && src == Ipv4Addr::LOCALHOST { + src = network.ip().context("not ip")?; + } + + inner_icmp_socket + .send_from_to(ipv4.payload(), src.into(), (*dst).into()) + .await + .context("sending ICMPv4 failed")?; + Ok(()) +} +async fn inner_icmp_socket_recv( + buf: &[u8], + src: IpAddr, + dst: IpAddr, + tokio_icmp_socket: &UdpSocket, + map: &mut HashMap<(Ipv4Addr, Identifier, SequenceNumber), Ipv4Addr>, + no_tun: bool, + network: &SharedNetworkAddr, +) -> anyhow::Result<()> { + let (IpAddr::V4(src), IpAddr::V4(mut dst)) = (src, dst) else { + return Ok(()); + }; + let Some(icmp) = IcmpPacket::new(buf) else { + return Ok(()); + }; + if icmp.get_icmp_type() != IcmpTypes::EchoReply + && icmp.get_icmp_type() != IcmpTypes::EchoRequest + { + return Ok(()); + } + let payload = icmp.payload(); + if payload.len() < 4 { + return Ok(()); + } + if no_tun && dst == network.ip().context("not ip")? { + dst = Ipv4Addr::LOCALHOST; + } + + let identifier = Identifier::new(u16::from_be_bytes([payload[0], payload[1]])); + let sequence_number = SequenceNumber::new(u16::from_be_bytes([payload[2], payload[3]])); + map.insert((dst, identifier, sequence_number), src); + tokio_icmp_socket + .send_to(buf, SocketAddr::new(dst.into(), 0)) + .await?; + Ok(()) +} diff --git a/vnt-core/src/nat/internal_nat/mod.rs b/vnt-core/src/nat/internal_nat/mod.rs new file mode 100644 index 0000000..ffcfa3d --- /dev/null +++ b/vnt-core/src/nat/internal_nat/mod.rs @@ -0,0 +1,225 @@ +use crate::context::{NetworkAddr, SharedNetworkAddr}; +use crate::nat::AllowSubnetExternalRoute; +use crate::protocol::ip_packet_protocol::HEAD_LENGTH; +use crate::protocol::transmission::TransmissionBytes; +use crate::tunnel_core::outbound::HybridOutbound; +use crate::utils::task_control::TaskGroup; +use anyhow::Context; +use bytes::BytesMut; +use pnet_packet::ip::IpNextHeaderProtocol; +use pnet_packet::ipv4::Ipv4Packet; +use std::net::{Ipv4Addr, SocketAddr}; +use std::str::FromStr; +use std::sync::Arc; +use tcp_ip::{IpStackConfig, IpStackRecv, IpStackSend}; +use tokio::io::{AsyncRead, AsyncWrite}; + +#[cfg(not(target_os = "android"))] +mod icmp_nat; +mod tcp_nat; +mod udp_nat; +#[derive(Clone)] +pub(crate) struct InternalNatInbound { + no_tun: bool, + ip_stack_send: Arc, + allow_subnet: AllowSubnetExternalRoute, + network: SharedNetworkAddr, +} +impl InternalNatInbound { + pub async fn create( + task_group: &TaskGroup, + mtu: u16, + hybrid_outbound: HybridOutbound, + allow_subnet: AllowSubnetExternalRoute, + network: SharedNetworkAddr, + no_tun: bool, + ) -> anyhow::Result { + let ip_stack_config = IpStackConfig { + mtu, + ..Default::default() + }; + let (ip_stack, ip_stack_send, ip_stack_recv) = tcp_ip::ip_stack(ip_stack_config)?; + #[cfg(not(target_os = "android"))] + icmp_nat::start_icmp_nat(task_group, &ip_stack, no_tun, network.clone()).await?; + tcp_nat::start_tcp_nat(task_group, &ip_stack, no_tun, network.clone()).await?; + udp_nat::start_udp_nat(task_group, &ip_stack).await?; + task_group.spawn(async move { + if let Err(e) = ip_stack_recv_task(ip_stack_recv, hybrid_outbound).await { + log::error!("ip stack recv task error: {e:?}"); + } + }); + Ok(Self { + no_tun, + ip_stack_send: Arc::new(ip_stack_send), + allow_subnet, + network, + }) + } + pub async fn send(&self, data: &[u8], net: &NetworkAddr) -> anyhow::Result<()> { + if data[0] >> 4 != 4 { + return Ok(()); + } + let Some(ipv4) = Ipv4Packet::new(data) else { + return Ok(()); + }; + let dest = ipv4.get_destination(); + if net.network().contains(&dest) + || dest == net.broadcast + || dest.is_broadcast() + || dest.is_multicast() + || self.allow_subnet.allow(&dest) + { + self.ip_stack_send.send_ip_packet(data).await?; + } + Ok(()) + } + pub async fn send_ipv4_payload( + &self, + protocol: IpNextHeaderProtocol, + src_ip: Ipv4Addr, + dest_ip: Ipv4Addr, + payload: BytesMut, + ) -> anyhow::Result<()> { + self.ip_stack_send + .send_ipv4_payload(protocol, src_ip, dest_ip, payload) + .await?; + Ok(()) + } +} + +async fn ip_stack_recv_task( + mut ip_stack_recv: IpStackRecv, + hybrid_outbound: HybridOutbound, +) -> anyhow::Result<()> { + loop { + let mut bytes = TransmissionBytes::new_offset_zeroed(HEAD_LENGTH); + let len = ip_stack_recv.recv(&mut bytes).await?; + bytes.set_len(len)?; + if let Err(e) = hybrid_outbound.ipv4_outbound_common(bytes).await { + log::warn!("ip_stack_recv_task,{e:?}"); + } + } +} + +impl InternalNatInbound { + fn network_contains(&self, ip: &Ipv4Addr) -> bool { + self.network + .network() + .map(|net| net.contains(ip)) + .unwrap_or(false) + } + pub fn use_nat(&self, dst: &Ipv4Addr) -> bool { + if self.no_tun { + return self.allow_nat(dst); + } + if self.network_contains(dst) { + return false; + } + self.allow_subnet.allow(dst) + } + pub fn no_tun(&self) -> bool { + self.no_tun + } + pub fn allow_nat(&self, dst: &Ipv4Addr) -> bool { + self.allow_subnet.allow(dst) || self.network_contains(dst) + } + pub async fn tcp_nat( + &self, + recv_stream: R, + send_stream: W, + mut dest_ip: Ipv4Addr, + dest_port: u16, + ) -> anyhow::Result<()> + where + R: AsyncRead + Unpin, + W: AsyncWrite + Unpin, + { + if self.no_tun { + let net = self.network.get().context("no network")?; + if dest_ip == net.ip { + dest_ip = Ipv4Addr::LOCALHOST; + } else if net.network().contains(&dest_ip) { + return Ok(()); + } + } + let dst = SocketAddr::new(dest_ip.into(), dest_port); + tcp_nat::stream_nat(recv_stream, send_stream, dst).await + } +} + +#[derive(Clone)] +pub(crate) struct PortMappingManager { + no_tun: bool, + allow_port_mapping: bool, + network: SharedNetworkAddr, +} + +impl PortMappingManager { + pub fn new(no_tun: bool, allow_port_mapping: bool, network: SharedNetworkAddr) -> Self { + Self { + no_tun, + allow_port_mapping, + network, + } + } + pub async fn tcp_mapping( + &self, + recv_stream: R, + send_stream: W, + dest: String, + dest_port: u16, + ) -> anyhow::Result<()> + where + R: AsyncRead + Unpin, + W: AsyncWrite + Unpin, + { + if !self.allow_port_mapping { + log::debug!("port mapping not enabled"); + return Ok(()); + } + if self.no_tun + && let Ok(dest_ip) = Ipv4Addr::from_str(&dest) + { + let net = self.network.get().context("no network")?; + + if dest_ip == net.ip { + let dst = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), dest_port); + return tcp_nat::stream_nat(recv_stream, send_stream, dst).await; + } else if net.network().contains(&dest_ip) { + return Ok(()); + } + } + let dst = format!("{}:{}", dest, dest_port); + tcp_nat::stream_nat(recv_stream, send_stream, dst).await + } + pub async fn udp_mapping( + &self, + recv_stream: R, + send_stream: W, + dest: String, + dest_port: u16, + ) -> anyhow::Result<()> + where + R: AsyncRead + Unpin, + W: AsyncWrite + Unpin, + { + if !self.allow_port_mapping { + log::debug!("port mapping not enabled"); + return Ok(()); + } + if self.no_tun + && let Ok(dest_ip) = Ipv4Addr::from_str(&dest) + { + let net = self.network.get().context("no network")?; + + if dest_ip == net.ip { + let dst = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), dest_port); + return udp_nat::stream_nat(recv_stream, send_stream, dst).await; + } else if net.network().contains(&dest_ip) { + return Ok(()); + } + } + let dst = format!("{}:{}", dest, dest_port); + udp_nat::stream_nat(recv_stream, send_stream, dst).await + } +} diff --git a/vnt-core/src/nat/internal_nat/tcp_nat.rs b/vnt-core/src/nat/internal_nat/tcp_nat.rs new file mode 100644 index 0000000..3382985 --- /dev/null +++ b/vnt-core/src/nat/internal_nat/tcp_nat.rs @@ -0,0 +1,81 @@ +use crate::context::SharedNetworkAddr; +use crate::utils::task_control::TaskGroup; +use anyhow::Context; +use std::fmt::Debug; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use tcp_ip::IpStack; +use tcp_ip::tcp::TcpListener; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::net::{TcpStream, ToSocketAddrs}; + +pub async fn start_tcp_nat( + task_group: &TaskGroup, + ip_stack: &IpStack, + no_tun: bool, + network: SharedNetworkAddr, +) -> anyhow::Result<()> { + let tcp_listener = TcpListener::bind_all(ip_stack.clone()).await?; + let group = task_group.clone(); + task_group.spawn(async move { + if let Err(e) = listen_task(&group, tcp_listener, no_tun, network).await { + log::error!("listen task error: {:?}", e); + } + }); + Ok(()) +} + +async fn listen_task( + task_group: &TaskGroup, + mut tcp_listener: TcpListener, + no_tun: bool, + network: SharedNetworkAddr, +) -> anyhow::Result<()> { + loop { + let (stream, _addr) = tcp_listener.accept().await?; + let mut local_addr = stream.local_addr()?; + let peer_addr = stream.peer_addr()?; + if no_tun { + let IpAddr::V4(ip) = local_addr.ip() else { + continue; + }; + if ip == network.ip().context("not ip")? { + // 无tun的情况下写入本机的则写到localhost + local_addr.set_ip(IpAddr::V4(Ipv4Addr::LOCALHOST)); + } + } + task_group.spawn(async move { + if let Err(e) = stream_task(stream, local_addr).await { + log::error!("stream task Error: {:?},{peer_addr}->{local_addr}", e); + } + }); + } +} + +async fn stream_task( + mut inner_stream: tcp_ip::tcp::TcpStream, + addr: SocketAddr, +) -> anyhow::Result<()> { + let mut tokio_stream = TcpStream::connect(addr).await?; + tokio::io::copy_bidirectional(&mut inner_stream, &mut tokio_stream).await?; + Ok(()) +} + +pub(crate) async fn stream_nat( + mut recv_stream: R, + mut send_stream: W, + addr: A, +) -> anyhow::Result<()> +where + R: AsyncRead + Unpin, + W: AsyncWrite + Unpin, +{ + let mut tokio_stream = TcpStream::connect(&addr) + .await + .with_context(|| format!("error connecting to {:?}", addr))?; + let (mut tcp_r, mut tcp_w) = tokio_stream.split(); + tokio::select! { + _ = tokio::io::copy(&mut recv_stream, &mut tcp_w) => {}, + _ = tokio::io::copy(&mut tcp_r, &mut send_stream) => {}, + } + Ok(()) +} diff --git a/vnt-core/src/nat/internal_nat/udp_nat.rs b/vnt-core/src/nat/internal_nat/udp_nat.rs new file mode 100644 index 0000000..1d4d2a6 --- /dev/null +++ b/vnt-core/src/nat/internal_nat/udp_nat.rs @@ -0,0 +1,188 @@ +use crate::utils::task_control::TaskGroup; +use anyhow::Context; +use bytes::Bytes; +use futures::{SinkExt, StreamExt}; +use std::collections::HashMap; +use std::fmt::Debug; +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tcp_ip::IpStack; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::net::ToSocketAddrs; +use tokio::sync::Mutex; +use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec}; + +struct NatEntry { + socket: Arc, + last_active: Instant, +} + +type NatTable = Arc>>; + +const NAT_IDLE_TIMEOUT: Duration = Duration::from_secs(60 * 5); +const NAT_GC_INTERVAL: Duration = Duration::from_secs(60); + +pub async fn start_udp_nat(task_group: &TaskGroup, ip_stack: &IpStack) -> anyhow::Result<()> { + let inner_socket = tcp_ip::udp::UdpSocket::bind_all(ip_stack.clone()).await?; + let inner_socket = Arc::new(inner_socket); + let nat_table: NatTable = Arc::new(Mutex::new(HashMap::new())); + let mut buf = vec![0u8; 65536]; + let group = task_group.clone(); + let nat_table_clone = nat_table.clone(); + task_group.spawn(async move { + loop { + let (len, src, dst) = match inner_socket.recv_from_to(&mut buf).await { + Ok(rs) => rs, + Err(e) => { + log::warn!("{e:?}"); + break; + } + }; + + if let Err(e) = + handle_outbound(&group, &inner_socket, &nat_table, src, dst, &buf[..len]).await + { + log::warn!("udp nat outbound error: {e:?}"); + } + } + }); + spawn_nat_gc(task_group, nat_table_clone); + Ok(()) +} + +async fn handle_outbound( + task_group: &TaskGroup, + inner: &Arc, + nat: &NatTable, + src: SocketAddr, + dst: SocketAddr, + packet: &[u8], +) -> anyhow::Result<()> { + let key = (src, dst); + + let socket = { + let mut table = nat.lock().await; + if let Some(entry) = table.get_mut(&key) { + entry.last_active = Instant::now(); + entry.socket.clone() + } else { + // 创建真实 UDP socket + let sock = tokio::net::UdpSocket::bind("0.0.0.0:0").await?; + sock.connect(dst).await?; + let sock = Arc::new(sock); + table.insert( + key, + NatEntry { + socket: sock.clone(), + last_active: Instant::now(), + }, + ); + + // 启动反向转发 + spawn_inbound( + task_group, + inner.clone(), + nat.clone(), + src, + dst, + sock.clone(), + ); + + sock + } + }; + + socket.send(packet).await?; + Ok(()) +} + +fn spawn_inbound( + task_group: &TaskGroup, + inner: Arc, + nat: NatTable, + src: SocketAddr, + dst: SocketAddr, + socket: Arc, +) { + task_group.spawn(async move { + let mut buf = vec![0u8; 65536]; + + loop { + let len = match socket.recv(&mut buf).await { + Ok(n) => n, + Err(_) => break, + }; + + // 反向写回 inner socket + if inner.send_from_to(&buf[..len], dst, src).await.is_err() { + break; + } + + // 更新活跃时间 + if let Some(entry) = nat.lock().await.get_mut(&(src, dst)) { + entry.last_active = Instant::now(); + } + } + + // 回收 NAT + nat.lock().await.remove(&(src, dst)); + }); +} + +fn spawn_nat_gc(task_group: &TaskGroup, nat: NatTable) { + task_group.spawn(async move { + let mut interval = tokio::time::interval(NAT_GC_INTERVAL); + + loop { + interval.tick().await; + + let now = Instant::now(); + let mut table = nat.lock().await; + + table.retain(|(src, dst), entry| { + let alive = now.duration_since(entry.last_active) < NAT_IDLE_TIMEOUT; + if !alive { + log::debug!("udp nat expired: {} -> {}", src, dst); + } + alive + }); + } + }); +} + +pub(crate) async fn stream_nat( + recv_stream: R, + send_stream: W, + addr: A, +) -> anyhow::Result<()> +where + R: AsyncRead + Unpin, + W: AsyncWrite + Unpin, +{ + let udp_socket = tokio::net::UdpSocket::bind("0.0.0.0:0").await?; + udp_socket + .connect(&addr) + .await + .with_context(|| format!("error connecting to {:?}", addr))?; + let mut framed_read = FramedRead::new(recv_stream, LengthDelimitedCodec::new()); + let mut framed_write = FramedWrite::new(send_stream, LengthDelimitedCodec::new()); + let mut buf = vec![0u8; 65536]; + + loop { + tokio::select! { + Some(buf) = framed_read.next() => { + let buf = buf?; + udp_socket.send(&buf).await?; + }, + rs = udp_socket.recv(&mut buf) =>{ + let len = rs?; + framed_write.send(Bytes::copy_from_slice(&buf[..len])).await?; + }, + else => { + break + } + } + } + Ok(()) +} diff --git a/vnt-core/src/nat/mod.rs b/vnt-core/src/nat/mod.rs new file mode 100644 index 0000000..cf8d69e --- /dev/null +++ b/vnt-core/src/nat/mod.rs @@ -0,0 +1,114 @@ +use ipnet::Ipv4Net; +use parking_lot::Mutex; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt; +use std::net::Ipv4Addr; +use std::str::FromStr; +use std::sync::Arc; + +pub(crate) mod internal_nat; + +#[derive(Clone, Debug)] +pub struct NetInput { + pub net: Ipv4Net, + pub target_ip: Ipv4Addr, +} +impl FromStr for NetInput { + type Err = String; + + fn from_str(s: &str) -> Result { + let parts: Vec<&str> = s.split(',').map(|x| x.trim()).collect(); + if parts.len() != 2 { + return Err("格式错误,应为 net,target_ip 例如: 192.168.0.0/24,10.26.0.2".into()); + } + + let net = Ipv4Net::from_str(parts[0]).map_err(|e| format!("网络段格式错误: {}", e))?; + + let target_ip = + Ipv4Addr::from_str(parts[1]).map_err(|e| format!("目标 IP 格式错误: {}", e))?; + + Ok(NetInput { net, target_ip }) + } +} +impl fmt::Display for NetInput { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{},{}", self.net, self.target_ip) + } +} +impl Serialize for NetInput { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for NetInput { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + s.parse().map_err(serde::de::Error::custom) + } +} + +#[derive(Clone, Default)] +pub struct SubnetExternalRoute { + route_table: Arc>>, +} +impl SubnetExternalRoute { + pub fn new(mut route_table: Vec) -> Self { + route_table.sort_by_key(|r| std::cmp::Reverse(r.net.prefix_len())); + SubnetExternalRoute { + route_table: Arc::new(Mutex::new(route_table)), + } + } + pub fn set_route_table(&self, mut route_table: Vec) { + route_table.sort_by_key(|r| std::cmp::Reverse(r.net.prefix_len())); + *self.route_table.lock() = route_table; + } + pub fn route(&self, ip: &Ipv4Addr) -> Option { + let route_table = self.route_table.lock(); + if route_table.is_empty() { + return None; + } + for net in route_table.iter() { + if net.net.contains(ip) { + return Some(net.target_ip); + } + } + None + } + pub fn all_route(&self) -> Vec { + self.route_table.lock().clone() + } + pub fn reset_route(&self, route_table: Vec) { + *self.route_table.lock() = route_table; + } +} + +#[derive(Clone)] +pub struct AllowSubnetExternalRoute { + route_table: Arc>, +} +impl AllowSubnetExternalRoute { + pub fn new(mut route_table: Vec) -> Self { + route_table.sort_by_key(|r| r.prefix_len()); + Self { + route_table: Arc::new(route_table), + } + } + pub fn allow(&self, ip: &Ipv4Addr) -> bool { + if self.route_table.is_empty() { + return false; + } + for net in self.route_table.iter() { + if net.contains(ip) { + return true; + } + } + false + } +} diff --git a/vnt-core/src/port_mapping/mod.rs b/vnt-core/src/port_mapping/mod.rs new file mode 100644 index 0000000..5694310 --- /dev/null +++ b/vnt-core/src/port_mapping/mod.rs @@ -0,0 +1,100 @@ +use crate::enhanced_tunnel::quic_over::quic_client::QuicTunnelClient; +use crate::utils::task_control::TaskGroup; +use pnet_packet::ip::{IpNextHeaderProtocol, IpNextHeaderProtocols}; +use std::fmt; +use std::net::{Ipv4Addr, SocketAddr}; +use std::str::FromStr; + +pub(crate) mod tcp_port_mapping; +pub(crate) mod udp_port_mapping; + +pub(crate) async fn port_mapping_start( + task_group: &TaskGroup, + list: Vec, + quic_tunnel_client: QuicTunnelClient, +) -> anyhow::Result<()> { + tcp_port_mapping::start(task_group, &list, quic_tunnel_client.clone()).await?; + udp_port_mapping::start(task_group, &list, quic_tunnel_client).await?; + Ok(()) +} +#[derive(Debug, Clone)] +pub struct PortMapping { + pub protocol: IpNextHeaderProtocol, + pub src_addr: SocketAddr, + pub virtual_target_ip: Ipv4Addr, + pub dst_host: String, + pub dst_port: u16, +} + +impl fmt::Display for PortMapping { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}://{}-{}-{}:{}", + protocol_to_str(self.protocol), + self.src_addr, + self.virtual_target_ip, + self.dst_host, + self.dst_port + ) + } +} + +impl FromStr for PortMapping { + type Err = String; + + fn from_str(s: &str) -> Result { + let (proto_str, rest) = s.split_once("://").ok_or("missing '://'")?; + + let protocol = + str_to_protocol(proto_str).ok_or_else(|| format!("unknown protocol: {}", proto_str))?; + + let mut parts = rest.splitn(3, '-'); + + let src_addr = parts + .next() + .ok_or("missing src_addr")? + .parse::() + .map_err(|e| format!("invalid src_addr: {}", e))?; + + let virtual_target_ip = parts + .next() + .ok_or("missing virtual_target_ip")? + .parse::() + .map_err(|e| format!("invalid virtual_target_ip: {}", e))?; + + let dst = parts.next().ok_or("missing destination")?; + + let (dst_host, dst_port) = dst.rsplit_once(':').ok_or("missing dst port")?; + + let dst_port = dst_port + .parse::() + .map_err(|e| format!("invalid dst_port: {}", e))?; + if dst_port == 0 { + return Err("invalid dst port: 0".to_string()); + } + Ok(Self { + protocol, + src_addr, + virtual_target_ip, + dst_host: dst_host.to_string(), + dst_port, + }) + } +} + +fn protocol_to_str(p: IpNextHeaderProtocol) -> &'static str { + match p { + IpNextHeaderProtocols::Tcp => "tcp", + IpNextHeaderProtocols::Udp => "udp", + _ => "unknown", + } +} + +fn str_to_protocol(s: &str) -> Option { + match s.to_ascii_lowercase().as_str() { + "tcp" => Some(IpNextHeaderProtocols::Tcp), + "udp" => Some(IpNextHeaderProtocols::Udp), + _ => None, + } +} diff --git a/vnt-core/src/port_mapping/tcp_port_mapping.rs b/vnt-core/src/port_mapping/tcp_port_mapping.rs new file mode 100644 index 0000000..518a16e --- /dev/null +++ b/vnt-core/src/port_mapping/tcp_port_mapping.rs @@ -0,0 +1,85 @@ +use crate::enhanced_tunnel::quic_over::quic_client::{QuicTunnelClient, send_handshake}; +use crate::port_mapping::PortMapping; +use crate::protocol::client_message::{ + PortProxyHandshake, QuicProxyHandshake, quic_proxy_handshake, +}; +use crate::utils::task_control::TaskGroup; +use anyhow::Context; +use pnet_packet::ip::IpNextHeaderProtocols; +use std::net::{Ipv4Addr, SocketAddr}; +use tokio::net::{TcpListener, TcpStream}; + +pub async fn start( + task_group: &TaskGroup, + list: &Vec, + quic_tunnel_client: QuicTunnelClient, +) -> anyhow::Result<()> { + for x in list { + if x.protocol != IpNextHeaderProtocols::Tcp { + continue; + } + log::info!("Starting TCP port mapping on {}", x); + let listener = TcpListener::bind(x.src_addr) + .await + .with_context(|| format!("Tcp port mapping Failed to bind to {}", x.src_addr))?; + let group = task_group.clone(); + let tunnel_client = quic_tunnel_client.clone(); + let mapping = x.clone(); + task_group.spawn(async move { + if let Err(e) = listen(&group, listener, &mapping, tunnel_client).await { + log::error!("listen {:?},mapping:{mapping}", e); + } + }); + } + Ok(()) +} + +async fn listen( + task_group: &TaskGroup, + listener: TcpListener, + mapping: &PortMapping, + quic_tunnel_client: QuicTunnelClient, +) -> anyhow::Result<()> { + loop { + let (stream, addr) = listener.accept().await?; + let tunnel_client = quic_tunnel_client.clone(); + let target_ip = mapping.virtual_target_ip; + let dst_host = mapping.dst_host.clone(); + let dst_port = mapping.dst_port; + task_group.spawn(async move { + if let Err(e) = + stream_copy(stream, addr, target_ip, dst_host, dst_port, tunnel_client).await + { + log::error!("TCP TCP Stream Error: {:?}", e); + } + }); + } +} + +async fn stream_copy( + mut tcp_stream: TcpStream, + src: SocketAddr, + target_ip: Ipv4Addr, + dst_host: String, + dst_port: u16, + quic_tunnel_client: QuicTunnelClient, +) -> anyhow::Result<()> { + let (mut send_stream, mut recv_stream) = quic_tunnel_client.open_bi(target_ip).await?; + let handshake = QuicProxyHandshake { + handshake: Some(quic_proxy_handshake::Handshake::TcpPortMapping( + PortProxyHandshake { + src_ip: src.ip().to_string(), + src_port: src.port().into(), + dst_host, + dst_port: dst_port as _, + }, + )), + }; + send_handshake(&mut send_stream, handshake).await?; + let (mut tcp_r, mut tcp_w) = tcp_stream.split(); + tokio::select! { + _ = tokio::io::copy(&mut recv_stream, &mut tcp_w) => {}, + _ = tokio::io::copy(&mut tcp_r, &mut send_stream) => {}, + } + Ok(()) +} diff --git a/vnt-core/src/port_mapping/udp_port_mapping.rs b/vnt-core/src/port_mapping/udp_port_mapping.rs new file mode 100644 index 0000000..31af7ed --- /dev/null +++ b/vnt-core/src/port_mapping/udp_port_mapping.rs @@ -0,0 +1,145 @@ +use crate::enhanced_tunnel::quic_over::quic_client::{QuicTunnelClient, send_handshake}; +use crate::port_mapping::PortMapping; +use crate::protocol::client_message::{ + PortProxyHandshake, QuicProxyHandshake, quic_proxy_handshake, +}; +use crate::utils::task_control::TaskGroup; +use anyhow::Context; +use bytes::Bytes; +use futures::{SinkExt, StreamExt}; +use parking_lot::Mutex; +use pnet_packet::ip::IpNextHeaderProtocols; +use std::collections::HashMap; +use std::net::{Ipv4Addr, SocketAddr}; +use std::sync::Arc; +use std::time::Duration; +use tokio::net::UdpSocket; +use tokio::sync::mpsc::Sender; +use tokio::sync::mpsc::error::TrySendError; +use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec}; + +pub async fn start( + task_group: &TaskGroup, + list: &Vec, + quic_tunnel_client: QuicTunnelClient, +) -> anyhow::Result<()> { + for x in list { + if x.protocol != IpNextHeaderProtocols::Udp { + continue; + } + log::info!("Starting UDP port mapping on {}", x); + let udp = UdpSocket::bind(x.src_addr) + .await + .with_context(|| format!("Udp port mapping Failed to bind to {}", x.src_addr))?; + let udp_socket = Arc::new(udp); + let group = task_group.clone(); + let tunnel_client = quic_tunnel_client.clone(); + let mapping = x.clone(); + task_group.spawn(async move { + if let Err(e) = recv(&group, udp_socket, &mapping, tunnel_client).await { + log::error!("recv {:?},mapping:{mapping}", e); + } + }); + } + Ok(()) +} + +async fn recv( + task_group: &TaskGroup, + udp_socket: Arc, + mapping: &PortMapping, + quic_tunnel_client: QuicTunnelClient, +) -> anyhow::Result<()> { + let mut buf = vec![0u8; 65536]; + let dest_map = Arc::new(Mutex::new(HashMap::>::new())); + loop { + let (len, src) = udp_socket.recv_from(&mut buf).await?; + let bytes = Bytes::copy_from_slice(&buf[..len]); + + let tx = { + let mut map = dest_map.lock(); + if let Some(tx) = map.get(&src) { + tx.clone() + } else { + let (tx, rx) = tokio::sync::mpsc::channel::(128); + let udp_socket = udp_socket.clone(); + let virtual_target_ip = mapping.virtual_target_ip; + let dst_host = mapping.dst_host.clone(); + let dst_port = mapping.dst_port; + let tunnel_client = quic_tunnel_client.clone(); + task_group.spawn(async move { + if let Err(e) = udp_mapping_handle( + udp_socket, + src, + virtual_target_ip, + dst_host, + dst_port, + rx, + tunnel_client, + ) + .await + { + log::error!("udp_mapping_handle {e:?},src:{src}"); + } + }); + + map.insert(src, tx.clone()); + tx + } + }; + + if let Err(err) = tx.try_send(bytes) { + match err { + TrySendError::Full(_) => {} + TrySendError::Closed(_) => { + let mut map = dest_map.lock(); + map.remove(&src); + } + } + } + } +} + +async fn udp_mapping_handle( + udp_socket: Arc, + src: SocketAddr, + target_ip: Ipv4Addr, + dst_host: String, + dst_port: u16, + mut rx: tokio::sync::mpsc::Receiver, + quic_tunnel_client: QuicTunnelClient, +) -> anyhow::Result<()> { + let (mut send_stream, recv_stream) = quic_tunnel_client.open_bi(target_ip).await?; + + let handshake = QuicProxyHandshake { + handshake: Some(quic_proxy_handshake::Handshake::UdpPortMapping( + PortProxyHandshake { + src_ip: src.ip().to_string(), + src_port: src.port().into(), + dst_host, + dst_port: dst_port as _, + }, + )), + }; + send_handshake(&mut send_stream, handshake).await?; + let mut framed_write = FramedWrite::new(send_stream, LengthDelimitedCodec::new()); + let mut framed_read = FramedRead::new(recv_stream, LengthDelimitedCodec::new()); + loop { + tokio::select! { + Some(buf) = framed_read.next()=>{ + let buf = buf?; + udp_socket.send_to(&buf, src).await?; + }, + Some(buf) = rx.recv()=>{ + framed_write.send(buf).await? + }, + _ = tokio::time::sleep(Duration::from_secs(60)) =>{ + break; + }, + else => { + break; + } + } + } + Ok(()) +} diff --git a/vnt-core/src/protocol/client_message.rs b/vnt-core/src/protocol/client_message.rs new file mode 100644 index 0000000..2de4d4d --- /dev/null +++ b/vnt-core/src/protocol/client_message.rs @@ -0,0 +1,98 @@ +mod proto { + include!(concat!(env!("OUT_DIR"), "/protocol.client.rs")); +} + +use anyhow::bail; +use bytes::BytesMut; +use prost::Message; +use std::net::{Ipv4Addr, Ipv6Addr}; + +use crate::protocol::ProtoToBytesMut; +pub use proto::*; + +pub fn encode_nat_info(nat_info: &rust_p2p_core::nat::NatInfo) -> proto::NatInfo { + let nat_type = match nat_info.nat_type { + rust_p2p_core::nat::NatType::Cone => proto::NatType::Cone, + rust_p2p_core::nat::NatType::Symmetric => proto::NatType::Symmetric, + }; + + proto::NatInfo { + nat_type: nat_type.into(), + public_ips: nat_info.public_ips.iter().map(|v| (*v).into()).collect(), + public_udp_ports: nat_info + .public_udp_ports + .iter() + .map(|v| (*v).into()) + .collect(), + public_port_range: nat_info.public_port_range.into(), + local_ipv4s: nat_info.local_ipv4s.iter().map(|v| (*v).into()).collect(), + ipv6: nat_info.ipv6.map(|v| v.octets().to_vec()), + local_udp_ports: nat_info + .local_udp_ports + .iter() + .map(|v| (*v).into()) + .collect(), + local_tcp_port: nat_info.local_tcp_port.into(), + public_tcp_port: nat_info.public_tcp_port.into(), + } +} +pub fn decode_nat_info(msg: proto::NatInfo) -> anyhow::Result { + let nat_type = match msg.nat_type() { + proto::NatType::Cone => rust_p2p_core::nat::NatType::Cone, + proto::NatType::Symmetric => rust_p2p_core::nat::NatType::Symmetric, + }; + let ipv6: Option<[u8; 16]> = msg.ipv6.and_then(|v| v.as_slice().try_into().ok()); + + // Validate all ports fit in u16 + let validate_port = |p: u32| -> anyhow::Result { + u16::try_from(p).map_err(|_| anyhow::anyhow!("invalid port number: {}", p)) + }; + + let public_udp_ports: Result, _> = msg + .public_udp_ports + .into_iter() + .map(validate_port) + .collect(); + let local_udp_ports: Result, _> = + msg.local_udp_ports.into_iter().map(validate_port).collect(); + + Ok(rust_p2p_core::nat::NatInfo { + nat_type, + public_ips: msg.public_ips.into_iter().map(|v| v.into()).collect(), + public_udp_ports: public_udp_ports?, + mapping_tcp_addr: vec![], + mapping_udp_addr: vec![], + public_port_range: validate_port(msg.public_port_range)?, + local_ipv4: msg + .local_ipv4s + .first() + .map(|v| (*v).into()) + .unwrap_or(Ipv4Addr::UNSPECIFIED), + local_ipv4s: msg.local_ipv4s.into_iter().map(|v| v.into()).collect(), + ipv6: ipv6.map(Ipv6Addr::from), + local_udp_ports: local_udp_ports?, + local_tcp_port: validate_port(msg.local_tcp_port)?, + public_tcp_port: validate_port(msg.public_tcp_port)?, + }) +} +#[derive(Clone, Debug)] +pub struct PunchInfo { + pub nat_info: rust_p2p_core::nat::NatInfo, +} + +impl PunchInfo { + pub fn from_slice(buf: &[u8]) -> anyhow::Result { + let msg = proto::PunchInfo::decode(buf)?; + let Some(nat_info) = msg.nat_info else { + bail!("Punched info decode failed."); + }; + let nat_info = decode_nat_info(nat_info)?; + Ok(Self { nat_info }) + } + pub fn encode(&self) -> BytesMut { + let message = proto::PunchInfo { + nat_info: Some(encode_nat_info(&self.nat_info)), + }; + message.encode_bytes_mut() + } +} diff --git a/vnt-core/src/protocol/control_message.rs b/vnt-core/src/protocol/control_message.rs new file mode 100644 index 0000000..b1d7369 --- /dev/null +++ b/vnt-core/src/protocol/control_message.rs @@ -0,0 +1,275 @@ +use crate::protocol::ProtoToBytesMut; +pub(crate) use crate::protocol::control_message::proto::SelectiveBroadcast; +use crate::protocol::control_message::proto::request_message::RequestPayload; +use crate::protocol::control_message::proto::response_message::ResponsePayload; +use anyhow::bail; +use bytes::BytesMut; +use prost::Message; +use std::net::Ipv4Addr; + +mod proto { + include!(concat!(env!("OUT_DIR"), "/protocol.control_message.rs")); +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] +pub enum RegistrationMode { + #[default] + Normal = 0, + PreRegister = 1, +} + +impl From for proto::RegistrationMode { + fn from(mode: RegistrationMode) -> Self { + match mode { + RegistrationMode::Normal => proto::RegistrationMode::Normal, + RegistrationMode::PreRegister => proto::RegistrationMode::PreRegister, + } + } +} + +impl From for RegistrationMode { + fn from(mode: proto::RegistrationMode) -> Self { + match mode { + proto::RegistrationMode::Normal => RegistrationMode::Normal, + proto::RegistrationMode::PreRegister => RegistrationMode::PreRegister, + } + } +} +pub(crate) struct RegRequestMsg { + pub network_code: String, + pub device_id: String, + pub ip: Option, + pub name: String, + pub version: String, + pub key_sign: Option, + pub ip_variable: bool, + pub server_id: u32, + pub registration_mode: RegistrationMode, +} +impl RegRequestMsg { + // pub fn check(&self) -> anyhow::Result<()> { + // if self.network_code.is_empty() { + // return Err(anyhow!("network_code cannot be empty")); + // } + // if self.network_code.len() > MAX_NETWORK_CODE_LEN { + // return Err(anyhow!( + // "network_code length exceeds {} characters (current: {})", + // MAX_NETWORK_CODE_LEN, + // self.network_code.len() + // )); + // } + // if self.device_id.is_empty() { + // return Err(anyhow!("device_id cannot be empty")); + // } + // if self.device_id.len() > MAX_DEVICE_ID_LEN { + // return Err(anyhow!( + // "device_id length exceeds {} characters (current: {})", + // MAX_DEVICE_ID_LEN, + // self.device_id.len() + // )); + // } + // + // if self.name.len() > MAX_NAME_LEN { + // return Err(anyhow!( + // "name length exceeds {} characters (current: {})", + // MAX_NAME_LEN, + // self.name.len() + // )); + // } + // + // if self.version.len() > MAX_VERSION_LEN { + // return Err(anyhow!( + // "version length exceeds {} characters (current: {})", + // MAX_VERSION_LEN, + // self.version.len() + // )); + // } + // + // Ok(()) + // } + // pub fn from(msg: proto::RegRequestMsg) -> anyhow::Result { + // Ok(Self { + // network_code: msg.network_code, + // device_id: msg.device_id, + // ip: msg.ip.map(|ip| ip.into()), + // name: msg.name, + // version: msg.version, + // key_sign: msg.key_sign, + // ip_variable: msg.ip_variable, + // server_id: msg.server_id, + // }) + // } + pub fn to(self) -> proto::RegRequestMsg { + proto::RegRequestMsg { + network_code: self.network_code, + device_id: self.device_id, + ip: self.ip.map(|ip| ip.into()), + name: self.name, + version: self.version, + key_sign: self.key_sign, + ip_variable: self.ip_variable, + server_id: self.server_id, + registration_mode: proto::RegistrationMode::from(self.registration_mode).into(), + } + } +} +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct RegResponseMsg { + pub ip: Ipv4Addr, + pub prefix_len: u8, + pub gateway: Ipv4Addr, + pub server_version: String, +} +impl RegResponseMsg { + pub fn from(msg: proto::RegResponseMsg) -> anyhow::Result { + Ok(Self { + ip: msg.ip.into(), + prefix_len: (msg.prefix_len & 0xFF) as u8, + gateway: msg.gateway.into(), + server_version: msg.server_version, + }) + } + pub fn to(self) -> proto::RegResponseMsg { + proto::RegResponseMsg { + ip: self.ip.into(), + prefix_len: self.prefix_len as _, + gateway: self.gateway.into(), + server_version: self.server_version, + } + } +} +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct ErrorResponseMsg { + pub code: u32, + pub message: String, +} +impl ErrorResponseMsg { + pub fn from(msg: proto::ErrorResponseMsg) -> anyhow::Result { + Ok(Self { + code: msg.code, + message: msg.message, + }) + } + pub fn to(self) -> proto::ErrorResponseMsg { + proto::ErrorResponseMsg { + code: self.code, + message: self.message, + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct ConfirmRegResponseMsg { + pub success: bool, +} +impl ConfirmRegResponseMsg { + pub fn from(msg: proto::ConfirmRegResponseMsg) -> anyhow::Result { + Ok(Self { + success: msg.success, + }) + } + pub fn to(self) -> proto::ConfirmRegResponseMsg { + proto::ConfirmRegResponseMsg { + success: self.success, + } + } +} +pub(crate) enum RequestMessage { + Reg(RegRequestMsg), + ConfirmReg, +} +impl RequestMessage { + pub fn encode(self) -> BytesMut { + let request_payload = match self { + RequestMessage::Reg(reg) => RequestPayload::Reg(reg.to()), + RequestMessage::ConfirmReg => RequestPayload::ConfirmReg(proto::ConfirmRegMsg {}), + }; + proto::RequestMessage { + request_payload: Some(request_payload), + } + .encode_bytes_mut() + } +} +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ResponseMessage { + Reg(RegResponseMsg), + Error(ErrorResponseMsg), + ConfirmReg(ConfirmRegResponseMsg), +} +impl ResponseMessage { + pub fn from_slice(buf: &[u8]) -> anyhow::Result { + let msg = proto::ResponseMessage::decode(buf)?; + let Some(payload) = msg.response_payload else { + bail!("unsupported") + }; + match payload { + ResponsePayload::Reg(reg) => Ok(ResponseMessage::Reg(RegResponseMsg::from(reg)?)), + ResponsePayload::Error(e) => Ok(ResponseMessage::Error(ErrorResponseMsg::from(e)?)), + ResponsePayload::ConfirmReg(c) => { + Ok(ResponseMessage::ConfirmReg(ConfirmRegResponseMsg::from(c)?)) + } + } + } + pub fn encode(self) -> BytesMut { + let response_payload = match self { + ResponseMessage::Reg(reg) => ResponsePayload::Reg(reg.to()), + ResponseMessage::Error(e) => ResponsePayload::Error(e.to()), + ResponseMessage::ConfirmReg(c) => ResponsePayload::ConfirmReg(c.to()), + }; + proto::ResponseMessage { + response_payload: Some(response_payload), + } + .encode_bytes_mut() + } +} + +impl SelectiveBroadcast { + pub fn new(ips: &[Ipv4Addr], data: Vec) -> Self { + SelectiveBroadcast { + ips: ips.iter().map(|v| (*v).into()).collect(), + data, + } + } +} + +#[derive(Debug, Clone)] +pub struct ClientSimpleInfo { + pub ip: Ipv4Addr, + pub online: bool, +} +impl ClientSimpleInfo { + pub fn from(msg: proto::ClientSimpleInfo) -> anyhow::Result { + Ok(Self { + ip: msg.ip.into(), + online: msg.online, + }) + } + pub fn to(self) -> proto::ClientSimpleInfo { + proto::ClientSimpleInfo { + ip: self.ip.into(), + online: self.online, + } + } +} +#[derive(Debug)] +pub struct ClientSimpleInfoList { + pub data_version: u64, + pub list: Vec, + pub is_all: bool, + pub time: i64, +} +impl ClientSimpleInfoList { + pub fn from_slice(buf: &[u8]) -> anyhow::Result { + let msg = proto::ClientSimpleInfoList::decode(buf)?; + let mut list = Vec::with_capacity(msg.list.len()); + for x in msg.list { + list.push(ClientSimpleInfo::from(x)?); + } + Ok(Self { + data_version: msg.data_version, + list, + is_all: msg.is_all, + time: msg.time, + }) + } +} diff --git a/vnt-core/src/protocol/ip_packet_protocol.rs b/vnt-core/src/protocol/ip_packet_protocol.rs new file mode 100644 index 0000000..27ac2fc --- /dev/null +++ b/vnt-core/src/protocol/ip_packet_protocol.rs @@ -0,0 +1,302 @@ +/* + 0 15 31 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 1 | msg_type(7) |max ttl(4) |curr ttl(4)| C | G | R | reserve(13) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | seq(32) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | src ID(32) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | dest ID(32) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | payload(n) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +*/ +#![allow(dead_code)] +use crate::protocol::transmission::TransmissionBytes; +use bytes::{Bytes, BytesMut}; +use std::io; +use zerocopy::byteorder::{NetworkEndian, U32}; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Ref, Unaligned}; + +#[derive(Debug, FromBytes, IntoBytes, Unaligned, KnownLayout, Immutable)] +#[repr(C)] +pub struct NetHeader { + /// Byte 0: bit7 = 1, bit0..6 = msg_type + pub type_byte: u8, + /// Byte 1: high 4 = max ttl, low 4 = curr ttl + pub ttl_byte: u8, + /// Byte 2: C(0x80) | G(0x40) | reserve + pub flags_byte: u8, + /// Byte 3: reserve + pub _reserved: u8, + + pub seq: U32, + pub src_id: U32, + pub dest_id: U32, +} +const COMPRESSED: u8 = 0x80; +const GATEWAY: u8 = 0x40; +const FEC: u8 = 0x20; +impl NetHeader { + #[inline] + pub fn msg_type(&self) -> u8 { + self.type_byte & 0x7F + } + + #[inline] + pub fn set_msg_type(&mut self, msg_type: u8) { + self.type_byte = (msg_type & 0x7F) | 0x80; + } + + #[inline] + pub fn max_ttl(&self) -> u8 { + self.ttl_byte >> 4 + } + + #[inline] + pub fn curr_ttl(&self) -> u8 { + self.ttl_byte & 0x0F + } + + #[inline] + pub fn set_ttl(&mut self, max: u8, curr: u8) { + self.ttl_byte = (max << 4) | (curr & 0x0F); + } + + #[inline] + pub fn decr_ttl(&mut self) { + let curr = self.curr_ttl(); + if curr == 0 { + return; + } + self.ttl_byte = (self.ttl_byte & 0xF0) | (curr - 1); + } + + fn set_flag(&mut self, mask: u8, val: bool) { + if val { + self.flags_byte |= mask; + } else { + self.flags_byte &= !mask; + } + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum MsgType { + Turn = 1, + Broadcast = 2, + ExcludeBroadcast = 3, + TargetBroadcast = 4, + + Ping = 5, + Pong = 6, + PingTurn = 7, + PongTurn = 8, + + PunchStart1 = 9, + PunchStart2 = 10, + PunchReq = 11, + PunchRes = 12, + + PushClientIps = 13, + + RpcReq = 14, + RpcRes = 15, + + Quic = 17, +} +impl From for u8 { + fn from(val: MsgType) -> Self { + val as u8 + } +} + +impl TryFrom for MsgType { + type Error = io::Error; + + fn try_from(value: u8) -> Result { + let val = match value { + 1 => MsgType::Turn, + 2 => MsgType::Broadcast, + 3 => MsgType::ExcludeBroadcast, + 4 => MsgType::TargetBroadcast, + + 5 => MsgType::Ping, + 6 => MsgType::Pong, + 7 => MsgType::PingTurn, + 8 => MsgType::PongTurn, + + 9 => MsgType::PunchStart1, + 10 => MsgType::PunchStart2, + 11 => MsgType::PunchReq, + 12 => MsgType::PunchRes, + + 13 => MsgType::PushClientIps, + + 14 => MsgType::RpcReq, + 15 => MsgType::RpcRes, + + 17 => MsgType::Quic, + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("invalid msg type:{value}"), + )); + } + }; + Ok(val) + } +} + +pub const HEAD_LENGTH: usize = std::mem::size_of::(); + +pub struct NetPacket { + buffer: B, +} +impl> NetPacket { + pub fn new(buffer: B) -> io::Result> { + if buffer.as_ref().len() < HEAD_LENGTH { + return Err(io::ErrorKind::InvalidInput.into()); + } + Ok(NetPacket { buffer }) + } + fn header(&self) -> Ref<&[u8], NetHeader> { + // Safe: NetHeader is Unaligned and length is validated in new() + let (header, _) = Ref::<&[u8], NetHeader>::from_prefix(self.buffer.as_ref()).unwrap(); + header + } + pub fn buffer(&self) -> &[u8] { + self.buffer.as_ref() + } + pub fn into_buffer(self) -> B { + self.buffer + } + pub fn source_buf(&self) -> &B { + &self.buffer + } + pub fn msg_type(&self) -> io::Result { + self.header().msg_type().try_into() + } + pub fn max_ttl(&self) -> u8 { + self.header().max_ttl() + } + pub fn ttl(&self) -> u8 { + self.header().curr_ttl() + } + + pub fn seq(&self) -> u32 { + self.header().seq.get() + } + + pub fn src_id(&self) -> u32 { + self.header().src_id.get() + } + + pub fn dest_id(&self) -> u32 { + self.header().dest_id.get() + } + pub fn is_compressed(&self) -> bool { + (self.header().flags_byte & COMPRESSED) != 0 + } + pub fn is_gateway(&self) -> bool { + (self.header().flags_byte & GATEWAY) != 0 + } + pub fn is_fec(&self) -> bool { + (self.header().flags_byte & FEC) != 0 + } + pub fn head(&self) -> &[u8] { + &self.buffer.as_ref()[..HEAD_LENGTH] + } + pub fn payload(&self) -> &[u8] { + &self.buffer.as_ref()[HEAD_LENGTH..] + } +} + +impl + AsMut<[u8]>> NetPacket { + fn header_mut(&mut self) -> Ref<&mut [u8], NetHeader> { + // Safe: NetHeader is Unaligned and length is validated in new() + let (header, _) = Ref::<&mut [u8], NetHeader>::from_prefix(self.buffer.as_mut()).unwrap(); + header + } + + pub fn set_msg_type(&mut self, msg_type: MsgType) { + self.header_mut().set_msg_type(msg_type.into()); + } + + pub fn decr_ttl(&mut self){ + self.header_mut().decr_ttl() + } + + pub fn set_ttl(&mut self, ttl: u8) { + self.header_mut().set_ttl(ttl, ttl); + } + + pub fn set_seq(&mut self, seq: u32) { + self.header_mut().seq.set(seq); + } + + pub fn set_src_id(&mut self, id: u32) { + self.header_mut().src_id.set(id); + } + + pub fn set_dest_id(&mut self, id: u32) { + self.header_mut().dest_id.set(id); + } + + pub fn set_compressed_flag(&mut self, compressed: bool) { + self.header_mut().set_flag(COMPRESSED, compressed); + } + pub fn set_gateway_flag(&mut self, gateway: bool) { + self.header_mut().set_flag(GATEWAY, gateway); + } + pub fn set_fec_flag(&mut self, fec: bool) { + self.header_mut().set_flag(FEC, fec); + } + + pub fn set_payload(&mut self, data: &[u8]) -> io::Result<()> { + let buf = self.buffer.as_mut(); + if buf.len() < HEAD_LENGTH + data.len() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid message length", + )); + } + buf[HEAD_LENGTH..HEAD_LENGTH + data.len()].copy_from_slice(data); + Ok(()) + } + pub fn head_mut(&mut self) -> &mut [u8] { + &mut self.buffer.as_mut()[..HEAD_LENGTH] + } + pub fn payload_mut(&mut self) -> &mut [u8] { + &mut self.buffer.as_mut()[HEAD_LENGTH..] + } + pub fn source_buf_mut(&mut self) -> &mut B { + &mut self.buffer + } +} + +impl Clone for NetPacket { + fn clone(&self) -> Self { + NetPacket { + buffer: self.buffer.clone(), + } + } +} + +impl NetPacket { + pub fn into_bytes(self) -> NetPacket { + NetPacket { + buffer: self.buffer.freeze(), + } + } +} + +impl NetPacket { + pub fn into_bytes(self) -> NetPacket { + NetPacket { + buffer: self.buffer.into_bytes().freeze(), + } + } +} diff --git a/vnt-core/src/protocol/mod.rs b/vnt-core/src/protocol/mod.rs new file mode 100644 index 0000000..9d81685 --- /dev/null +++ b/vnt-core/src/protocol/mod.rs @@ -0,0 +1,21 @@ +use bytes::BytesMut; +use prost::Message; + +pub(crate) mod client_message; +pub mod control_message; +pub(crate) mod ip_packet_protocol; +pub(crate) mod rpc_message; +pub(crate) mod transmission; + +pub trait ProtoToBytesMut: Message { + fn encode_bytes_mut(&self) -> BytesMut + where + Self: Sized, + { + let mut bytes_mut = BytesMut::with_capacity(self.encoded_len()); + self.encode_raw(&mut bytes_mut); + bytes_mut + } +} + +impl ProtoToBytesMut for T {} diff --git a/vnt-core/src/protocol/rpc_message.rs b/vnt-core/src/protocol/rpc_message.rs new file mode 100644 index 0000000..a3d431e --- /dev/null +++ b/vnt-core/src/protocol/rpc_message.rs @@ -0,0 +1,4 @@ +mod proto { + include!(concat!(env!("OUT_DIR"), "/protocol.rpc.rs")); +} +pub use proto::*; diff --git a/vnt-core/src/protocol/transmission.rs b/vnt-core/src/protocol/transmission.rs new file mode 100644 index 0000000..28096e9 --- /dev/null +++ b/vnt-core/src/protocol/transmission.rs @@ -0,0 +1,266 @@ +use bytes::{Buf, Bytes, BytesMut}; +use std::borrow::{Borrow, BorrowMut}; +use std::io; +use std::ops::{Deref, DerefMut}; + +const DEFAULT_BUF_SIZE: usize = 2048; +#[derive(Clone)] +pub struct TransmissionBytes { + buf: BytesMut, + start: usize, + end: usize, +} +impl From for TransmissionBytes { + fn from(buf: BytesMut) -> TransmissionBytes { + let end = buf.len(); + Self { buf, start: 0, end } + } +} +impl From for TransmissionBytes { + fn from(buf: Bytes) -> TransmissionBytes { + let end = buf.len(); + Self { + buf: BytesMut::from(buf), + start: 0, + end, + } + } +} +impl From<&[u8]> for TransmissionBytes { + fn from(buf: &[u8]) -> TransmissionBytes { + let end = buf.len(); + Self { + buf: BytesMut::from(buf), + start: 0, + end, + } + } +} + +impl TransmissionBytes { + pub fn new_offset(start: usize) -> Self { + TransmissionBytes { + buf: BytesMut::zeroed(DEFAULT_BUF_SIZE), + start, + end: start, + } + } + pub fn new_offset_zeroed(start: usize) -> Self { + TransmissionBytes { + buf: BytesMut::zeroed(DEFAULT_BUF_SIZE), + start, + end: DEFAULT_BUF_SIZE, + } + } + pub fn zeroed(cap: usize) -> Self { + TransmissionBytes { + buf: BytesMut::zeroed(cap), + start: 0, + end: cap, + } + } + pub fn zeroed_size(size: usize, reserve: usize) -> Self { + TransmissionBytes { + buf: BytesMut::zeroed(size + reserve), + start: 0, + end: size, + } + } + #[allow(dead_code)] + pub fn with_capacity(head_room: usize, capacity: usize) -> Self { + TransmissionBytes { + buf: BytesMut::zeroed(capacity), + start: head_room, + end: head_room, + } + } + pub fn len(&self) -> usize { + self.end - self.start + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + #[allow(dead_code)] + pub fn capacity(&self) -> usize { + self.buf.capacity() + } + /// 头部可用空间(可向前扩展的字节数) + #[inline] + pub fn head_room(&self) -> usize { + self.start + } + + /// 尾部可用空间(可向后扩展的字节数) + #[inline] + #[allow(dead_code)] + pub fn tail_room(&self) -> usize { + self.buf.capacity() - self.end + } + #[inline] + fn as_slice(&self) -> &[u8] { + &self.buf[self.start..self.end] + } + + #[inline] + fn as_slice_mut(&mut self) -> &mut [u8] { + &mut self.buf[self.start..self.end] + } + pub fn put(&mut self, data: &[u8]) -> io::Result<()> { + let need = data.len(); + let free = self.buf.capacity() - self.end; + + if need > free { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("data too large:need={need},free={free}"), + )); + } + + self.buf[self.end..self.end + need].copy_from_slice(data); + self.end += need; + Ok(()) + } + pub fn retreat_head(&mut self, len: usize) -> io::Result<()> { + if len > self.head_room() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "retreat_head beyond start: len={len}, head_room={}", + self.head_room() + ), + )); + } + self.start -= len; + Ok(()) + } + pub fn advance_head(&mut self, len: usize) -> io::Result<()> { + if len > self.len() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "advance_head beyond end: len={len}, data_len={}", + self.len() + ), + )); + } + self.start += len; + Ok(()) + } + pub fn set_len(&mut self, new_len: usize) -> io::Result<()> { + let new_end = self.start + new_len; + if new_end > self.buf.capacity() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "set_len exceeds capacity: new_len={new_len}, max={}", + self.buf.capacity() - self.start + ), + )); + } + self.end = new_end; + Ok(()) + } + pub fn resize(&mut self, new_len: usize, value: u8) { + let new_end = self.start + new_len; + self.buf.resize(new_end, value); + self.end = new_end; + } + pub fn extend_end(&mut self, n: usize) { + if self.end + n > self.buf.len() { + self.buf.resize(self.end + n, 0); + } + self.end += n; + } + pub fn shrink_end(&mut self, n: usize) { + if n >= self.end - self.start { + self.end = self.start; + } else { + self.end -= n; + } + } + #[allow(dead_code)] + pub fn clear(&mut self) { + self.start = 0; + self.end = 0; + } + pub fn into_bytes(mut self) -> BytesMut { + self.buf.truncate(self.end); + if self.start > 0 { + self.buf.advance(self.start); + } + self.buf + } +} + +impl AsRef<[u8]> for TransmissionBytes { + #[inline] + fn as_ref(&self) -> &[u8] { + self.as_slice() + } +} + +impl Deref for TransmissionBytes { + type Target = [u8]; + + #[inline] + fn deref(&self) -> &[u8] { + self.as_ref() + } +} + +impl AsMut<[u8]> for TransmissionBytes { + #[inline] + fn as_mut(&mut self) -> &mut [u8] { + self.as_slice_mut() + } +} + +impl DerefMut for TransmissionBytes { + #[inline] + fn deref_mut(&mut self) -> &mut [u8] { + self.as_mut() + } +} + +impl Borrow<[u8]> for TransmissionBytes { + fn borrow(&self) -> &[u8] { + self.as_ref() + } +} + +impl BorrowMut<[u8]> for TransmissionBytes { + fn borrow_mut(&mut self) -> &mut [u8] { + self.as_mut() + } +} + +pub trait ShrinkEnd { + fn shrink_end(&mut self, n: usize); +} +pub trait ExtendEnd { + fn extend_end(&mut self, n: usize); +} + +impl ShrinkEnd for TransmissionBytes { + fn shrink_end(&mut self, n: usize) { + self.shrink_end(n); + } +} + +impl ExtendEnd for TransmissionBytes { + fn extend_end(&mut self, n: usize) { + self.extend_end(n); + } +} + +impl ShrinkEnd for &mut TransmissionBytes { + fn shrink_end(&mut self, n: usize) { + TransmissionBytes::shrink_end(self, n); + } +} + +impl ExtendEnd for &mut TransmissionBytes { + fn extend_end(&mut self, n: usize) { + TransmissionBytes::extend_end(self, n); + } +} diff --git a/vnt-core/src/tls/cert.rs b/vnt-core/src/tls/cert.rs new file mode 100644 index 0000000..88d01eb --- /dev/null +++ b/vnt-core/src/tls/cert.rs @@ -0,0 +1,114 @@ +use anyhow::{Context, Result}; +use rcgen::{CertificateParams, DnType, KeyPair, PKCS_ED25519, SerialNumber}; +use rustls::pki_types; +use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; +use sha2::{Digest, Sha256}; +use time::{Duration, OffsetDateTime}; + +const ED25519_PKCS8_V1_PREFIX: [u8; 16] = [ + 0x30, 0x2e, // Sequence (len 46) + 0x02, 0x01, 0x00, // Version 0 + 0x30, 0x05, // Sequence (len 5) + 0x06, 0x03, 0x2b, 0x65, 0x70, // OID: 1.3.101.112 (Ed25519) + 0x04, 0x22, // Octet String (len 34) - 包装私钥 + 0x04, 0x20, // Octet String (len 32) - 内部 CurvePrivateKey +]; + +pub fn generate_deterministic_cert( + password: &str, +) -> Result<(CertificateDer<'static>, PrivateKeyDer<'static>)> { + // 基于密码生成 32 字节的确定性种子 + let seed = derive_seed_from_password(password); + + let mut pkcs8_bytes = Vec::with_capacity(ED25519_PKCS8_V1_PREFIX.len() + seed.len()); + pkcs8_bytes.extend_from_slice(&ED25519_PKCS8_V1_PREFIX); + pkcs8_bytes.extend_from_slice(&seed); + let private_key_der = pki_types::PrivateKeyDer::try_from(pkcs8_bytes.clone()) + .map_err(|e| anyhow::anyhow!("Failed to convert private key: {}", e))?; + let key_pair = KeyPair::from_der_and_sign_algo(&private_key_der, &PKCS_ED25519) + .context("Failed to load determinstic Ed25519 key")?; + + let mut params = CertificateParams::new(vec!["deterministic-node".to_string()])?; + params + .distinguished_name + .push(DnType::CommonName, "Deterministic Self-Signed Cert"); + + let not_before = OffsetDateTime::UNIX_EPOCH; + let not_after = not_before + Duration::days(365 * 1000); + params.not_before = not_before; + params.not_after = not_after; + + let serial_number_bytes = derive_serial_number(password); + params.serial_number = Some(SerialNumber::from_slice(&serial_number_bytes)); + + // Ed25519 签名是确定性的 (RFC 8032),不需要随机数,因此每次运行结果字节完全一致 + let cert = params + .self_signed(&key_pair) + .context("Failed to sign certificate")?; + + let cert_der = cert.der().clone(); + + let private_key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(pkcs8_bytes)); + + Ok((cert_der, private_key_der)) +} + +fn derive_serial_number(password: &str) -> [u8; 20] { + let mut hasher = Sha256::new(); + hasher.update(b"vnt-serial-v1:"); + hasher.update(password.as_bytes()); + let result = hasher.finalize(); + + let mut serial = [0u8; 20]; + serial.copy_from_slice(&result[..20]); + serial +} + +fn derive_seed_from_password(password: &str) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(b"vnt-ed25519-seed-v1:"); + hasher.update(password.as_bytes()); + + let mut result = hasher.finalize(); + for _ in 0..10 { + let mut hasher = Sha256::new(); + hasher.update(result); + result = hasher.finalize(); + } + + result.into() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_deterministic_cert_generation() { + let password = "test_password_123"; + + // 生成两次证书 + let (cert1, key1) = generate_deterministic_cert(password).unwrap(); + let (cert2, key2) = generate_deterministic_cert(password).unwrap(); + + // 验证私钥相同 + assert_eq!( + key1.secret_der(), + key2.secret_der(), + "Private keys should be identical" + ); + + // 验证证书相同 + assert_eq!(cert1, cert2, "Certificates should be identical"); + } + + #[test] + fn test_different_passwords_generate_different_certs() { + let (cert1, key1) = generate_deterministic_cert("password1").unwrap(); + let (cert2, key2) = generate_deterministic_cert("password2").unwrap(); + + // 不同密码应该生成不同的证书和密钥 + assert_ne!(cert1, cert2); + assert_ne!(key1.secret_der(), key2.secret_der()); + } +} diff --git a/vnt-core/src/tls/mod.rs b/vnt-core/src/tls/mod.rs new file mode 100644 index 0000000..a565d1f --- /dev/null +++ b/vnt-core/src/tls/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod cert; +pub mod verifier; diff --git a/vnt-core/src/tls/verifier.rs b/vnt-core/src/tls/verifier.rs new file mode 100644 index 0000000..e269c2f --- /dev/null +++ b/vnt-core/src/tls/verifier.rs @@ -0,0 +1,228 @@ +use anyhow::Context; +use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; +use rustls::pki_types::{CertificateDer, ServerName, UnixTime}; +use rustls::{CertificateError, ClientConfig, Error, SignatureScheme}; +use sha2::{Digest, Sha256}; +use std::fmt; +use std::str::FromStr; + +#[derive(Debug)] +pub struct FingerprintVerifier { + pub expected_fingerprint: [u8; 32], +} +impl FingerprintVerifier { + pub fn new(expected_fingerprint: [u8; 32]) -> Self { + Self { + expected_fingerprint, + } + } +} + +impl ServerCertVerifier for FingerprintVerifier { + fn verify_server_cert( + &self, + end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName, + _ocsp_response: &[u8], + _now: UnixTime, + ) -> Result { + let mut hasher = Sha256::new(); + hasher.update(end_entity.as_ref()); + let calculated_hash: [u8; 32] = hasher.finalize().into(); + + if calculated_hash == self.expected_fingerprint { + Ok(ServerCertVerified::assertion()) + } else { + log::error!( + "Certificate fingerprint mismatch. Expected: {:X?}, Got: {:X?}", + self.expected_fingerprint, + calculated_hash + ); + Err(Error::InvalidCertificate(CertificateError::BadSignature)) + } + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + // RSA schemes + rustls::SignatureScheme::RSA_PKCS1_SHA256, + rustls::SignatureScheme::RSA_PKCS1_SHA384, + rustls::SignatureScheme::RSA_PKCS1_SHA512, + rustls::SignatureScheme::RSA_PSS_SHA256, + rustls::SignatureScheme::RSA_PSS_SHA384, + rustls::SignatureScheme::RSA_PSS_SHA512, + // ECDSA schemes + rustls::SignatureScheme::ECDSA_NISTP256_SHA256, + rustls::SignatureScheme::ECDSA_NISTP384_SHA384, + rustls::SignatureScheme::ECDSA_NISTP521_SHA512, + // EdDSA schemes + rustls::SignatureScheme::ED25519, + rustls::SignatureScheme::ED448, + ] + } +} +#[derive(Debug)] +pub struct InsecureVerifier; + +impl ServerCertVerifier for InsecureVerifier { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName, + _ocsp_response: &[u8], + _now: UnixTime, + ) -> Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + SignatureScheme::RSA_PKCS1_SHA256, + SignatureScheme::RSA_PKCS1_SHA384, + SignatureScheme::RSA_PKCS1_SHA512, + SignatureScheme::RSA_PSS_SHA256, + SignatureScheme::RSA_PSS_SHA384, + SignatureScheme::RSA_PSS_SHA512, + SignatureScheme::ECDSA_NISTP256_SHA256, + SignatureScheme::ECDSA_NISTP384_SHA384, + SignatureScheme::ECDSA_NISTP521_SHA512, + SignatureScheme::ED25519, + SignatureScheme::ED448, + ] + } +} + +pub fn load_root_cert() -> anyhow::Result { + let mut root_cert_store = rustls::RootCertStore::empty(); + let certs = rustls_native_certs::load_native_certs().certs; + for cert in certs { + root_cert_store + .add(cert) + .context("Failed to add native cert to store")?; + } + Ok(root_cert_store) +} + +#[derive(Debug, Clone, Default)] +pub enum CertValidationMode { + #[default] + InsecureSkipVerification, + VerifyFingerprint([u8; 32]), + Standard, +} +impl FromStr for CertValidationMode { + type Err = String; + + fn from_str(value: &str) -> Result { + let val = value.trim().to_lowercase(); + if val == "skip" { + return Ok(CertValidationMode::InsecureSkipVerification); + } + if val == "standard" { + return Ok(CertValidationMode::Standard); + } + if let Some(hex_str) = val.strip_prefix("finger:") { + let decoded = + hex::decode(hex_str).map_err(|e| format!("Invalid hex in fingerprint: {}", e))?; + + if decoded.len() != 32 { + return Err(format!( + "Fingerprint must be 32 bytes (64 hex chars), got {} bytes", + decoded.len() + )); + } + + let mut arr = [0u8; 32]; + arr.copy_from_slice(&decoded); + return Ok(CertValidationMode::VerifyFingerprint(arr)); + } + Err(format!("Unknown certificate validation mode: {}", value)) + } +} +impl fmt::Display for CertValidationMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CertValidationMode::InsecureSkipVerification => { + write!(f, "skip") + } + CertValidationMode::Standard => { + write!(f, "standard") + } + CertValidationMode::VerifyFingerprint(fingerprint) => { + let hex_str = hex::encode(fingerprint); + + write!(f, "finger:{}", hex_str) + } + } + } +} + +impl CertValidationMode { + pub fn build_verifier(&self) -> anyhow::Result> { + match self { + CertValidationMode::InsecureSkipVerification => { + Ok(std::sync::Arc::new(InsecureVerifier)) + } + CertValidationMode::VerifyFingerprint(fingerprint) => { + Ok(std::sync::Arc::new(FingerprintVerifier { + expected_fingerprint: *fingerprint, + })) + } + CertValidationMode::Standard => { + let root_store = load_root_cert()?; + let verifier = + rustls::client::WebPkiServerVerifier::builder(std::sync::Arc::new(root_store)) + .build()?; + + Ok(verifier) + } + } + } + pub fn create_tls_client_config(&self) -> anyhow::Result { + let verifier = self.build_verifier()?; + + let config = ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(verifier) + .with_no_client_auth(); + Ok(config) + } +} diff --git a/vnt-core/src/tun/enhanced_tun.rs b/vnt-core/src/tun/enhanced_tun.rs new file mode 100644 index 0000000..3ad80b9 --- /dev/null +++ b/vnt-core/src/tun/enhanced_tun.rs @@ -0,0 +1,18 @@ +use crate::context::NetworkAddr; +use crate::nat::internal_nat::InternalNatInbound; +use crate::protocol::transmission::TransmissionBytes; +use crate::tun::TunDataInbound; + +#[derive(Clone)] +pub enum EnhancedTunInbound { + Tun(TunDataInbound), + Nat(InternalNatInbound), +} +impl EnhancedTunInbound { + pub async fn inbound(&self, data: TransmissionBytes, net: &NetworkAddr) -> anyhow::Result<()> { + match self { + EnhancedTunInbound::Tun(tun) => tun.send(data, net).await, + EnhancedTunInbound::Nat(nat) => nat.send(&data, net).await, + } + } +} diff --git a/vnt-core/src/tun/general.rs b/vnt-core/src/tun/general.rs new file mode 100644 index 0000000..e16f388 --- /dev/null +++ b/vnt-core/src/tun/general.rs @@ -0,0 +1,236 @@ +use crate::enhanced_tunnel::outbound::EnhancedOutbound; +use crate::protocol::ip_packet_protocol::HEAD_LENGTH; +use crate::protocol::transmission::TransmissionBytes; +use crate::utils::task_control::{SubTask, TaskGroup}; +use anyhow::{Context, bail}; +use bytes::BytesMut; +use futures::{SinkExt, StreamExt}; +use std::io; +use std::net::Ipv4Addr; +use std::sync::Arc; +use tokio::sync::mpsc::{Receiver, Sender}; +use tun_rs::async_framed::{Decoder, DeviceFramedRead, DeviceFramedWrite, Encoder}; +use tun_rs::{AsyncDevice, DeviceBuilder}; + +#[derive(Clone)] +pub struct DeviceIOManager { + task_group: TaskGroup, + device: DeviceMutex, +} +type DeviceMutex = Arc, Option<(Ipv4Addr, u8)>)>>; +pub struct DeviceTask { + device: Arc, + task_recv: SubTask, + task_send: SubTask, +} +#[derive(Debug, Default)] +pub struct DeviceConfig { + pub tun_name: Option, + #[cfg(unix)] + pub tun_fd: Option, + pub mtu: Option, +} + +impl DeviceConfig { + pub fn set_tun_name(mut self, tun_name: String) -> Self { + self.tun_name = Some(tun_name); + self + } + #[cfg(unix)] + pub fn set_tun_fd(mut self, tun_fd: i32) -> Self { + self.tun_fd = Some(tun_fd); + self + } + pub fn set_mtu(mut self, mtu: u16) -> Self { + self.mtu = Some(mtu); + self + } +} +#[derive(Clone)] +pub struct TunInbound { + pub(crate) sender: Sender, +} + +pub struct TunReceiver { + receiver: Receiver, +} +pub fn tun_channel() -> (TunInbound, TunReceiver) { + let (sender, receiver) = tokio::sync::mpsc::channel(1024); + (TunInbound { sender }, TunReceiver { receiver }) +} + +impl DeviceIOManager { + pub fn new(task_group: TaskGroup) -> DeviceIOManager { + Self { + task_group, + device: Arc::new(Default::default()), + } + } + pub async fn stop_task(&self) { + let mut guard = self.device.lock().await; + if let Some(dev) = guard.0.take() { + dev.task_recv.stop().await; + dev.task_send.stop().await; + } + } + pub async fn start_task( + &self, + device_config: DeviceConfig, + receiver: TunReceiver, + enhanced_outbound: EnhancedOutbound, + ) -> anyhow::Result<()> { + self.stop_task().await; + let task = create( + &self.task_group, + device_config, + receiver.receiver, + enhanced_outbound, + )?; + self.device.lock().await.0.replace(task); + Ok(()) + } + #[cfg(not(target_os = "android"))] + pub async fn tun_if_index(&self) -> anyhow::Result { + let guard = self.device.lock().await; + if let Some(v) = &guard.0 { + Ok(v.device.if_index()?) + } else { + bail!("device doesn't exist") + } + } + pub async fn set_network(&self, ip: Ipv4Addr, prefix_len: u8) -> anyhow::Result<()> { + let mut guard = self.device.lock().await; + let Some(dev) = guard.0.as_ref() else { + bail!("未启动tun") + }; + if let Some(v) = guard.1.as_ref() + && v.0 == ip + && v.1 == prefix_len + { + return Ok(()); + } + dev.device + .set_network_address(ip, prefix_len, None) + .context("设置IP失败")?; + guard.1 = Some((ip, prefix_len)); + Ok(()) + } +} + +fn create_tun(config: DeviceConfig) -> anyhow::Result { + #[cfg(unix)] + if let Some(fd) = config.tun_fd { + // SAFETY: Caller must ensure fd is a valid, open file descriptor for a TUN device. + // Using an invalid fd may cause undefined behavior. + unsafe { return Ok(AsyncDevice::from_fd(fd)?) } + } + let mut builder = DeviceBuilder::new(); + if let Some(tun_name) = config.tun_name { + builder = builder.name(tun_name); + } + if let Some(mtu) = config.mtu { + builder = builder.mtu(mtu); + } + #[cfg(windows)] + { + builder = builder.metric(1); + } + #[cfg(target_os = "linux")] + { + builder = builder.offload(true); + } + let dev = builder.build_async().context("创建tun失败")?; + #[cfg(target_os = "linux")] + { + _ = dev.set_tx_queue_len(1000); + } + Ok(dev) +} +fn create( + task_group: &TaskGroup, + config: DeviceConfig, + receiver: Receiver, + enhanced_outbound: EnhancedOutbound, +) -> anyhow::Result { + let device = Arc::new(create_tun(config)?); + + let device_framed_read = DeviceFramedRead::new(device.clone(), BytesCodec::new()); + let device_framed_write = DeviceFramedWrite::new(device.clone(), BytesCodec::new()); + + let task_recv = task_group.spawn(async move { + if let Err(e) = in_tun_loop(receiver, device_framed_write).await { + log::error!("in_tun_loop error: {e:?}") + } + }); + let task_send = task_group.spawn(async move { + if let Err(e) = out_tun_loop(device_framed_read, enhanced_outbound).await { + log::error!("out_tun_loop error: {e:?}"); + } + }); + + Ok(DeviceTask { + device, + task_recv, + task_send, + }) +} + +async fn in_tun_loop( + mut receiver: Receiver, + mut device_framed_write: DeviceFramedWrite>, +) -> anyhow::Result<()> { + while let Some(data) = receiver.recv().await { + match device_framed_write.send(data).await { + Ok(_) => {} + Err(e) => { + log::error!("send to tun error: {:?}", e); + return Err(anyhow::anyhow!(e)); + } + } + } + Ok(()) +} + +async fn out_tun_loop( + mut device_framed_read: DeviceFramedRead>, + enhanced_outbound: EnhancedOutbound, +) -> anyhow::Result<()> { + while let Some(rs) = device_framed_read.next().await { + let bytes_mut = rs?; + enhanced_outbound.ipv4_outbound(bytes_mut).await; + } + Ok(()) +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Default)] +pub struct BytesCodec(()); +impl BytesCodec { + pub fn new() -> BytesCodec { + BytesCodec(()) + } +} +impl Decoder for BytesCodec { + type Item = TransmissionBytes; + type Error = io::Error; + + fn decode(&mut self, buf: &mut BytesMut) -> Result, io::Error> { + if !buf.is_empty() { + let mut bytes = TransmissionBytes::new_offset(HEAD_LENGTH); + bytes.put(buf)?; + buf.clear(); + Ok(Some(bytes)) + } else { + Ok(None) + } + } +} + +impl Encoder for BytesCodec { + type Error = io::Error; + + fn encode(&mut self, data: TransmissionBytes, buf: &mut BytesMut) -> Result<(), io::Error> { + buf.reserve(data.len()); + buf.extend_from_slice(&data); + Ok(()) + } +} diff --git a/vnt-core/src/tun/mod.rs b/vnt-core/src/tun/mod.rs new file mode 100644 index 0000000..524e02b --- /dev/null +++ b/vnt-core/src/tun/mod.rs @@ -0,0 +1,6 @@ +mod general; +pub use general::*; +mod sender; +pub use sender::*; + +pub mod enhanced_tun; diff --git a/vnt-core/src/tun/sender.rs b/vnt-core/src/tun/sender.rs new file mode 100644 index 0000000..06679ae --- /dev/null +++ b/vnt-core/src/tun/sender.rs @@ -0,0 +1,40 @@ +use crate::context::NetworkAddr; +use crate::nat::AllowSubnetExternalRoute; +use crate::protocol::transmission::TransmissionBytes; +use crate::tun::TunInbound; +use pnet_packet::ipv4::Ipv4Packet; + +#[derive(Clone)] +pub struct TunDataInbound { + allow_subnet: AllowSubnetExternalRoute, + tun_inbound: TunInbound, +} +impl TunDataInbound { + pub fn new(tun_inbound: TunInbound, allow_subnet: AllowSubnetExternalRoute) -> Self { + Self { + allow_subnet, + tun_inbound, + } + } +} + +impl TunDataInbound { + pub async fn send(&self, data: TransmissionBytes, net: &NetworkAddr) -> anyhow::Result<()> { + if data[0] >> 4 != 4 { + return Ok(()); + } + let Some(ipv4) = Ipv4Packet::new(data.as_ref()) else { + return Ok(()); + }; + let dest = ipv4.get_destination(); + if net.network().contains(&dest) + || dest == net.broadcast + || dest.is_broadcast() + || dest.is_multicast() + || self.allow_subnet.allow(&dest) + { + self.tun_inbound.sender.send(data).await?; + } + Ok(()) + } +} diff --git a/vnt-core/src/tunnel_core/mod.rs b/vnt-core/src/tunnel_core/mod.rs new file mode 100644 index 0000000..f5b82c7 --- /dev/null +++ b/vnt-core/src/tunnel_core/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod p2p; +pub mod server; + +pub(crate) mod outbound; diff --git a/vnt-core/src/tunnel_core/outbound.rs b/vnt-core/src/tunnel_core/outbound.rs new file mode 100644 index 0000000..271a91d --- /dev/null +++ b/vnt-core/src/tunnel_core/outbound.rs @@ -0,0 +1,286 @@ +use crate::compression::PacketCompression; +use crate::context::{NetworkAddr, ServerInfoCollection, SharedNetworkAddr, TrafficStats}; +use crate::crypto::PacketCrypto; +use crate::fec::FecEncoder; +use crate::nat::SubnetExternalRoute; +use crate::protocol::ip_packet_protocol::{HEAD_LENGTH, MsgType, NetPacket}; +use crate::protocol::transmission::TransmissionBytes; +use crate::tunnel_core::p2p::outbound::P2pOutbound; +use crate::tunnel_core::server::outbound::ServerOutbound; +use anyhow::bail; +use bytes::Bytes; +use pnet_packet::ipv4::Ipv4Packet; +use std::net::Ipv4Addr; + +#[derive(Clone)] +pub(crate) struct BasicOutbound { + server_outbound: ServerOutbound, + p2p_outbound: Option, + packet_crypto: PacketCrypto, +} + +impl BasicOutbound { + pub fn new( + server_outbound: ServerOutbound, + p2p_outbound: Option, + packet_crypto: PacketCrypto, + ) -> Self { + Self { + server_outbound, + p2p_outbound, + packet_crypto, + } + } + + /// 获取加密保留空间大小 + pub fn encrypt_reserve(&self) -> usize { + self.packet_crypto.encrypt_reserve() + } + + /// 加密数据包 + pub fn encrypt_in_place( + &self, + packet: &mut NetPacket, + ) -> anyhow::Result<()> { + Ok(self.packet_crypto.encrypt_in_place(packet)?) + } + + /// 发送原始数据包到指定目标(通过P2P或服务器) + pub async fn send_raw( + &self, + dest: Ipv4Addr, + packet: NetPacket, + ) -> anyhow::Result<()> { + let packet = packet.into_bytes(); + if let Some(p2p) = self.p2p_outbound.as_ref() + && let Some(route) = p2p.get_route_by_id(&dest) + { + p2p.send_raw_to(packet, &route.route_key()).await?; + } else { + self.server_outbound.send_raw(dest, packet).await?; + } + Ok(()) + } + + /// 发送到默认服务器 + pub async fn send_default_raw( + &self, + packet: NetPacket, + ) -> anyhow::Result<()> { + let bytes = packet.into_buffer().into_bytes().freeze(); + self.server_outbound + .send_default_raw(NetPacket::new(bytes)?) + .await + } + + /// 广播发送 + pub async fn send_raw_broadcast( + &self, + exclude_ips: Option>, + packet: NetPacket, + ) -> anyhow::Result<()> { + self.server_outbound + .send_raw_broadcast(exclude_ips, packet) + .await + } + + /// 检查是否存在到目标的路由 + pub fn exists_route(&self, dest: &Ipv4Addr) -> bool { + if let Some(p2p) = self.p2p_outbound.as_ref() + && p2p.exists_route_by_id(dest) + { + return true; + } + self.server_outbound.exists_route(dest) + } + + /// P2P广播(内部转换类型) + pub fn p2p_broadcast_transmission( + &self, + list: &[Ipv4Addr], + max_count: usize, + packet: &NetPacket, + ) -> Option> { + if let Some(p2p) = self.p2p_outbound.as_ref() { + let vec = p2p.p2p_broadcast(list, max_count, packet); + if vec.is_empty() { None } else { Some(vec) } + } else { + None + } + } + + /// 发送加密后的数据包 + pub async fn send_encrypted_packet( + &self, + dest: Ipv4Addr, + mut packet: NetPacket, + ) -> anyhow::Result<()> { + // 加密 + self.packet_crypto.encrypt_in_place(&mut packet)?; + + // 发送 + if let Some(p2p) = self.p2p_outbound.as_ref() + && let Some(route) = p2p.get_route_by_id(&dest) + { + let bytes = packet.into_buffer().into_bytes().freeze(); + p2p.send_raw_to(NetPacket::new(bytes)?, &route.route_key()) + .await?; + } else { + let bytes = packet.into_buffer().into_bytes().freeze(); + self.server_outbound + .send_raw(dest, NetPacket::new(bytes)?) + .await?; + } + Ok(()) + } +} + +#[derive(Clone)] +pub(crate) struct HybridOutbound { + network: SharedNetworkAddr, + server_info: ServerInfoCollection, + traffic_stats: TrafficStats, + basic_outbound: BasicOutbound, + packet_compression: PacketCompression, + external_route: SubnetExternalRoute, + fec_encoder: Option, +} +impl HybridOutbound { + pub fn new( + network: SharedNetworkAddr, + server_info: ServerInfoCollection, + traffic_stats: TrafficStats, + basic_outbound: BasicOutbound, + packet_compression: PacketCompression, + external_route: SubnetExternalRoute, + fec_encoder: Option, + ) -> Self { + Self { + network, + server_info, + traffic_stats, + basic_outbound, + packet_compression, + external_route, + fec_encoder, + } + } + pub async fn outbound_raw( + &self, + dest: Ipv4Addr, + mut packet: NetPacket, + ) -> anyhow::Result<()> { + if packet.src_id() == 0 { + if let Some(ip) = self.network.ip() { + packet.set_src_id(ip.into()); + } else { + bail!("Not src ip") + } + } + + let len = packet.buffer().len() as u64; + + if let Some(fec_encoder) = &self.fec_encoder { + packet = fec_encoder.encode(packet)?; + } + + self.basic_outbound.send_raw(dest, packet).await?; + self.traffic_stats.record_tx(dest, len); + Ok(()) + } + pub async fn ipv4_outbound_common(&self, data: TransmissionBytes) -> anyhow::Result<()> { + let Some(net) = self.network.get() else { + bail!("Not src ip") + }; + self.ipv4_outbound(net, data).await + } + pub async fn ipv4_outbound( + &self, + net: NetworkAddr, + mut data: TransmissionBytes, + ) -> anyhow::Result<()> { + let Some(ipv4) = Ipv4Packet::new(data.as_ref()) else { + return Ok(()); + }; + let mut dest = ipv4.get_destination(); + let len = data.len() as u64; + data.retreat_head(HEAD_LENGTH)?; + let mut packet = NetPacket::new(data)?; + packet.set_msg_type(MsgType::Turn); + packet.set_src_id(net.ip.into()); + packet.set_ttl(5); + // 路由 + if !net.network().contains(&dest) { + if let Some(v) = self.external_route.route(&dest) { + dest = v; + } else { + return Ok(()); + } + } + packet.set_dest_id(dest.into()); + + packet = self + .packet_compression + .compress(packet, self.basic_outbound.encrypt_reserve())?; + + if let Some(fec_encoder) = &self.fec_encoder { + packet = fec_encoder.encode(packet)?; + } + + // 发送 + self.basic_outbound + .send_encrypted_packet(dest, packet) + .await?; + self.traffic_stats.record_tx(dest, len); + Ok(()) + } + pub async fn ipv4_gateway_outbound( + &self, + net: NetworkAddr, + mut data: TransmissionBytes, + ) -> anyhow::Result<()> { + data.retreat_head(HEAD_LENGTH)?; + let mut packet = NetPacket::new(data)?; + packet.set_msg_type(MsgType::Turn); + packet.set_src_id(net.ip.into()); + packet.set_dest_id(net.gateway.into()); + packet.set_ttl(5); + packet.set_gateway_flag(true); + self.basic_outbound.send_default_raw(packet).await?; + Ok(()) + } + pub async fn ipv4_broadcast_outbound( + &self, + net: NetworkAddr, + mut data: TransmissionBytes, + ) -> anyhow::Result<()> { + data.retreat_head(HEAD_LENGTH)?; + let mut packet = NetPacket::new(data)?; + packet.set_msg_type(MsgType::Broadcast); + packet.set_src_id(net.ip.into()); + packet.set_dest_id(Ipv4Addr::BROADCAST.into()); + packet.set_ttl(5); + let mut packet = self + .packet_compression + .compress(packet, self.basic_outbound.encrypt_reserve())?; + self.basic_outbound.encrypt_in_place(&mut packet)?; + let packet_bytes = packet.into_bytes(); + let list = self.server_info.client_online_ips(); + let exclude_ips = self + .basic_outbound + .p2p_broadcast_transmission(&list, 16, &packet_bytes); + if let Some(exclude_ips) = &exclude_ips + && exclude_ips.len() == list.len() + { + return Ok(()); + } + + self.basic_outbound + .send_raw_broadcast(exclude_ips, packet_bytes) + .await + } + #[allow(dead_code)] + pub fn has_route(&self, dest: &Ipv4Addr) -> bool { + self.basic_outbound.exists_route(dest) + } +} diff --git a/vnt-core/src/tunnel_core/p2p/inbound.rs b/vnt-core/src/tunnel_core/p2p/inbound.rs new file mode 100644 index 0000000..9a4e61c --- /dev/null +++ b/vnt-core/src/tunnel_core/p2p/inbound.rs @@ -0,0 +1,268 @@ +use crate::compression::PacketCompression; +use crate::context::{NetworkRoute, PacketLossStats}; +use crate::crypto::PacketCrypto; +use crate::enhanced_tunnel::inbound::EnhancedInbound; +use crate::fec::FecDecoder; +use crate::protocol::ip_packet_protocol::{HEAD_LENGTH, MsgType, NetPacket}; +use crate::protocol::transmission::TransmissionBytes; +use crate::tunnel_core::p2p::outbound::P2pOutbound; +use crate::tunnel_core::p2p::route_table::{Route, RouteTable}; +use anyhow::bail; +use rust_p2p_core::route::RouteKey; +use rust_p2p_core::tunnel::Tunnel; +use std::net::{IpAddr, Ipv4Addr}; + +struct PacketContext { + msg_type: MsgType, + src_ip: Ipv4Addr, + dest_ip: Ipv4Addr, + max_ttl: u8, + ttl: u8, +} + +pub(crate) struct P2pInboundConfig { + pub network_route: NetworkRoute, + pub route_table: RouteTable, + pub packet_loss_stats: PacketLossStats, + pub packet_crypto: PacketCrypto, + pub packet_compression: PacketCompression, + pub enhanced_inbound: EnhancedInbound, + pub fec_decoder: FecDecoder, +} + +#[derive(Clone)] +pub(crate) struct P2pInboundHandler { + network_route: NetworkRoute, + route_table: RouteTable, + packet_loss_stats: PacketLossStats, + packet_crypto: PacketCrypto, + packet_compression: PacketCompression, + enhanced_inbound: EnhancedInbound, + fec_decoder: FecDecoder, +} + +impl P2pInboundHandler { + pub fn new(config: P2pInboundConfig) -> Self { + Self { + network_route: config.network_route, + route_table: config.route_table, + packet_loss_stats: config.packet_loss_stats, + packet_crypto: config.packet_crypto, + packet_compression: config.packet_compression, + enhanced_inbound: config.enhanced_inbound, + fec_decoder: config.fec_decoder, + } + } + fn network_contains(&self, ip: &Ipv4Addr) -> bool { + self.network_route.network_contains(ip) + } + pub async fn next_handle( + &self, + buf: TransmissionBytes, + route_key: RouteKey, + p2p_socket_manager: &P2pOutbound, + tunnel: &mut Tunnel, + ) { + if let Err(e) = self + .next_handle_impl(buf, route_key, p2p_socket_manager, tunnel) + .await + { + log::warn!( + "Error while handling P2pInboundHandler: {:?},route={route_key:?}", + e + ); + } + } + async fn next_handle_impl( + &self, + buf: TransmissionBytes, + route_key: RouteKey, + p2p_socket_manager: &P2pOutbound, + tunnel: &mut Tunnel, + ) -> anyhow::Result<()> { + let mut net_packet = NetPacket::new(buf)?; + let msg_type = net_packet.msg_type()?; + let src_ip = Ipv4Addr::from(net_packet.src_id()); + let dest_ip = Ipv4Addr::from(net_packet.dest_id()); + if src_ip == dest_ip { + return Ok(()); + } + net_packet.decr_ttl(); + + let max_ttl = net_packet.max_ttl(); + let ttl = net_packet.ttl(); + if max_ttl <= ttl { + return Ok(()); + } + let Some(net) = self.network_route.network.get() else { + bail!("未找到自身IP") + }; + if net.ip != dest_ip + && !dest_ip.is_broadcast() + && !dest_ip.is_unspecified() + && dest_ip != net.broadcast + { + // 帮忙转发数据包 + if ttl >= 1 { + if let Some(route) = p2p_socket_manager.get_route_by_id(&dest_ip) { + p2p_socket_manager + .send_raw_to(net_packet.into_bytes(), &route.route_key()) + .await?; + } else { + log::debug!("未找到到 {} 的路由,无法转发", dest_ip); + } + } + return Ok(()); + } + + if msg_type == MsgType::Quic { + if net_packet.is_fec() { + let packets = self.fec_decoder.receive(net_packet)?; + if let Some(packets) = packets { + for pkt in packets { + self.enhanced_inbound + .inbound(&net, msg_type, src_ip, pkt) + .await?; + } + } + return Ok(()); + } + self.enhanced_inbound + .inbound(&net, msg_type, src_ip, net_packet) + .await?; + return Ok(()); + } + + // 解密 + self.packet_crypto.decrypt_in_place(&mut net_packet)?; + + let ctx = PacketContext { + msg_type, + src_ip, + dest_ip, + max_ttl, + ttl, + }; + + // FEC 解码(始终尝试解码,如果有 FEC 标志) + if net_packet.is_fec() { + let packets = self.fec_decoder.receive(net_packet)?; + if let Some(packets) = packets { + for pkt in packets { + let pkt = self.packet_compression.decompress(pkt)?; + self.process_decompressed_packet(&net, route_key, tunnel, pkt, &ctx) + .await?; + } + } + return Ok(()); + } + + // 解压缩 + let net_packet = self.packet_compression.decompress(net_packet)?; + self.process_decompressed_packet(&net, route_key, tunnel, net_packet, &ctx) + .await + } + + async fn process_decompressed_packet( + &self, + net: &crate::context::NetworkAddr, + route_key: RouteKey, + tunnel: &mut Tunnel, + net_packet: NetPacket, + ctx: &PacketContext, + ) -> anyhow::Result<()> { + match ctx.msg_type { + MsgType::Turn | MsgType::Broadcast | MsgType::ExcludeBroadcast => { + self.enhanced_inbound + .inbound(net, ctx.msg_type, ctx.src_ip, net_packet) + .await?; + } + MsgType::Ping => { + let metric = ctx.max_ttl - ctx.ttl; + self.route_table + .add_route_if_absent(ctx.src_ip, Route::from_default_rt(route_key, metric)); + let mut packet = NetPacket::new(TransmissionBytes::zeroed_size( + HEAD_LENGTH + 8, + self.packet_crypto.encrypt_reserve(), + ))?; + packet.set_msg_type(MsgType::Pong); + packet.set_ttl(1); + packet.set_src_id(ctx.dest_ip.into()); + packet.set_dest_id(ctx.src_ip.into()); + packet.set_payload(net_packet.payload())?; + self.packet_crypto.encrypt_in_place(&mut packet)?; + tunnel + .send_to(packet.into_bytes().into_buffer(), route_key.addr()) + .await?; + } + MsgType::Pong => { + if net_packet.payload().len() >= 8 { + let metric = ctx.max_ttl - ctx.ttl; + let time = i64::from_be_bytes(net_packet.payload()[..8].try_into()?); + let now = crate::utils::time::now_ts_ms(); + if now >= time { + self.route_table.add_route( + ctx.src_ip, + Route::from(route_key, metric, (now - time) as _), + ); + self.packet_loss_stats.record_received(ctx.src_ip); + } + } + } + MsgType::PunchStart1 => {} + MsgType::PunchStart2 => {} + MsgType::PunchReq => { + if let IpAddr::V4(ip) = route_key.addr().ip() + && self.network_contains(&ip) + { + log::info!("===========loop PunchReq {route_key:?} {:?}", ctx.src_ip); + return Ok(()); + } + log::info!( + "PunchReq 打洞成功 {}->{},route={route_key:?}", + ctx.src_ip, + ctx.dest_ip + ); + self.route_table.add_owner_route(ctx.src_ip, route_key); + let mut packet = NetPacket::new(TransmissionBytes::zeroed_size( + HEAD_LENGTH + 8, + self.packet_crypto.encrypt_reserve(), + ))?; + packet.set_msg_type(MsgType::PunchRes); + packet.set_ttl(1); + packet.set_src_id(ctx.dest_ip.into()); + packet.set_dest_id(ctx.src_ip.into()); + packet.set_payload(&crate::utils::time::now_ts_ms().to_be_bytes())?; + + self.packet_crypto.encrypt_in_place(&mut packet)?; + tunnel + .send_to(packet.into_bytes().into_buffer(), route_key.addr()) + .await?; + } + MsgType::PunchRes => { + if let IpAddr::V4(ip) = route_key.addr().ip() + && self.network_contains(&ip) + { + log::info!("===========loop PunchRes {route_key:?} {:?}", ctx.src_ip); + return Ok(()); + } + log::info!( + "PunchRes 打洞成功 {}->{},route={route_key:?}", + ctx.src_ip, + ctx.dest_ip + ); + self.route_table.add_owner_route(ctx.src_ip, route_key); + } + MsgType::PingTurn => {} + MsgType::PongTurn => {} + _ => {} + } + Ok(()) + } + + pub async fn tcp_disconnect(&self, route_key: RouteKey) { + if let Some(ip) = self.route_table.get_id_by_route_key(&route_key) { + self.route_table.remove_route(&ip, &route_key); + } + } +} diff --git a/vnt-core/src/tunnel_core/p2p/mod.rs b/vnt-core/src/tunnel_core/p2p/mod.rs new file mode 100644 index 0000000..220b13a --- /dev/null +++ b/vnt-core/src/tunnel_core/p2p/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod inbound; +pub(crate) mod outbound; +pub(crate) mod transport; + +pub(crate) mod route_table; diff --git a/vnt-core/src/tunnel_core/p2p/outbound.rs b/vnt-core/src/tunnel_core/p2p/outbound.rs new file mode 100644 index 0000000..962041a --- /dev/null +++ b/vnt-core/src/tunnel_core/p2p/outbound.rs @@ -0,0 +1,133 @@ +use crate::crypto::PacketCrypto; +use crate::protocol::ip_packet_protocol::NetPacket; +use crate::protocol::transmission::TransmissionBytes; +use crate::tunnel_core::p2p::route_table::{Route, RouteTable}; +use bytes::Bytes; +use rust_p2p_core::route::RouteKey; +use rust_p2p_core::tunnel::SocketManager; +use std::net::Ipv4Addr; + +#[derive(Clone)] +pub(crate) struct P2pOutbound { + manager: SocketManager, + route_table: RouteTable, + packet_crypto: PacketCrypto, +} +impl P2pOutbound { + pub fn new( + manager: SocketManager, + route_table: RouteTable, + packet_crypto: PacketCrypto, + ) -> Self { + Self { + manager, + route_table, + packet_crypto, + } + } + pub fn encrypt_reserve(&self) -> usize { + self.packet_crypto.encrypt_reserve() + } + // pub async fn send_raw(&self, buf: NetPacket) -> anyhow::Result<()> { + // let dest_id = Ipv4Addr::from(buf.dest_id()); + // let route = self.route_table.get_route_by_id(&dest_id)?; + // self.manager + // .send_to(buf.into_buffer(), &route.route_key()) + // .await?; + // Ok(()) + // } + // pub async fn send(&self, mut buf: NetPacket) -> anyhow::Result<()> { + // let dest_id = Ipv4Addr::from(buf.dest_id()); + // let route = self.route_table.get_route_by_id(&dest_id)?; + // self.packet_crypto.encrypt_in_place(&mut buf)?; + // self.manager + // .send_to(buf.into_buffer().into_bytes().freeze(), &route.route_key()) + // .await?; + // Ok(()) + // } + pub async fn send_raw_to( + &self, + buf: NetPacket, + route_key: &RouteKey, + ) -> anyhow::Result<()> { + self.manager.send_to(buf.into_buffer(), route_key).await?; + Ok(()) + } + pub async fn send_to( + &self, + mut buf: NetPacket, + route_key: &RouteKey, + ) -> anyhow::Result<()> { + self.packet_crypto.encrypt_in_place(&mut buf)?; + self.manager + .send_to(buf.into_buffer().into_bytes().freeze(), route_key) + .await?; + Ok(()) + } + pub fn get_route_by_id(&self, id: &Ipv4Addr) -> Option { + self.route_table.get_route_by_id(id).ok() + } + pub fn get_p2p_route_by_id(&self, id: &Ipv4Addr) -> Option { + self.route_table.get_route_by_id(id).ok().filter(|v| v.is_direct()) + } + pub fn exists_route_by_id(&self, id: &Ipv4Addr) -> bool { + self.route_table.exists(id) + } + + // pub async fn send_to_id( + // &self, + // buf: NetPacket, + // id: &Ipv4Addr, + // ) -> anyhow::Result { + // let Ok(route) = self.route_table.get_route_by_id(id) else { + // return Ok(false); + // }; + // self.send_to(buf, &route.route_key()).await?; + // Ok(true) + // } + // pub fn try_send_to_id( + // &self, + // buf: NetPacket, + // id: &Ipv4Addr, + // ) -> anyhow::Result { + // let Ok(route) = self.route_table.get_route_by_id(id) else { + // return Ok(false); + // }; + // self.try_send_to(buf, &route.route_key())?; + // Ok(true) + // } + // pub fn try_send_to( + // &self, + // buf: NetPacket, + // route_key: &RouteKey, + // ) -> anyhow::Result<()> { + // self.manager + // .try_send_to(buf.into_buffer().into_bytes(), route_key)?; + // Ok(()) + // } + pub fn p2p_broadcast( + &self, + ips: &[Ipv4Addr], + max: usize, + buf: &NetPacket, + ) -> Vec { + let mut list = Vec::with_capacity(ips.len().min(max)); + + for id in ips { + let Some(route) = self.get_p2p_route_by_id(id) else { + continue; + }; + if self + .manager + .try_send_to(buf.source_buf().clone(), &route.route_key()) + .is_ok() + { + list.push(*id); + if list.len() >= max { + break; + } + } + } + list + } +} diff --git a/vnt-core/src/tunnel_core/p2p/route_table.rs b/vnt-core/src/tunnel_core/p2p/route_table.rs new file mode 100644 index 0000000..64a972a --- /dev/null +++ b/vnt-core/src/tunnel_core/p2p/route_table.rs @@ -0,0 +1,265 @@ +use parking_lot::{Mutex, RwLock}; +use rust_p2p_core::route::{RouteKey, DEFAULT_RTT}; +use std::collections::HashMap; +use std::net::Ipv4Addr; +use std::sync::Arc; +use std::time::Instant; + + +#[derive(Copy, Clone, Debug)] +pub struct Route { + route_key: RouteKey, + metric: u8, + rtt: u32, +} +impl Route { + pub fn from(route_key: RouteKey, metric: u8, rtt: u32) -> Self { + Self { + route_key, + metric, + rtt, + } + } + pub fn from_default_rt(route_key: RouteKey, metric: u8) -> Self { + Self { + route_key, + metric, + rtt: DEFAULT_RTT, + } + } + pub fn route_key(&self) -> RouteKey { + self.route_key + } + + pub fn is_direct(&self) -> bool { + self.metric == 1 + } + pub fn rtt(&self) -> u32 { + self.rtt + } + pub fn metric(&self) -> u8 { + self.metric + } +} + +#[derive(Clone)] +pub struct RouteTable { + inner: Arc, +} + +#[derive(Default)] +struct RouteTableInner { + route_table: RwLock>>, + route_key_time: Mutex>, + route_key_owner: Mutex>, +} + +impl Default for RouteTable { + fn default() -> Self { + Self::new() + } +} + +impl RouteTable { + pub fn new() -> Self { + Self { + inner: Arc::new(RouteTableInner::default()), + } + } + + /// 获取指定 ID 的最优路由 + pub fn get_route_by_id(&self, id: &Ipv4Addr) -> anyhow::Result { + self.inner + .get_by_id(id) + .ok_or_else(|| anyhow::anyhow!("route not found for {}", id)) + } + + /// 检查是否存在到指定 ID 的路由 + pub fn exists(&self, id: &Ipv4Addr) -> bool { + self.inner.get_by_id(id).is_some() + } + + /// 判断是否需要打洞(没有路由或只有中继路由) + pub fn need_punch(&self, id: &Ipv4Addr) -> bool { + let guard = self.inner.route_table.read(); + let Some(list) = guard.get(id) else { + return true; + }; + // 如果没有直连路由(metric=1),则需要打洞 + !list.iter().any(|r| r.is_direct()) + } + + /// 获取直连路由数量(用于判断是否直连) + pub fn p2p_num(&self, id: &Ipv4Addr) -> usize { + let guard = self.inner.route_table.read(); + let Some(list) = guard.get(id) else { + return 0; + }; + list.iter().filter(|r| r.is_direct()).count() + } + + /// 添加 owner 路由(打洞请求响应时调用) + pub fn add_owner_route(&self, id: Ipv4Addr, key: RouteKey) { + self.inner.add_owner_route(id, key); + } + + /// 添加路由(心跳时调用,用于更新路由时间和添加跨节点转发路由) + pub fn add_route(&self, id: Ipv4Addr, route: Route) { + self.inner.add_route(id, route); + } + + /// 如果路由不存在则添加(用于 Ping 消息) + pub fn add_route_if_absent(&self, id: Ipv4Addr, route: Route) { + let guard = self.inner.route_table.read(); + if guard.contains_key(&id) { + return; + } + drop(guard); + self.inner.add_route(id, route); + } + + /// 获取所有路由表 + pub fn route_table(&self) -> Vec<(Ipv4Addr, Vec)> { + let guard = self.inner.route_table.read(); + guard.iter().map(|(k, v)| (*k, v.clone())).collect() + } + + /// 根据 RouteKey 查找对应的 IP + pub fn get_id_by_route_key(&self, route_key: &RouteKey) -> Option { + let owner_map = self.inner.route_key_owner.lock(); + owner_map.get(route_key).copied() + } + + /// 移除指定 IP 和 RouteKey 的路由 + pub fn remove_route(&self, id: &Ipv4Addr, route_key: &RouteKey) { + let mut table = self.inner.route_table.write(); + let mut owner_map = self.inner.route_key_owner.lock(); + let mut time_map = self.inner.route_key_time.lock(); + + if let Some(list) = table.get_mut(id) { + list.retain(|r| r.route_key() != *route_key); + if list.is_empty() { + table.remove(id); + } + } + + if let Some(owner_id) = owner_map.get(route_key) { + if owner_id == id { + owner_map.remove(route_key); + } + } + + time_map.remove(&(*id, *route_key)); + } + + /// 移除过期的路由 + pub fn remove_oldest_route(&self, expired_time: Instant) { + self.inner.remove_oldest_route(expired_time); + } +} + +impl RouteTableInner { + fn get_by_id(&self, id: &Ipv4Addr) -> Option { + let guard = self.route_table.read(); + let list = guard.get(id)?; + list.first().cloned() + } + + fn add_owner_route(&self, id: Ipv4Addr, key: RouteKey) { + let route = Route::from_default_rt(key, 1); + let mut guard = self.route_table.write(); + + self.route_key_owner.lock().insert(key, id); + self.route_key_time.lock().insert((id, key), Instant::now()); + + let list = guard.entry(id).or_insert_with(|| Vec::with_capacity(6)); + if list.iter().any(|v| v.route_key() == key) { + return; + } + list.push(route); + } + + fn add_route(&self, id: Ipv4Addr, route: Route) { + let key = route.route_key(); + let mut guard = self.route_table.write(); + + // 检查是否是 owner 路由 + let mut route_key_owner = self.route_key_owner.lock(); + if route.is_direct() { + route_key_owner.entry(key).or_insert(id); + } else { + if !guard.contains_key(&id) { + return; + } + } + + // 更新时间 + self.route_key_time.lock().insert((id, key), Instant::now()); + + let list = guard.entry(id).or_insert_with(|| Vec::with_capacity(6)); + + // 如果路由已存��,更新并重新排序 + if let Some(idx) = list.iter().position(|v| v.route_key() == key) { + list[idx] = route; + // 向前冒泡(如果 RTT 更小) + let mut i = idx; + while i > 0 && list[i].rtt() < list[i - 1].rtt() { + list.swap(i, i - 1); + i -= 1; + } + // 向后冒泡(如果 RTT 更大) + while i + 1 < list.len() && list[i].rtt() > list[i + 1].rtt() { + list.swap(i, i + 1); + i += 1; + } + return; + } + + // 插入新路由,保持按 RTT 排序 + let mut pos = list.len(); + for (i, r) in list.iter().enumerate() { + if route.rtt() < r.rtt() { + pos = i; + break; + } + } + list.insert(pos, route); + } + + fn remove_oldest_route(&self, expired_time: Instant) { + let mut expired_keys = Vec::new(); + { + let mut time_map = self.route_key_time.lock(); + time_map.retain(|(id, route_key), t| { + if *t <= expired_time { + expired_keys.push((*id, *route_key)); + false + } else { + true + } + }); + } + + if expired_keys.is_empty() { + return; + } + + let mut table = self.route_table.write(); + let mut owner_map = self.route_key_owner.lock(); + + for (id, route_key) in expired_keys { + if let Some(list) = table.get_mut(&id) { + list.retain(|r| r.route_key() != route_key); + if list.is_empty() { + table.remove(&id); + } + } + + if let Some(owner_id) = owner_map.get(&route_key) { + if *owner_id == id { + owner_map.remove(&route_key); + } + } + } + } +} diff --git a/vnt-core/src/tunnel_core/p2p/transport/mod.rs b/vnt-core/src/tunnel_core/p2p/transport/mod.rs new file mode 100644 index 0000000..15d8925 --- /dev/null +++ b/vnt-core/src/tunnel_core/p2p/transport/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod nat_test; +pub(crate) mod punch; + +pub(crate) mod task; diff --git a/vnt-core/src/tunnel_core/p2p/transport/nat_test.rs b/vnt-core/src/tunnel_core/p2p/transport/nat_test.rs new file mode 100644 index 0000000..604472c --- /dev/null +++ b/vnt-core/src/tunnel_core/p2p/transport/nat_test.rs @@ -0,0 +1,387 @@ +use crate::context::AppState; +use rust_p2p_core::nat::{NatInfo, NatType}; +use rust_p2p_core::tunnel::SocketManager; +use rust_p2p_core::tunnel::udp::Model; +use std::collections::HashMap; +use std::io; +use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::time::Duration; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; + +pub async fn my_nat_info(app_context: AppState, socket_manager: SocketManager) { + loop { + my_nat_info_impl(&app_context, &socket_manager).await; + tokio::time::sleep(Duration::from_secs(60 * 30)).await; + } +} +async fn my_nat_info_impl(app_context: &AppState, socket_manager: &SocketManager) { + let network = app_context.network.network(); + let mut local_ipv4s = Vec::new(); + let mut local_ipv6 = Vec::new(); + match getifaddrs::getifaddrs() { + Ok(addrs) => { + for x in addrs { + let Some(ip) = x.address.ip_addr() else { + continue; + }; + if ip.is_loopback() { + continue; + } + if ip.is_unspecified() { + continue; + } + if ip.is_multicast() { + continue; + } + + match ip { + IpAddr::V4(addr) => { + if addr.is_documentation() { + continue; + } + if addr.is_broadcast() { + continue; + } + if let Some(network) = &network + && network.contains(&addr) + { + continue; + } + local_ipv4s.push(addr); + } + IpAddr::V6(addr) => { + if addr.is_unique_local() { + continue; + } + if addr.is_unicast_link_local() { + continue; + } + local_ipv6.push(addr); + } + } + } + } + Err(e) => { + log::error!("getifaddrs error: {e}"); + } + } + log::info!("local_ipv4s: {:?}", local_ipv4s); + let local_ipv4 = rust_p2p_core::extend::addr::local_ipv4() + .await + .unwrap_or_else(|e| { + log::warn!("local ipv4 failed {e:?}"); + local_ipv4s + .first() + .cloned() + .unwrap_or(Ipv4Addr::UNSPECIFIED) + }); + local_ipv4s = vec![local_ipv4]; + let mut ipv6 = rust_p2p_core::extend::addr::local_ipv6().await.ok(); + if let Some(addr) = ipv6 { + if addr.is_loopback() + || addr.is_unique_local() + || addr.is_unicast_link_local() + || addr.is_unspecified() + || addr.is_multicast() + { + ipv6 = local_ipv6.first().cloned(); + } + } else { + ipv6 = local_ipv6.first().cloned(); + } + let local_udp_ports = socket_manager + .udp_socket_manager_as_ref() + .unwrap() + .local_ports() + .unwrap(); + let local_tcp_port = socket_manager + .tcp_socket_manager_as_ref() + .unwrap() + .local_addr() + .port(); + log::info!( + "local_ipv4={local_ipv4},ipv6={ipv6:?},local_udp_ports:{local_udp_ports:?},local_tcp_port:{local_tcp_port:?}" + ); + let mut public_ports = local_udp_ports.clone(); + public_ports.fill(0); + let mut nat_info = NatInfo { + nat_type: NatType::Cone, + public_ips: vec![], + public_udp_ports: public_ports, + mapping_tcp_addr: vec![], + mapping_udp_addr: vec![], + public_port_range: 0, + local_ipv4s, + local_ipv4, + ipv6, + local_udp_ports, + local_tcp_port, + public_tcp_port: 0, + }; + let mut stun_server = app_context.udp_stun(); + if stun_server.is_empty() { + stun_server = default_udp_stun(); + } + let (nat_type, public_ips, port_range) = rust_p2p_core::stun::stun_test_nat(stun_server, None) + .await + .unwrap_or_else(|e| { + log::warn!("stun_test_nat {e:?}"); + (NatType::Cone, vec![], 0) + }); + log::info!("nat_type:{nat_type:?},public_ips:{public_ips:?},port_range={port_range}"); + nat_info.nat_type = nat_type; + nat_info.public_ips = public_ips; + nat_info.public_port_range = port_range; + app_context.nat_info.replace_nat_info(nat_info); + let model = match nat_type { + NatType::Cone => Model::Low, + NatType::Symmetric => Model::High, + }; + if let Err(e) = socket_manager + .udp_socket_manager_as_ref() + .unwrap() + .switch_model(model) + { + log::error!("switch_model error: {e:?}"); + } +} + +pub async fn query_udp_public_addr_loop(app_context: AppState, socket_manager: SocketManager) { + let mut udp_stun_servers = app_context.udp_stun(); + if udp_stun_servers.is_empty() { + udp_stun_servers = default_udp_stun(); + } + let udp_len = udp_stun_servers.len(); + let mut udp_count = 0; + let stun_request = rust_p2p_core::stun::send_stun_request(); + loop { + let stun = &udp_stun_servers[udp_count % udp_len]; + udp_count += 1; + match tokio::net::lookup_host(stun.as_str()).await { + Ok(mut addr) => { + if let Some(addr) = addr.next() + && let Some(w) = socket_manager.udp_socket_manager_as_ref() + && let Err(e) = w.detect_pub_addrs(&stun_request, addr).await + { + log::info!("detect_pub_addrs {e:?} {addr:?}"); + } + } + Err(e) => { + log::info!("query_public_addr lookup_host {e:?} {stun:?}",); + } + } + let not_port = app_context + .get_nat_info() + .map(|v| v.public_udp_ports.contains(&0)) + .unwrap_or(true); + if not_port { + tokio::time::sleep(Duration::from_secs(2)).await; + } else { + tokio::time::sleep(Duration::from_secs(60)).await; + } + } +} + +pub(crate) async fn query_tcp_public_addr_loop( + app_context: AppState, + socket_manager: SocketManager, +) { + use rand::Rng; + use rand::seq::SliceRandom; + + let tcp_stun_servers = { + let servers = app_context.tcp_stun(); + if servers.is_empty() { + default_tcp_stun() + } else { + servers + } + }; + + if tcp_stun_servers.is_empty() { + return; + } + log::debug!("tcp_stun_servers = {tcp_stun_servers:?}"); + + let stun_request = rust_p2p_core::stun::send_stun_request(); + let target_conn_count = tcp_stun_servers.len().min(2); + let mut active_connections: HashMap = HashMap::new(); + + 'outer: loop { + while active_connections.len() < target_conn_count { + let mut candidates: Vec<&String> = tcp_stun_servers.iter().collect(); + candidates.shuffle(&mut rand::rng()); + + let mut connected = false; + for stun in candidates { + let addr = match tokio::net::lookup_host(stun.as_str()).await { + Ok(mut addrs) => addrs.next(), + Err(e) => { + log::debug!("lookup_host failed {stun} {e}"); + continue; + } + }; + + let Some(addr) = addr else { + continue; + }; + + if active_connections.contains_key(&addr) { + continue; + } + + let Some(w) = socket_manager.tcp_socket_manager_as_ref() else { + continue; + }; + + match tokio::time::timeout(Duration::from_secs(5), w.connect_reuse_port_raw(addr)) + .await + { + Ok(Ok(mut tcp_stream)) => { + let write_result = tokio::time::timeout( + Duration::from_secs(5), + tcp_stream.write_all(&stun_request), + ) + .await; + + if let Ok(Ok(_)) = write_result { + match stun_tcp_read(&mut tcp_stream).await { + Ok(pub_addr) => { + log::debug!( + "update_tcp_public_addr {stun} {addr} -> {pub_addr}" + ); + + let existing_pub_addr = + active_connections.values().next().map(|(_, p)| *p); + + if let Some(existing) = existing_pub_addr + && existing != pub_addr + { + log::debug!( + "pub_addr mismatch: {existing} != {pub_addr}, wait 60s" + ); + active_connections.clear(); + app_context.nat_info.update_tcp_public_addr( + SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0).into(), + ); + tokio::time::sleep(Duration::from_secs(5 * 60)).await; + continue 'outer; + } + + active_connections.insert(addr, (tcp_stream, pub_addr)); + connected = true; + break; + } + Err(e) => { + log::debug!("stun_tcp_read failed {stun} {addr} {e}"); + } + } + } else { + log::debug!("write stun request failed {stun} {addr}"); + } + } + Ok(Err(e)) => { + log::debug!("connect_reuse_port_raw failed {stun} {addr} {e}"); + } + Err(_) => { + log::debug!("connect_reuse_port_raw timeout {stun} {addr}"); + } + } + } + + if !connected { + break; + } + } + let existing_pub_addr = active_connections.values().next().map(|(_, p)| *p); + if let Some(existing) = existing_pub_addr { + app_context.nat_info.update_tcp_public_addr(existing); + } + + let sleep_secs = rand::rng().random_range(10u64..=15); + tokio::time::sleep(Duration::from_secs(sleep_secs)).await; + + let mut to_remove = Vec::new(); + let addrs: Vec = active_connections.keys().cloned().collect(); + + for addr in addrs { + let (tcp_stream, _) = active_connections.get_mut(&addr).unwrap(); + let mut buf = [0u8; 1024]; + + match tcp_stream.try_read(&mut buf) { + Ok(0) => { + log::warn!("stun tcp close {addr} EOF"); + to_remove.push(addr); + continue; + } + Err(e) if e.kind() != std::io::ErrorKind::WouldBlock => { + log::warn!("stun tcp read error {addr} {e}"); + to_remove.push(addr); + continue; + } + _ => {} + } + + match tokio::time::timeout(Duration::from_secs(3), tcp_stream.write_all(&stun_request)) + .await + { + Ok(Ok(_)) => {} + Ok(Err(e)) => { + log::warn!("stun tcp write error {addr} {e}"); + to_remove.push(addr); + } + Err(_) => { + log::warn!("stun tcp write timeout {addr}"); + to_remove.push(addr); + } + } + } + + for addr in to_remove { + active_connections.remove(&addr); + } + } +} + +async fn stun_tcp_read(tcp_stream: &mut TcpStream) -> io::Result { + let mut head = [0; 20]; + match tokio::time::timeout(Duration::from_secs(5), tcp_stream.read_exact(&mut head)).await { + Ok(rs) => rs?, + Err(_) => Err(io::Error::from(io::ErrorKind::TimedOut))?, + }; + let len = u16::from_be_bytes([head[2], head[3]]) as usize; + let mut buf = vec![0; len + 20]; + buf[..20].copy_from_slice(&head); + match tokio::time::timeout( + Duration::from_secs(5), + tcp_stream.read_exact(&mut buf[20..]), + ) + .await + { + Ok(rs) => rs?, + Err(_) => Err(io::Error::from(io::ErrorKind::TimedOut))?, + }; + if let Some(addr) = rust_p2p_core::stun::recv_stun_response(&buf) { + Ok(addr) + } else { + log::debug!("stun_tcp_read {buf:?}"); + Err(io::Error::from(io::ErrorKind::InvalidData)) + } +} + +fn default_udp_stun() -> Vec { + vec![ + "stun.miwifi.com:3478".to_string(), + "stun.chat.bilibili.com:3478".to_string(), + "stun.l.google.com:19302".to_string(), + ] +} + +fn default_tcp_stun() -> Vec { + vec![ + "stun.flashdance.cx:3478".to_string(), + "stun.sipnet.net:3478".to_string(), + "stun.nextcloud.com:443".to_string(), + ] +} diff --git a/vnt-core/src/tunnel_core/p2p/transport/punch.rs b/vnt-core/src/tunnel_core/p2p/transport/punch.rs new file mode 100644 index 0000000..1faa917 --- /dev/null +++ b/vnt-core/src/tunnel_core/p2p/transport/punch.rs @@ -0,0 +1,148 @@ +use crate::context::nat::PunchBackoff; +use crate::context::{ServerInfoCollection, SharedNetworkAddr}; +use crate::crypto::PacketCrypto; +use crate::protocol::client_message::PunchInfo; +use crate::protocol::ip_packet_protocol::{HEAD_LENGTH, MsgType, NetPacket}; +use crate::protocol::transmission::TransmissionBytes; +use crate::tunnel_core::p2p::route_table::RouteTable; +use crate::tunnel_core::server::outbound::ServerOutbound; +use anyhow::bail; +use log::error; +use rand::seq::SliceRandom; +use rust_p2p_core::punch::{PunchModel, Puncher}; +use std::net::Ipv4Addr; +use std::time::Duration; + +pub struct PunchTaskContext { + pub network: SharedNetworkAddr, + pub server_info: ServerInfoCollection, + pub punch_backoff: PunchBackoff, + pub punch_info_getter: PunchInfoGetter, +} + +pub type PunchInfoGetter = std::sync::Arc Option + Send + Sync>; + +pub async fn punch_task( + tunnel_to_server: ServerOutbound, + route_table: RouteTable, + ctx: PunchTaskContext, +) -> anyhow::Result<()> { + loop { + tokio::time::sleep(Duration::from_secs(5)).await; + let Some(src_ip) = ctx.network.ip() else { + continue; + }; + let Some(punch_info) = (ctx.punch_info_getter)() else { + continue; + }; + let mut list = ctx.server_info.client_online_ips(); + list.shuffle(&mut rand::rng()); + list.truncate(5); + for dest_ip in list { + if dest_ip <= src_ip { + continue; + } + if ctx.server_info.is_any_server_connected(None) && route_table.need_punch(&dest_ip) { + if !ctx.punch_backoff.should_punch(dest_ip) { + continue; + } + log::info!("punching {dest_ip}"); + + let data = punch_info.encode(); + let mut net_packet = NetPacket::new(TransmissionBytes::zeroed_size( + HEAD_LENGTH + data.len(), + tunnel_to_server.encrypt_reserve(), + ))?; + net_packet.set_msg_type(MsgType::PunchStart1); + net_packet.set_ttl(2); + net_packet.set_src_id(src_ip.into()); + net_packet.set_dest_id(dest_ip.into()); + net_packet.set_payload(data.as_ref())?; + if let Err(e) = tunnel_to_server.send(dest_ip, net_packet).await { + error!("punch send error {:?}", e); + } + } + } + } +} +#[derive(Clone)] +pub struct NatPuncher { + network: SharedNetworkAddr, + punch_backoff: PunchBackoff, + puncher: Option, + packet_crypto: PacketCrypto, +} + +impl NatPuncher { + pub fn new( + network: SharedNetworkAddr, + punch_backoff: PunchBackoff, + puncher: Option, + packet_crypto: PacketCrypto, + ) -> Self { + Self { + network, + punch_backoff, + puncher, + packet_crypto, + } + } + pub fn punch(&self, dest_ip: Ipv4Addr, punch_info: PunchInfo) -> anyhow::Result { + if self.puncher.is_none() { + return Ok(false); + } + if !self.punch_backoff.should_punch(dest_ip) { + return Ok(false); + } + self.punch_uncheck_delay(dest_ip, punch_info, Some(Duration::from_millis(50)))?; + Ok(true) + } + pub fn punch_uncheck(&self, dest_ip: Ipv4Addr, punch_info: PunchInfo) -> anyhow::Result<()> { + self.punch_uncheck_delay(dest_ip, punch_info, None) + } + pub fn punch_uncheck_delay( + &self, + dest_ip: Ipv4Addr, + punch_info: PunchInfo, + time: Option, + ) -> anyhow::Result<()> { + let Some(puncher) = self.puncher.clone() else { + return Ok(()); + }; + let Some(src_ip) = self.network.ip() else { + bail!("not ip"); + }; + let packet_crypto = self.packet_crypto.clone(); + tokio::spawn(async move { + if let Some(time) = time { + tokio::time::sleep(time).await; + } + if let Err(e) = punch_now(puncher, src_ip, dest_ip, punch_info, packet_crypto).await { + log::warn!("punch send error {:?}", e); + } + }); + Ok(()) + } +} +async fn punch_now( + puncher: Puncher, + src_ip: Ipv4Addr, + dest_ip: Ipv4Addr, + nat_info: PunchInfo, + packet_crypto: PacketCrypto, +) -> anyhow::Result<()> { + let mut packet = NetPacket::new(TransmissionBytes::zeroed_size( + HEAD_LENGTH + 8, + packet_crypto.encrypt_reserve(), + ))?; + packet.set_msg_type(MsgType::PunchReq); + packet.set_ttl(1); + packet.set_src_id(src_ip.into()); + packet.set_dest_id(dest_ip.into()); + packet.set_payload(&crate::utils::time::now_ts_ms().to_be_bytes())?; + packet_crypto.encrypt_in_place(&mut packet)?; + let buf = packet.buffer(); + let punch_info = rust_p2p_core::punch::PunchInfo::new(PunchModel::all(), nat_info.nat_info); + puncher.punch_now(Some(buf), buf, punch_info).await?; + Ok(()) +} diff --git a/vnt-core/src/tunnel_core/p2p/transport/task.rs b/vnt-core/src/tunnel_core/p2p/transport/task.rs new file mode 100644 index 0000000..b82ec21 --- /dev/null +++ b/vnt-core/src/tunnel_core/p2p/transport/task.rs @@ -0,0 +1,337 @@ +use crate::context::nat::MyNatInfo; +use crate::context::{AppState, PacketLossStats, SharedNetworkAddr}; +use crate::crypto::PacketCrypto; +use crate::protocol::ip_packet_protocol::{HEAD_LENGTH, MsgType, NetPacket}; +use crate::protocol::transmission::TransmissionBytes; +use crate::tunnel_core::p2p::inbound::P2pInboundHandler; +use crate::tunnel_core::p2p::outbound::P2pOutbound; +use crate::tunnel_core::p2p::route_table::RouteTable; +use crate::tunnel_core::p2p::transport::nat_test::{ + my_nat_info, query_tcp_public_addr_loop, query_udp_public_addr_loop, +}; +use crate::tunnel_core::p2p::transport::punch::{PunchTaskContext, punch_task}; +use crate::tunnel_core::server::outbound::ServerOutbound; +use crate::utils::task_control::TaskGroup; +use rust_p2p_core::punch::Puncher; +use rust_p2p_core::tunnel::{Tunnel, TunnelDispatcher, new_tunnel_component}; +use std::net::Ipv4Addr; +use std::sync::Arc; +use std::time::Duration; + +pub async fn init_tunnel( + task_group: TaskGroup, + app_state: AppState, + tunnel_to_server: ServerOutbound, + packet_crypto: PacketCrypto, +) -> anyhow::Result<(Puncher, P2pOutbound, P2pTask)> { + let udp_config = rust_p2p_core::tunnel::config::UdpTunnelConfig::default() + .set_main_udp_count(2) + .set_sub_udp_count(82); + let tcp_config = rust_p2p_core::tunnel::config::TcpTunnelConfig::new(Box::new( + rust_p2p_core::tunnel::tcp::LengthPrefixedInitCodec, + )) + .set_tcp_multiplexing_limit(2); + let config = rust_p2p_core::tunnel::config::TunnelConfig::empty() + .set_udp_tunnel_config(udp_config) + .set_tcp_tunnel_config(tcp_config); + let (tunnel_dispatcher, puncher) = new_tunnel_component(config)?; + let route_table = app_state.route_table.clone(); + let socket_manager = P2pOutbound::new( + tunnel_dispatcher.socket_manager(), + route_table.clone(), + packet_crypto, + ); + task_group.spawn(my_nat_info( + app_state.clone(), + tunnel_dispatcher.socket_manager(), + )); + let manager = tunnel_dispatcher.socket_manager(); + task_group.spawn(query_udp_public_addr_loop( + app_state.clone(), + manager.clone(), + )); + task_group.spawn(query_tcp_public_addr_loop(app_state.clone(), manager)); + + task_group.spawn(route_timeout_task(route_table.clone())); + let app_state_for_punch = app_state.clone(); + let punch_ctx = PunchTaskContext { + network: app_state.network.clone(), + server_info: app_state.server_info_collection.clone(), + punch_backoff: app_state.punch_backoff.clone(), + punch_info_getter: Arc::new(move || app_state_for_punch.get_punch_info()), + }; + task_group.spawn(punch_task(tunnel_to_server, route_table.clone(), punch_ctx)); + task_group.spawn(ping_all( + app_state.network.clone(), + app_state.packet_loss_stats.clone(), + route_table.clone(), + socket_manager.clone(), + )); + task_group.spawn(relay_probe_task( + app_state.network.clone(), + app_state.server_info_collection.clone(), + route_table.clone(), + socket_manager.clone(), + )); + let p2p_task = P2pTask { + task_group, + nat_info: app_state.nat_info.clone(), + socket_manager: socket_manager.clone(), + tunnel_dispatcher, + }; + Ok((puncher, socket_manager, p2p_task)) +} +pub struct P2pTask { + task_group: TaskGroup, + nat_info: MyNatInfo, + socket_manager: P2pOutbound, + tunnel_dispatcher: TunnelDispatcher, +} +impl P2pTask { + pub fn start(self, p2p_inbound_handler: P2pInboundHandler) { + self.task_group.spawn(tunnel_dispatch_task( + self.nat_info, + self.task_group.clone(), + self.tunnel_dispatcher, + p2p_inbound_handler, + self.socket_manager, + )); + } +} + +pub async fn ping_all( + network: SharedNetworkAddr, + packet_loss_stats: PacketLossStats, + route_table: RouteTable, + socket_manager: P2pOutbound, +) { + loop { + tokio::time::sleep(Duration::from_secs(5)).await; + let Some(src) = network.ip() else { + continue; + }; + let vec = route_table.route_table(); + + for (id, list) in vec { + for (index, route) in list.iter().enumerate() { + if index > 2 { + break; + } + let Ok(mut ping) = NetPacket::new(TransmissionBytes::zeroed_size( + HEAD_LENGTH + 8, + socket_manager.encrypt_reserve(), + )) else { + continue; + }; + ping.set_msg_type(MsgType::Ping); + ping.set_ttl(1); + ping.set_src_id(src.into()); + ping.set_dest_id(id.into()); + ping.set_payload(&crate::utils::time::now_ts_ms().to_be_bytes()) + .unwrap(); + if socket_manager + .send_to(ping, &route.route_key()) + .await + .is_ok() + { + packet_loss_stats.record_sent(id); + } + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + } +} +pub async fn route_timeout_task(route_table: RouteTable) { + loop { + tokio::time::sleep(Duration::from_secs(10)).await; + let expired_time = std::time::Instant::now() - Duration::from_secs(10); + route_table.remove_oldest_route(expired_time); + } +} + +/// 客户端中继探测任务 +/// 每5分钟执行一次,找到所有未直连的目标IP,通过Ping消息发送给已打洞的客户端 +pub async fn relay_probe_task( + network: SharedNetworkAddr, + server_info: crate::context::ServerInfoCollection, + route_table: RouteTable, + socket_manager: P2pOutbound, +) { + use rand::prelude::*; + + loop { + tokio::time::sleep(Duration::from_secs(300)).await; // 5分钟 + + let Some(src) = network.ip() else { + continue; + }; + + let online_ips = server_info.client_online_ips(); + if online_ips.is_empty() { + continue; + } + + let mut non_direct_targets = Vec::new(); + for ip in online_ips { + if ip == src { + continue; + } + + let is_direct = route_table + .get_route_by_id(&ip) + .ok() + .map(|route| route.is_direct()) + .unwrap_or(false); + + if !is_direct { + non_direct_targets.push(ip); + if non_direct_targets.len() >= 20 { + break; + } + } + } + + if non_direct_targets.is_empty() { + continue; + } + + let non_direct_count = non_direct_targets.len(); + + let targets_to_probe: Vec = { + let mut rng = rand::rng(); + if non_direct_targets.len() <= 10 { + non_direct_targets + } else { + non_direct_targets + .choose_multiple(&mut rng, 10) + .copied() + .collect() + } + }; + + let all_routes = route_table.route_table(); + let mut direct_peers = Vec::new(); + for (ip, routes) in &all_routes { + if let Some(best_route) = routes.first() { + if best_route.is_direct() { + direct_peers.push((*ip, best_route.route_key())); + } + } + } + + if direct_peers.is_empty() { + log::debug!("No direct peers available for relay probe"); + continue; + } + + let max_probes_per_target = 3.min(direct_peers.len()); + + for target_ip in &targets_to_probe { + let selected_peers: Vec<_> = { + let mut rng = rand::rng(); + direct_peers + .iter() + .filter(|(ip, _)| ip != target_ip) + .choose_multiple(&mut rng, max_probes_per_target) + .into_iter() + .cloned() + .collect() + }; + + for (relay_ip, route_key) in selected_peers { + // 构造Ping消息,目标是target_ip,但发送给relay_ip + let Ok(mut ping) = NetPacket::new(TransmissionBytes::zeroed_size( + HEAD_LENGTH + 8, + socket_manager.encrypt_reserve(), + )) else { + continue; + }; + + ping.set_msg_type(MsgType::Ping); + ping.set_ttl(2); // TTL设为2,允许中继一次 + ping.set_src_id(src.into()); + ping.set_dest_id((*target_ip).into()); + ping.set_payload(&crate::utils::time::now_ts_ms().to_be_bytes()) + .unwrap(); + + // 发送给已打洞的客户端,让它中继到目标 + if let Err(e) = socket_manager.send_to(ping, &route_key).await { + log::debug!( + "Failed to send relay probe to {} for target {}: {:?}", + relay_ip, + target_ip, + e + ); + } + } + + // 控制发送速率,避免网络拥塞 + tokio::time::sleep(Duration::from_millis(50)).await; + } + + log::info!( + "Relay probe task completed: {} targets probed (from {} non-direct), {} direct peers", + targets_to_probe.len(), + non_direct_count, + direct_peers.len() + ); + } +} + +/// 隧道收发调度与数据分发 +pub async fn tunnel_dispatch_task( + nat_info: MyNatInfo, + task_group: TaskGroup, + mut tunnel_factory: TunnelDispatcher, + p2p_inbound_handler: P2pInboundHandler, + p2p_socket_manager: P2pOutbound, +) { + loop { + let mut tunnel = match tunnel_factory.dispatch().await { + Ok(rs) => rs, + Err(e) => { + log::error!("tunnel disptach :{e:?}"); + return; + } + }; + log::info!("tunnel {:?}-{:?}", tunnel.protocol(), tunnel.remote_addr()); + let p2p_inbound_handler = p2p_inbound_handler.clone(); + let nat_info = nat_info.clone(); + let p2p_socket_manager = p2p_socket_manager.clone(); + task_group.spawn(async move { + let mut buf = vec![0; 65536]; + while let Some(rs) = tunnel.recv_from(&mut buf).await { + let (len, route_key) = match rs { + Ok(rs) => rs, + Err(e) => { + log::warn!("recv_from {e:?}"); + if tunnel.protocol().is_udp() { + continue; + } + break; + } + }; + if tunnel.protocol().is_udp() + && rust_p2p_core::stun::is_stun_response(&buf[..len]) + && let Some(pub_addr) = rust_p2p_core::stun::recv_stun_response(&buf[..len]) + { + nat_info.update_public_addr(route_key.index(), pub_addr); + continue; + } + let mut bytes = TransmissionBytes::zeroed(len); + bytes.copy_from_slice(&buf[..len]); + p2p_inbound_handler + .next_handle(bytes, route_key, &p2p_socket_manager, &mut tunnel) + .await; + } + log::info!( + "drop tunnel {:?}-{:?}", + tunnel.protocol(), + tunnel.remote_addr() + ); + if let Tunnel::Tcp(tcp) = tunnel { + p2p_inbound_handler.tcp_disconnect(tcp.route_key()).await; + } + }); + } +} diff --git a/vnt-core/src/tunnel_core/server/connection_manager.rs b/vnt-core/src/tunnel_core/server/connection_manager.rs new file mode 100644 index 0000000..22e6298 --- /dev/null +++ b/vnt-core/src/tunnel_core/server/connection_manager.rs @@ -0,0 +1,346 @@ +use crate::compression::PacketCompression; +use crate::context::config::Config; +use crate::context::nat::{MyNatInfo, PunchBackoff}; +use crate::context::{AppState, NetworkAddr, NetworkRoute, PeerInfoMap, ServerInfoCollection}; +use crate::crypto::PacketCrypto; +use crate::enhanced_tunnel::inbound::EnhancedInbound; +use crate::fec::FecDecoder; +use crate::protocol::control_message::{ + ConfirmRegResponseMsg, RegResponseMsg, RegistrationMode, RequestMessage, ResponseMessage, +}; +use crate::tunnel_core::p2p::transport::punch::NatPuncher; +use crate::tunnel_core::server::inbound::ServerTurnInboundHandler; +use crate::tunnel_core::server::outbound::ServerOutbound; +use crate::tunnel_core::server::rpc::{RpcNotifier, ServerRPC}; +use crate::tunnel_core::server::transport::TransportClient; +use crate::tunnel_core::server::transport::config::ConnectRegConfig; +use crate::utils::task_control::TaskGroup; +use anyhow::bail; +use bytes::Bytes; +use std::collections::HashMap; +use std::net::Ipv4Addr; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::sync::mpsc::{Receiver, Sender}; + +pub struct InboundHandlerConfig { + pub network_route: NetworkRoute, + pub server_info: ServerInfoCollection, + pub nat_info: MyNatInfo, + pub peer_map: PeerInfoMap, + pub punch_backoff: PunchBackoff, + pub puncher: NatPuncher, + pub packet_crypto: PacketCrypto, + pub packet_compression: PacketCompression, + pub enhanced_inbound: EnhancedInbound, + pub fec_decoder: FecDecoder, +} + +pub struct ServerTurnManager { + server_id: u32, + config: ConnectRegConfig, + receiver: Option>, + notifier: RpcNotifier, + transport_client: TransportClient, +} +pub(crate) fn create_server_tunnel( + app_state: AppState, + config: &Config, + packet_crypto: PacketCrypto, +) -> (Vec, ServerOutbound, ServerRPC) { + let mut rpc_notifier: HashMap = HashMap::new(); + let mut sender_map: HashMap> = HashMap::new(); + let mut server_manager_list = Vec::with_capacity(config.server_addr.len()); + let mut server_addr_list = Vec::with_capacity(config.server_addr.len()); + for (index, server_addr) in config.server_addr.iter().enumerate() { + let connect_reg_config = config.to_connect_config(index); + + let server_id = index as u32; + + let (s, r) = tokio::sync::mpsc::channel(1024); + + let notifier = RpcNotifier::new(); + let manager = + ServerTurnManager::new(server_id, connect_reg_config.clone(), r, notifier.clone()); + server_addr_list.push((server_id, server_addr.clone())); + rpc_notifier.insert(server_id, notifier); + sender_map.insert(server_id, s); + server_manager_list.push(manager); + } + let server_info_collection = app_state.server_info_collection.clone(); + server_info_collection.update_server(server_addr_list); + let tunnel_to_server = + ServerOutbound::new(Arc::new(sender_map), server_info_collection, packet_crypto); + + let server_rpc = ServerRPC::new(tunnel_to_server.clone(), rpc_notifier); + + (server_manager_list, tunnel_to_server, server_rpc) +} + +impl ServerTurnManager { + pub fn new( + server_id: u32, + config: ConnectRegConfig, + receiver: Receiver<(Bytes, Instant)>, + notifier: RpcNotifier, + ) -> Self { + let connector = TransportClient::new(); + Self { + server_id, + transport_client: connector, + config, + receiver: Some(receiver), + notifier, + } + } + pub fn disconnect(&mut self) { + self.transport_client.disconnect(); + } + + pub async fn connect_and_reg( + &mut self, + mode: RegistrationMode, + ) -> anyhow::Result { + let connect_config = self.config.to_connect_config().await?; + log::info!( + "Connecting to server[{}] {:?} with mode {:?}", + self.server_id, + connect_config, + mode, + ); + + self.transport_client + .connect_timeout(&connect_config, Duration::from_secs(10)) + .await?; + + let reg_msg = self.config.reg_msg_request(self.server_id, mode); + let request_msg = RequestMessage::Reg(reg_msg); + let encoded = request_msg.encode(); + + self.transport_client + .send(encoded.freeze()) + .await?; + let buf = self + .transport_client + .next_timeout(Duration::from_secs(10)) + .await?; + let response = ResponseMessage::from_slice(&buf)?; + match &response { + ResponseMessage::Reg(_) => {} + ResponseMessage::Error(_e) => { + self.disconnect(); + } + ResponseMessage::ConfirmReg(_) => { + self.disconnect(); + } + } + Ok(response) + } + + pub async fn send_confirm(&mut self) -> anyhow::Result { + self.transport_client + .send(RequestMessage::ConfirmReg.encode().freeze()) + .await?; + let buf = self + .transport_client + .next_timeout(Duration::from_secs(10)) + .await?; + let response = ResponseMessage::from_slice(&buf)?; + match response { + ResponseMessage::ConfirmReg(msg) => Ok(msg), + ResponseMessage::Error(e) => bail!("Confirm failed: {}", e.message), + _ => bail!("Unexpected response"), + } + } + + pub fn set_ip(&mut self, ip: Ipv4Addr) { + self.config.ip = Some(ip); + } + + /// Start data handling task with an already established connection. + pub fn data_handle_task_connected( + mut self, + task_group: &TaskGroup, + config: Box, + initial_response: NetworkAddr, + ) { + let data_handler = + ServerTurnInboundHandler::new(self.server_id, initial_response, config); + let task_group_ = task_group.clone(); + let Some(mut receiver) = self.receiver.take() else { + unreachable!() + }; + + task_group.spawn(async move { + let mut already_connected = true; + loop { + if !already_connected { + self.disconnect(); + data_handler.handle_disconnected(); + let msg = match self.connect_and_reg(RegistrationMode::Normal).await { + Ok(msg) => msg, + Err(e) => { + log::error!("连接服务器失败:{e:?}"); + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + continue; + } + }; + match &msg { + ResponseMessage::Reg(reg) => { + if reg.ip != initial_response.ip + || reg.prefix_len != initial_response.prefix_len + || reg.gateway != initial_response.gateway + { + log::error!("虚拟网络发生变化"); + break; + } + // 保存服务器版本 + if !reg.server_version.is_empty() { + data_handler.set_server_version(reg.server_version.clone()); + } + } + ResponseMessage::Error(e) => { + log::error!("注册失败 {e:?}"); + break; + } + _ => { + log::error!("错误的注册消息"); + break; + } + } + } + log::info!("已连接服务器:{}", self.config.server_addr); + data_handler.handle_connected(); + + if let Err(e) = self + .data_handle_loop(&mut receiver, &data_handler) + .await + { + log::error!("Error on data_handle_loop: {:?}", e); + } + already_connected = false; + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + self.disconnect(); + data_handler.handle_disconnected(); + task_group_.stop(); + }); + } + + pub async fn data_handle_loop( + &mut self, + receiver: &mut Receiver<(Bytes, Instant)>, + data_handler: &ServerTurnInboundHandler, + ) -> anyhow::Result<()> { + let mut time = crate::utils::time::now_ts_ms(); + let mut ping_interval = tokio::time::interval(Duration::from_secs(5)); + loop { + tokio::select! { + Some((buf,expired)) = receiver.recv() => { + if expired < Instant::now(){ + continue; + } + self.transport_client.send(buf).await?; + } + rs = self.transport_client.next() => { + time = crate::utils::time::now_ts_ms(); + let data = rs?; + if let Err(e) = data_handler.handle(&mut self.transport_client,data, &self.notifier,time).await{ + log::warn!("Error handling data: {:?}", e); + } + } + _ = ping_interval.tick() => { + let now = crate::utils::time::now_ts_ms(); + if now > time + Duration::from_secs(20).as_millis() as i64 { + bail!("timeout") + } + data_handler.handle_ping(&mut self.transport_client,now).await?; + } + else => { + bail!("receiver closed"); + } + } + } + } +} + +/// Coordinated multi-server pre-registration. +/// 1. First server uses PRE_REGISTER mode to get IP +/// 2. Other servers pre-register with the obtained IP +/// 3. Send confirmation to all servers +/// 4. Return the registration response +pub async fn coordinated_registration( + managers: &mut Vec, +) -> anyhow::Result { + if managers.is_empty() { + bail!("No servers to register"); + } + + // Step 1: First server pre-register to get IP + log::info!( + "Starting coordinated registration with {} servers", + managers.len() + ); + let first_response = managers[0] + .connect_and_reg(RegistrationMode::PreRegister) + .await?; + + let ip = match &first_response { + ResponseMessage::Reg(reg) => reg.ip, + ResponseMessage::Error(e) => bail!("First server registration failed: {}", e.message), + _ => bail!("Unexpected response from first server"), + }; + log::info!("Got IP {} from first server", ip); + + // Step 2: Set IP and pre-register with other servers + for manager in managers.iter_mut().skip(1) { + manager.set_ip(ip); + } + + if managers.len() > 1 { + let other_results: Vec<_> = futures::future::join_all( + managers + .iter_mut() + .skip(1) + .map(|m| m.connect_and_reg(RegistrationMode::PreRegister)), + ) + .await; + + // Check all responses + for (i, result) in other_results.iter().enumerate() { + match result { + Ok(ResponseMessage::Reg(_)) => { + log::info!("Server {} pre-registered successfully", i + 1); + } + Ok(ResponseMessage::Error(e)) => { + bail!("Server {} registration failed: {}", i + 1, e.message) + } + Err(e) => bail!("Server {} registration failed: {}", i + 1, e), + _ => bail!("Unexpected response from server {}", i + 1), + } + } + } + + // Step 3: Send confirmation to all servers + log::info!("Sending confirmation to all servers"); + let confirm_results: Vec<_> = + futures::future::join_all(managers.iter_mut().map(|m| m.send_confirm())).await; + + // Check all confirmation responses + for (i, result) in confirm_results.into_iter().enumerate() { + match result { + Ok(msg) if msg.success => { + log::info!("Server {} confirmed successfully", i); + } + Ok(_) => bail!("Server {} confirmation failed", i), + Err(e) => bail!("Server {} confirmation failed: {}", i, e), + } + } + + log::info!("Coordinated registration completed successfully"); + // Return first server's response (contains IP info) + match first_response { + ResponseMessage::Reg(reg) => Ok(reg), + _ => unreachable!(), + } +} diff --git a/vnt-core/src/tunnel_core/server/inbound.rs b/vnt-core/src/tunnel_core/server/inbound.rs new file mode 100644 index 0000000..abe9e78 --- /dev/null +++ b/vnt-core/src/tunnel_core/server/inbound.rs @@ -0,0 +1,332 @@ +use crate::compression::PacketCompression; +use crate::context::nat::{MyNatInfo, PunchBackoff}; +use crate::context::{NetworkAddr, NetworkRoute, PeerInfoMap, ServerInfoCollection}; +use crate::crypto::PacketCrypto; +use crate::enhanced_tunnel::inbound::EnhancedInbound; +use crate::fec::FecDecoder; +use crate::protocol::client_message::PunchInfo; +use crate::protocol::control_message::ClientSimpleInfoList; +use crate::protocol::ip_packet_protocol::{HEAD_LENGTH, MsgType, NetPacket}; +use crate::protocol::rpc_message::RpcMessageResponse; +use crate::protocol::transmission::TransmissionBytes; +use crate::tunnel_core::p2p::transport::punch::NatPuncher; +use crate::tunnel_core::server::rpc::RpcNotifier; +use crate::tunnel_core::server::transport::TransportClient; +use anyhow::bail; +use pnet_packet::Packet; +use pnet_packet::icmp::{IcmpPacket, IcmpTypes}; +use pnet_packet::ipv4::Ipv4Packet; +use prost::Message; +use rust_p2p_core::nat::NatInfo; +use std::net::Ipv4Addr; + +pub(crate) struct ServerTurnInboundHandler { + server_id: u32, + network_addr: Option, + network_route: NetworkRoute, + server_info: ServerInfoCollection, + nat_info: MyNatInfo, + peer_map: PeerInfoMap, + punch_backoff: PunchBackoff, + puncher: NatPuncher, + packet_crypto: PacketCrypto, + packet_compression: PacketCompression, + enhanced_inbound: EnhancedInbound, + fec_decoder: FecDecoder, +} +impl ServerTurnInboundHandler { + pub fn new( + server_id: u32, + network_addr: NetworkAddr, + config: Box, + ) -> Self { + let config = *config; + Self { + server_id, + network_addr: Some(network_addr), + network_route: config.network_route, + server_info: config.server_info, + nat_info: config.nat_info, + peer_map: config.peer_map, + punch_backoff: config.punch_backoff, + puncher: config.puncher, + packet_crypto: config.packet_crypto, + packet_compression: config.packet_compression, + enhanced_inbound: config.enhanced_inbound, + fec_decoder: config.fec_decoder, + } + } + fn network_contains(&self, ip: &Ipv4Addr) -> bool { + self.network_route.network_contains(ip) + } + fn filter_ip(&self, mut info: NatInfo) -> NatInfo { + if self.network_contains(&info.local_ipv4) { + info.local_ipv4 = Ipv4Addr::UNSPECIFIED; + } + info.local_ipv4s.retain(|ip| !self.network_contains(ip)); + info + } + fn get_punch_info(&self) -> Option { + self.nat_info.get().map(|info| PunchInfo { + nat_info: self.filter_ip(info), + }) + } + fn update_peer_nat_info(&self, ip: Ipv4Addr, nat_info: NatInfo) { + self.peer_map.update_nat_info(ip, nat_info); + } + + pub async fn handle_server_data( + & self, + transport_client: &mut TransportClient, + network_addr: NetworkAddr, + data: TransmissionBytes, + rpc_notifier: &RpcNotifier, + now: i64, + ) -> anyhow::Result<()> { + let net_packet = NetPacket::new(data)?; + let src = net_packet.src_id().into(); + let msg_type = net_packet.msg_type()?; + let mut net_packet = self.packet_compression.decompress(net_packet)?; + + match msg_type { + MsgType::Turn => { + // 只允许icmp EchoReply + let Some(ipv4) = Ipv4Packet::new(net_packet.payload()) else { + return Ok(()); + }; + if ipv4.get_version() != 4 { + return Ok(()); + } + if ipv4.get_next_level_protocol() != pnet_packet::ip::IpNextHeaderProtocols::Icmp { + return Ok(()); + } + let Some(icmp) = IcmpPacket::new(ipv4.payload()) else { + return Ok(()); + }; + if icmp.get_icmp_type() != IcmpTypes::EchoReply { + return Ok(()); + } + self.enhanced_inbound + .inbound(&network_addr, msg_type, src, net_packet) + .await?; + } + MsgType::Ping => { + net_packet.set_ttl(2); + net_packet.set_msg_type(MsgType::Pong); + net_packet.set_src_id(network_addr.ip.into()); + net_packet.set_dest_id(src.into()); + transport_client.send_turn(net_packet).await?; + } + MsgType::PongTurn => { + // 服务端ping 回复,记录延迟 + if net_packet.payload().len() == 8 + 8 { + let time = i64::from_be_bytes(net_packet.payload()[..8].try_into()?); + // let data_version = u64::from_be_bytes(net_packet.payload()[8..].try_into()?); + if now >= time { + self.server_info + .set_server_rtt(self.server_id, (now - time) as u32); + } + } + } + MsgType::PushClientIps => { + let list = ClientSimpleInfoList::from_slice(net_packet.payload())?; + self.server_info.update_client_simple_list( + self.server_id, + network_addr.ip, + list, + now, + ); + } + MsgType::RpcRes => { + // 设置rpc响应 + let response = RpcMessageResponse::decode(net_packet.payload())?; + rpc_notifier.notify_response(response); + } + _ => {} + } + Ok(()) + } + pub async fn handle_client_data( + &self, + network_addr: NetworkAddr, + transport_client: &mut TransportClient, + data: TransmissionBytes, + ) -> anyhow::Result<()> { + let mut net_packet = NetPacket::new(data)?; + let msg_type = net_packet.msg_type()?; + let src = Ipv4Addr::from(net_packet.src_id()); + let dest = Ipv4Addr::from(net_packet.dest_id()); + + if msg_type == MsgType::Quic { + // QUIC 数据不加密不压缩,但可能有 FEC + if net_packet.is_fec() { + let packets = self.fec_decoder.receive(net_packet)?; + if let Some(packets) = packets { + for pkt in packets { + self.enhanced_inbound + .inbound(&network_addr, msg_type, src, pkt) + .await?; + } + } + return Ok(()); + } + self.enhanced_inbound + .inbound(&network_addr, msg_type, src, net_packet) + .await?; + return Ok(()); + } + + // 解密 + if let Err(e) = self.packet_crypto.decrypt_in_place(&mut net_packet) { + log::error!("{},mst_type={msg_type:?},src={src},dst={dest}", e); + return Ok(()); + } + + // FEC 解码(始终尝试解码,如果有 FEC 标志) + if net_packet.is_fec() { + let packets = self.fec_decoder.receive(net_packet)?; + if let Some(packets) = packets { + for pkt in packets { + let pkt = self.packet_compression.decompress(pkt)?; + self.process_decompressed_packet( + network_addr, + transport_client, + pkt, + msg_type, + src, + dest, + ) + .await?; + } + } + return Ok(()); + } + + // 解压缩 + let net_packet = self.packet_compression.decompress(net_packet)?; + self.process_decompressed_packet( + network_addr, + transport_client, + net_packet, + msg_type, + src, + dest, + ) + .await + } + + async fn process_decompressed_packet( + &self, + network_addr: NetworkAddr, + transport_client: &mut TransportClient, + net_packet: NetPacket, + msg_type: MsgType, + src: Ipv4Addr, + dest: Ipv4Addr, + ) -> anyhow::Result<()> { + match msg_type { + MsgType::Turn | MsgType::Broadcast => { + self.enhanced_inbound + .inbound(&network_addr, msg_type, src, net_packet) + .await?; + } + MsgType::PunchStart1 => { + // 对方发起打洞 + let peer_punch_info = PunchInfo::from_slice(net_packet.payload())?; + let Some(self_punch_info) = self.get_punch_info() else { + return Ok(()); + }; + log::info!( + "对方主动发起打洞 对方nat信息={peer_punch_info:?},自己nat信息={self_punch_info:?} {src}->{dest}" + ); + self.update_peer_nat_info(src, peer_punch_info.nat_info.clone()); + let rs = self.puncher.punch(src, peer_punch_info)?; + if rs { + let bytes_mut = self_punch_info.encode(); + let mut net_packet = NetPacket::new(TransmissionBytes::zeroed_size( + HEAD_LENGTH + bytes_mut.len(), + self.packet_crypto.encrypt_reserve(), + ))?; + net_packet.set_msg_type(MsgType::PunchStart2); + net_packet.set_ttl(2); + net_packet.set_src_id(dest.into()); + net_packet.set_dest_id(src.into()); + net_packet.set_payload(&bytes_mut)?; + self.packet_crypto.encrypt_in_place(&mut net_packet)?; + transport_client.send_turn(net_packet).await?; + }else{ + log::info!("限制打洞频率") + } + } + MsgType::PunchStart2 => { + self.punch_backoff.record(src); + // 对方回复开始打洞 + let peer_punch_info = PunchInfo::from_slice(net_packet.payload())?; + self.update_peer_nat_info(src, peer_punch_info.nat_info.clone()); + log::info!("对方回复开始打洞 {:?} {src}->{dest}", peer_punch_info); + self.puncher.punch_uncheck(src, peer_punch_info)?; + } + _ => {} + } + Ok(()) + } + + pub async fn handle( + &self, + transport_client: &mut TransportClient, + data: TransmissionBytes, + rpc_notifier: &RpcNotifier, + now: i64, + ) -> anyhow::Result<()> { + let net_packet = NetPacket::new(&data)?; + let Some(network_addr) = self.network_addr else { + bail!("未找到自身IP") + }; + + if net_packet.is_gateway() { + // 服务端数据 + return self + .handle_server_data(transport_client, network_addr, data, rpc_notifier, now) + .await; + } + let dest = Ipv4Addr::from(net_packet.dest_id()); + if !dest.is_broadcast() && !dest.is_unspecified() && network_addr.ip != dest { + return Ok(()); + } + self.handle_client_data(network_addr, transport_client, data) + .await + } + pub async fn handle_ping( + &self, + transport_client: &mut TransportClient, + now: i64, + ) -> anyhow::Result<()> { + let mut ping_packet = NetPacket::new(TransmissionBytes::zeroed(HEAD_LENGTH + 8 + 8))?; + ping_packet.set_ttl(1); + ping_packet.set_msg_type(MsgType::PingTurn); + ping_packet.set_gateway_flag(true); + ping_packet.set_payload(&now.to_be_bytes())?; + ping_packet.payload_mut()[0..8].copy_from_slice(&now.to_be_bytes()); + ping_packet.payload_mut()[8..] + .copy_from_slice(&self.server_info.data_version(self.server_id).to_be_bytes()); + transport_client + .send(ping_packet.into_buffer().into_bytes().freeze()) + .await?; + Ok(()) + } + pub fn handle_connected(&self) { + self.server_info.set_server_connected(self.server_id, true); + self.server_info + .set_last_connected_time(self.server_id, Some(crate::utils::time::now_ts_ms())); + self.server_info.set_disconnected_time(self.server_id, None); + } + pub fn set_server_version(&self, version: String) { + self.server_info.set_server_version(self.server_id, version); + } + pub fn handle_disconnected(&self) { + if self.server_info.set_server_connected(self.server_id, false) { + self.server_info + .set_disconnected_time(self.server_id, Some(crate::utils::time::now_ts_ms())); + } + } +} diff --git a/vnt-core/src/tunnel_core/server/mod.rs b/vnt-core/src/tunnel_core/server/mod.rs new file mode 100644 index 0000000..60b2855 --- /dev/null +++ b/vnt-core/src/tunnel_core/server/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod connection_manager; +pub(crate) mod inbound; +pub(crate) mod outbound; +pub(crate) mod rpc; +pub mod transport; diff --git a/vnt-core/src/tunnel_core/server/outbound.rs b/vnt-core/src/tunnel_core/server/outbound.rs new file mode 100644 index 0000000..51b4669 --- /dev/null +++ b/vnt-core/src/tunnel_core/server/outbound.rs @@ -0,0 +1,329 @@ +use crate::context::ServerInfoCollection; +use crate::crypto::PacketCrypto; +use crate::protocol::ProtoToBytesMut; +use crate::protocol::control_message::SelectiveBroadcast; +use crate::protocol::ip_packet_protocol::{HEAD_LENGTH, MsgType, NetPacket}; +use crate::protocol::transmission::TransmissionBytes; +use anyhow::{Context, bail}; +use bytes::Bytes; +use std::collections::HashMap; +use std::net::Ipv4Addr; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::sync::mpsc::Sender; + +#[derive(Clone)] +pub(crate) struct ServerOutbound { + server_id_list: Arc>, + sender: Arc>>, + server_info_collection: ServerInfoCollection, + packet_crypto: PacketCrypto, +} +impl ServerOutbound { + pub fn new( + sender: Arc>>, + server_info_collection: ServerInfoCollection, + packet_crypto: PacketCrypto, + ) -> Self { + let server_id_list = Arc::new(sender.keys().copied().collect()); + Self { + server_id_list, + sender, + server_info_collection, + packet_crypto, + } + } + pub fn exists_route(&self, dest: &Ipv4Addr) -> bool { + self.server_info_collection.exists_online_client_ip(dest) + } + pub fn server_id_list(&self) -> &Vec { + &self.server_id_list + } + pub fn encrypt_reserve(&self) -> usize { + self.packet_crypto.encrypt_reserve() + } + + pub async fn send_to_gateway_expired( + &self, + server_id: u32, + mut buf: NetPacket, + expired: Duration, + ) -> anyhow::Result<()> { + if !self.server_info_collection.is_server_connected(server_id) { + bail!("未连接服务器") + } + buf.set_gateway_flag(true); + self.send_expired_impl(server_id, buf, expired).await + } + + pub async fn send( + &self, + dest_ip: Ipv4Addr, + buf: NetPacket, + ) -> anyhow::Result<()> { + self.send_expired(dest_ip, buf, Duration::from_secs(5)) + .await + } + pub async fn send_expired( + &self, + dest_ip: Ipv4Addr, + buf: NetPacket, + expired: Duration, + ) -> anyhow::Result<()> { + let Some(server_id) = self + .server_info_collection + .find_ip_to_server(&self.server_id_list, &dest_ip) + else { + bail!("not found ip route: {dest_ip}") + }; + self.send_expired_impl(server_id, buf, expired).await + } + async fn send_expired_impl( + &self, + server_id: u32, + mut buf: NetPacket, + expired: Duration, + ) -> anyhow::Result<()> { + if !buf.is_gateway() { + self.packet_crypto.encrypt_in_place(&mut buf)?; + } + let Some(sender) = self.sender.get(&server_id) else { + bail!("not found server") + }; + sender + .send_timeout( + ( + buf.into_buffer().into_bytes().freeze(), + Instant::now() + expired, + ), + expired, + ) + .await + .context("connect server task failed") + } + + pub async fn send_raw(&self, dest_ip: Ipv4Addr, buf: NetPacket) -> anyhow::Result<()> { + let Some(server_id) = self + .server_info_collection + .find_ip_to_server(&self.server_id_list, &dest_ip) + else { + bail!("not found ip route: {dest_ip}") + }; + let expired = Duration::from_secs(5); + let Some(sender) = self.sender.get(&server_id) else { + bail!("not found server") + }; + sender + .send_timeout((buf.into_buffer(), Instant::now() + expired), expired) + .await + .context("connect server task failed") + } + pub async fn send_default_raw(&self, buf: NetPacket) -> anyhow::Result<()> { + let Some(server_id) = self + .server_info_collection + .find_connected_server(&self.server_id_list) + else { + bail!("not found default route") + }; + let expired = Duration::from_secs(5); + let Some(sender) = self.sender.get(&server_id) else { + bail!("not found server") + }; + sender + .send_timeout((buf.into_buffer(), Instant::now() + expired), expired) + .await + .context("connect server task failed") + } + + pub async fn send_raw_broadcast( + &self, + exclude_ips: Option>, + buf: NetPacket, + ) -> anyhow::Result<()> { + let buf = buf.into_buffer(); + let expired = Duration::from_secs(5); + + let map: HashMap, u32)> = + self.server_info_collection.server_client_ip_map(); + if map.is_empty() { + bail!("no connected servers with clients"); + } + + // 只有一个服务器,直接发送 + if map.len() == 1 { + let (server_id, (ips, _)) = map.iter().next().expect("map has exactly one element"); + if ips.is_empty() { + return Ok(()); + } + let sender = self + .sender + .get(server_id) + .context("server sender not found")?; + + return if let Some(exclude_ips) = exclude_ips { + send_exclude_broadcast(sender.clone(), buf, exclude_ips, expired).await + } else { + send_direct(sender.clone(), buf, expired).await + }; + } + + // 找到最优服务器 + let (max_server_id, (max_ips, _)) = map + .iter() + .max_by(|(_, (ips_a, rtt_a)), (_, (ips_b, rtt_b))| { + let score_a = ips_a.len() as f64 / (*rtt_a as f64 + 1.0); + let score_b = ips_b.len() as f64 / (*rtt_b as f64 + 1.0); + score_a + .partial_cmp(&score_b) + .unwrap_or(std::cmp::Ordering::Equal) + }) + .context("failed to find server with most IPs")?; + if max_ips.is_empty() { + return Ok(()); + } + let max_ip_set: std::collections::HashSet<_> = max_ips.iter().collect(); + let exclude_set: std::collections::HashSet<_> = exclude_ips + .as_ref() + .map(|ips| ips.iter().collect()) + .unwrap_or_default(); + + let mut handles = Vec::new(); + + // 任务1: 向主服务器发送 + let sender = self + .sender + .get(max_server_id) + .cloned() + .context("max server sender not found")?; + + if let Some(exclude_ips) = exclude_ips.clone() { + let buf_clone = buf.clone(); + let handle = tokio::spawn(async move { + send_exclude_broadcast(sender, buf_clone, exclude_ips, expired).await + }); + handles.push(handle); + } else { + let buf_clone = buf.clone(); + let handle = tokio::spawn(async move { send_direct(sender, buf_clone, expired).await }); + handles.push(handle); + } + + // 任务2-N: 向其他服务器发送目标广播 + for (server_id, (ips, _rtt)) in map.iter() { + if *server_id == *max_server_id { + continue; + } + + // 筛选目标IP:不在最大服务器中,也不在排除列表中 + let target_ips: Vec = ips + .iter() + .filter(|ip| !max_ip_set.contains(ip) && !exclude_set.contains(ip)) + .copied() + .collect(); + + if target_ips.is_empty() { + continue; + } + + let sender = match self.sender.get(server_id).cloned() { + Some(s) => s, + None => continue, + }; + let buf_clone = buf.clone(); + + let handle = tokio::spawn(async move { + send_target_broadcast(sender, target_ips, buf_clone, expired).await + }); + handles.push(handle); + } + + // 等待所有任务完成 + let mut errors = Vec::new(); + for handle in handles { + match handle.await { + Ok(Ok(())) => {} + Ok(Err(e)) => errors.push(e), + Err(e) => errors.push(anyhow::anyhow!("task join error: {}", e)), + } + } + + if !errors.is_empty() { + bail!( + "broadcast failed with {} errors: {:?}", + errors.len(), + errors + ); + } + + Ok(()) + } +} +async fn send_exclude_broadcast( + sender: Sender<(Bytes, Instant)>, + buf: Bytes, + exclude_ips: Vec, + expired: Duration, +) -> anyhow::Result<()> { + let broadcast = SelectiveBroadcast::new(&exclude_ips, buf.to_vec()); + let bytes = broadcast.encode_bytes_mut(); + + let mut packet = NetPacket::new(TransmissionBytes::zeroed(HEAD_LENGTH + bytes.len()))?; + packet.set_msg_type(MsgType::ExcludeBroadcast); + packet.set_ttl(5); + packet.payload_mut().copy_from_slice(&bytes); + + sender + .send_timeout( + ( + packet.into_buffer().into_bytes().freeze(), + Instant::now() + expired, + ), + expired, + ) + .await + .context("failed to send exclude broadcast")?; + + Ok(()) +} + +// 直接发送原始数据 +async fn send_direct( + sender: Sender<(Bytes, Instant)>, + buf: Bytes, + expired: Duration, +) -> anyhow::Result<()> { + sender + .send_timeout((buf, Instant::now() + expired), expired) + .await + .context("failed to send direct broadcast")?; + + Ok(()) +} + +async fn send_target_broadcast( + sender: Sender<(Bytes, Instant)>, + target_ips: Vec, + buf: Bytes, + expired: Duration, +) -> anyhow::Result<()> { + let target_broadcast = SelectiveBroadcast::new(&target_ips, buf.to_vec()); + let target_bytes = target_broadcast.encode_bytes_mut(); + + let mut packet = NetPacket::new(TransmissionBytes::zeroed(HEAD_LENGTH + target_bytes.len()))?; + packet.set_msg_type(MsgType::TargetBroadcast); + packet.set_ttl(5); + packet.payload_mut().copy_from_slice(&target_bytes); + + sender + .send_timeout( + ( + packet.into_buffer().into_bytes().freeze(), + Instant::now() + expired, + ), + expired, + ) + .await + .context("failed to send target broadcast")?; + + Ok(()) +} diff --git a/vnt-core/src/tunnel_core/server/rpc.rs b/vnt-core/src/tunnel_core/server/rpc.rs new file mode 100644 index 0000000..0765222 --- /dev/null +++ b/vnt-core/src/tunnel_core/server/rpc.rs @@ -0,0 +1,150 @@ +use crate::protocol::ip_packet_protocol::{HEAD_LENGTH, MsgType, NetPacket}; +use crate::protocol::rpc_message::rpc_message_request::RpcReqPayload; +use crate::protocol::rpc_message::rpc_message_response::RpcResPayload; +use crate::protocol::rpc_message::{ + ClientInfo, ClientListRequest, ClientListResponse, RpcMessageRequest, RpcMessageResponse, +}; +use crate::protocol::transmission::TransmissionBytes; +use crate::tunnel_core::server::outbound::ServerOutbound; +use anyhow::bail; +use parking_lot::Mutex; +use prost::Message; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::oneshot; +use tokio::sync::oneshot::Sender; + +#[derive(Clone)] +pub struct ServerRPC { + tunnel_to_server: ServerOutbound, + rpc_notifier: HashMap, +} +#[derive(Clone)] +pub(crate) struct RpcNotifier { + pending_requests: Arc>>>, + rpc_id: Arc>, +} + +impl RpcNotifier { + pub fn new() -> Self { + Self { + pending_requests: Arc::new(Mutex::new(HashMap::new())), + rpc_id: Arc::new(Mutex::new(0)), + } + } + + pub fn create_request_and_waiter(&self) -> RpcResponseWaiter { + let id: u64 = { + let mut id_lock = self.rpc_id.lock(); + *id_lock += 1; + *id_lock + }; + + let (tx, rx) = oneshot::channel(); + + { + let mut pending = self.pending_requests.lock(); + pending.insert(id, tx); + } + + RpcResponseWaiter { + id, + pending_requests_handle: Arc::clone(&self.pending_requests), + rx, + } + } + + pub fn notify_response(&self, response: RpcMessageResponse) { + let mut pending = self.pending_requests.lock(); + + if let Some(tx) = pending.remove(&response.id) { + let _ = tx.send(response); + } + } +} + +pub(crate) struct RpcResponseWaiter { + id: u64, + pending_requests_handle: Arc>>>, + rx: oneshot::Receiver, +} + +impl RpcResponseWaiter { + pub async fn wait_for_response( + mut self, + timeout: Duration, + ) -> anyhow::Result { + let result = tokio::time::timeout(timeout, &mut self.rx).await; + + match result { + Err(_) => bail!("timeout waiting for response"), + Ok(Ok(response)) => Ok(response), + Ok(Err(_)) => bail!("closed connection"), + } + } +} + +impl Drop for RpcResponseWaiter { + fn drop(&mut self) { + let mut pending = self.pending_requests_handle.lock(); + let _ = pending.remove(&self.id); + } +} + +impl ServerRPC { + pub(crate) fn new( + tunnel_to_server: ServerOutbound, + rpc_notifier: HashMap, + ) -> Self { + Self { + tunnel_to_server, + rpc_notifier, + } + } + + pub async fn client_list(&self) -> anyhow::Result { + let mut map: HashMap = HashMap::new(); + for server_id in self.tunnel_to_server.server_id_list() { + match self.client_list_target(*server_id).await { + Ok(rs) => { + for client in rs.list { + map.entry(client.id.clone()).or_insert(client); + } + } + Err(e) => { + log::error!("client list target failed: {}", e); + } + } + } + + Ok(ClientListResponse { + list: map.into_values().collect(), + }) + } + pub async fn client_list_target(&self, server_id: u32) -> anyhow::Result { + let Some(rpc_notifier) = self.rpc_notifier.get(&server_id) else { + bail!("no RPC notifier"); + }; + let waiter = rpc_notifier.create_request_and_waiter(); + let request = RpcMessageRequest { + id: waiter.id, + rpc_req_payload: Some(RpcReqPayload::ClientListReq(ClientListRequest::default())), + }; + let buf = request.encode_to_vec(); + let mut packet = NetPacket::new(TransmissionBytes::zeroed(HEAD_LENGTH + buf.len()))?; + packet.set_msg_type(MsgType::RpcReq); + packet.set_gateway_flag(true); + packet.set_ttl(1); + packet.set_payload(&buf)?; + + self.tunnel_to_server + .send_to_gateway_expired(server_id, packet, Duration::from_secs(1)) + .await?; + let response = waiter.wait_for_response(Duration::from_secs(3)).await?; + if let Some(RpcResPayload::ClientListRes(res)) = response.rpc_res_payload { + return Ok(res); + } + bail!("unexpected response: {:?}", response); + } +} diff --git a/vnt-core/src/tunnel_core/server/transport/config.rs b/vnt-core/src/tunnel_core/server/transport/config.rs new file mode 100644 index 0000000..918db06 --- /dev/null +++ b/vnt-core/src/tunnel_core/server/transport/config.rs @@ -0,0 +1,173 @@ +use crate::protocol::control_message::{RegRequestMsg, RegistrationMode}; +use crate::tls::verifier::CertValidationMode; +use anyhow::Context; +use rand::seq::SliceRandom; +use std::fmt; +use std::net::{Ipv4Addr, SocketAddr}; +use std::str::FromStr; + +#[derive(Debug, Clone)] +pub(crate) struct ConnectRegConfig { + pub server_addr: ProtocolAddress, + pub cert_mode: CertValidationMode, + pub network_code: String, + pub device_id: String, + pub device_name: String, + pub ip: Option, + pub key_sign: Option, + pub ip_variable: bool, +} +#[derive(Debug, Clone)] +pub(crate) struct ConnectConfig { + pub protocol_type: ProtocolType, + pub server_addr: SocketAddr, + pub server_domain: String, + pub cert_mode: CertValidationMode, +} +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] +pub enum ProtocolType { + Quic, + #[default] + TlsTcp, + Wss, + Dynamic, +} +#[derive(Debug, Clone)] +pub struct ProtocolAddress { + pub protocol_type: ProtocolType, + pub address: String, +} +impl Default for ProtocolAddress { + fn default() -> Self { + Self { + protocol_type: ProtocolType::default(), + address: "127.0.0.1:29872".to_string(), + } + } +} +impl FromStr for ProtocolAddress { + type Err = String; + + fn from_str(s: &str) -> Result { + let (protocol_type, server_addr) = parse_server(s)?; + Ok(Self { + protocol_type, + address: server_addr, + }) + } +} +impl fmt::Display for ProtocolAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let prefix = match self.protocol_type { + ProtocolType::Quic => "quic://", + ProtocolType::TlsTcp => "tcp://", + ProtocolType::Wss => "wss://", + ProtocolType::Dynamic => "dynamic://", + }; + write!(f, "{}{}", prefix, self.address) + } +} +pub fn parse_server(val: &str) -> Result<(ProtocolType, String), String> { + let val = val.trim().to_lowercase(); + if let Some(s) = val.strip_prefix("quic://") { + return Ok((ProtocolType::Quic, s.to_string())); + } + if let Some(s) = val.strip_prefix("tcp://") { + return Ok((ProtocolType::TlsTcp, s.to_string())); + } + if let Some(s) = val.strip_prefix("wss://") { + return Ok((ProtocolType::Wss, s.to_string())); + } + if let Some(s) = val.strip_prefix("dynamic://") { + return Ok((ProtocolType::Dynamic, s.to_string())); + } + if val.contains("://") { + return Err(format!("Unknown protocol in server address: {}", val)); + } + Ok((ProtocolType::TlsTcp, val)) +} +impl ConnectRegConfig { + pub fn reg_msg_request( + &self, + server_id: u32, + registration_mode: RegistrationMode, + ) -> RegRequestMsg { + RegRequestMsg { + network_code: self.network_code.to_string(), + device_id: self.device_id.to_string(), + ip: self.ip, + name: self.device_name.to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + key_sign: self.key_sign.clone(), + ip_variable: self.ip_variable, + server_id, + registration_mode, + } + } + pub async fn to_connect_config(&self) -> anyhow::Result { + let (protocol_type, server_domain) = match self.server_addr.protocol_type { + ProtocolType::Dynamic => { + let mut txt = crate::utils::dns_query::dns_query_txt( + &self.server_addr.address, + vec![], + &None, + ) + .await?; + txt.shuffle(&mut rand::rng()); + let x = txt.first().context("DNS query failed")?; + let x = x.to_lowercase(); + let (protocol_type, domain) = if let Some(v) = x.strip_prefix("udp://") { + (ProtocolType::Quic, v) + } else if let Some(v) = x.strip_prefix("quic://") { + (ProtocolType::Quic, v) + } else if let Some(v) = x.strip_prefix("tcp://") { + (ProtocolType::TlsTcp, v) + } else if let Some(v) = x.strip_prefix("ws://") { + (ProtocolType::TlsTcp, v) + } else if let Some(v) = x.strip_prefix("wss://") { + (ProtocolType::TlsTcp, v) + } else { + (ProtocolType::TlsTcp, x.as_str()) + }; + (protocol_type, domain.to_owned()) + } + v => (v, self.server_addr.address.to_string()), + }; + let server_addr = + crate::utils::dns_query::dns_query_one(&server_domain, &vec![], &None).await?; + let server_domain = strip_port(&server_domain).to_owned(); + Ok(ConnectConfig { + protocol_type, + server_addr, + server_domain, + cert_mode: self.cert_mode.clone(), + }) + } +} +fn strip_port(addr: &str) -> &str { + if let Some(stripped) = addr.strip_prefix('[') + && let Some(pos) = stripped.find(']') + { + return &stripped[..pos]; + } + + if addr.contains(':') && !addr.contains('.') && addr.matches(':').count() > 1 { + return addr; + } + + if let Some((host, port)) = addr.rsplit_once(':') + && port.chars().all(|c| c.is_ascii_digit()) + { + return host; + } + + addr +} +impl ConnectConfig { + pub fn server_addr(&self) -> SocketAddr { + self.server_addr + } + pub fn server_name(&self) -> &String { + &self.server_domain + } +} diff --git a/vnt-core/src/tunnel_core/server/transport/mod.rs b/vnt-core/src/tunnel_core/server/transport/mod.rs new file mode 100644 index 0000000..64c89c1 --- /dev/null +++ b/vnt-core/src/tunnel_core/server/transport/mod.rs @@ -0,0 +1,105 @@ +use crate::protocol::ip_packet_protocol::NetPacket; +use crate::protocol::transmission::TransmissionBytes; +use crate::tunnel_core::server::transport::config::{ConnectConfig, ProtocolType}; +use crate::tunnel_core::server::transport::quic::QuicTransport; +use crate::tunnel_core::server::transport::tcp::TlsTcpTransport; +use crate::tunnel_core::server::transport::wss::WssTransport; +use anyhow::{Context, bail}; +use bytes::Bytes; +use std::time::Duration; + +pub mod config; +pub(crate) mod quic; +pub(crate) mod tcp; +pub(crate) mod wss; + +#[derive(Default)] +pub(crate) enum TransportClient { + Quic(QuicTransport), + TlsTcp(TlsTcpTransport), + Wss(WssTransport), + #[default] + Pending, +} + +impl TransportClient { + pub fn new() -> Self { + TransportClient::default() + } + pub fn disconnect(&mut self) { + match self { + TransportClient::Quic(c) => c.disconnect(), + TransportClient::TlsTcp(c) => c.disconnect(), + TransportClient::Wss(c) => c.disconnect(), + TransportClient::Pending => {} + }; + *self = TransportClient::Pending; + } + pub async fn connect_timeout( + &mut self, + config: &ConnectConfig, + timeout: Duration, + ) -> anyhow::Result<()> { + tokio::time::timeout(timeout, self.connect(config)) + .await + .context("timeout")? + } + pub async fn connect(&mut self, config: &ConnectConfig) -> anyhow::Result<()> { + match self { + TransportClient::Quic(c) => c.connect(config).await?, + TransportClient::TlsTcp(c) => c.connect(config).await?, + TransportClient::Wss(c) => c.connect(config).await?, + TransportClient::Pending => match config.protocol_type { + ProtocolType::Quic => { + let mut transport = QuicTransport::new(); + transport.connect(config).await?; + *self = TransportClient::Quic(transport); + } + ProtocolType::TlsTcp => { + let mut transport = TlsTcpTransport::new(); + transport.connect(config).await?; + *self = TransportClient::TlsTcp(transport); + } + ProtocolType::Wss => { + let mut transport = WssTransport::new(); + transport.connect(config).await?; + *self = TransportClient::Wss(transport); + } + ProtocolType::Dynamic => { + bail!("unreachable connect") + } + }, + }; + Ok(()) + } + + pub async fn send(&mut self, buf: Bytes) -> anyhow::Result<()> { + match self { + TransportClient::Quic(c) => c.send(buf).await, + TransportClient::TlsTcp(c) => c.send(buf).await, + TransportClient::Wss(c) => c.send(buf).await, + TransportClient::Pending => { + bail!("Not connected"); + } + } + } + pub async fn next(&mut self) -> anyhow::Result { + match self { + TransportClient::Quic(c) => c.next().await, + TransportClient::TlsTcp(c) => c.next().await, + TransportClient::Wss(c) => c.next().await, + + TransportClient::Pending => { + bail!("Not connected"); + } + } + } + pub async fn next_timeout(&mut self, timeout: Duration) -> anyhow::Result { + tokio::time::timeout(timeout, self.next()) + .await + .context("timeout")? + } + pub async fn send_turn(&mut self, buf: NetPacket) -> anyhow::Result<()> { + self.send(buf.into_buffer().into_bytes().freeze()).await + } +} diff --git a/vnt-core/src/tunnel_core/server/transport/quic.rs b/vnt-core/src/tunnel_core/server/transport/quic.rs new file mode 100644 index 0000000..ca9e0a1 --- /dev/null +++ b/vnt-core/src/tunnel_core/server/transport/quic.rs @@ -0,0 +1,90 @@ +use crate::protocol::transmission::TransmissionBytes; +use crate::tls::verifier::CertValidationMode; +use crate::tunnel_core::server::transport::config::ConnectConfig; +use anyhow::{Context, bail}; +use bytes::Bytes; +use futures::{SinkExt, StreamExt}; +use quinn::{ClientConfig, Endpoint, RecvStream, SendStream}; +use std::sync::Arc; +use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec}; + +#[derive(Default)] +pub struct QuicTransport { + framed: Option<( + FramedWrite, + FramedRead, + )>, +} +impl QuicTransport { + pub fn new() -> Self { + Default::default() + } + pub fn disconnect(&mut self) { + self.framed = None; + } + pub async fn connect(&mut self, config: &ConnectConfig) -> anyhow::Result<()> { + if self.framed.is_some() { + bail!("Already connected"); + } + let (w, r) = connect_quic(config).await?; + self.framed = Some((w, r)); + Ok(()) + } + pub async fn send(&mut self, buf: Bytes) -> anyhow::Result<()> { + let Some((w, _r)) = self.framed.as_mut() else { + bail!("Not connected"); + }; + w.send(buf).await.context("send to server failed") + } + pub async fn next(&mut self) -> anyhow::Result { + let Some((_w, r)) = self.framed.as_mut() else { + bail!("Not connected"); + }; + r.next() + .await + .context("EOF")? + .context("receive from server failed") + .map(TransmissionBytes::from) + } +} +pub async fn connect_quic( + config: &ConnectConfig, +) -> anyhow::Result<( + FramedWrite, + FramedRead, +)> { + let server_addr = config.server_addr(); + let server_name = config.server_name(); + let quic_config = create_client_config(&config.cert_mode)?; + let mut endpoint = match Endpoint::client((std::net::Ipv6Addr::UNSPECIFIED, 0).into()) { + Ok(endpoint) => endpoint, + Err(e) => { + log::warn!("Failed to create QUIC endpoint: {}", e); + Endpoint::client((std::net::Ipv4Addr::UNSPECIFIED, 0).into()) + .context("Failed to create QUIC endpoint")? + } + }; + + endpoint.set_default_client_config(quic_config); + let connection = endpoint + .connect(server_addr, server_name)? + .await + .context("Failed to establish QUIC connection")?; + let (send_stream, recv_stream) = connection + .open_bi() + .await + .context("Failed to open bidirectional stream")?; + let framed_write = FramedWrite::new(send_stream, LengthDelimitedCodec::new()); + let framed_read = FramedRead::new(recv_stream, LengthDelimitedCodec::new()); + Ok((framed_write, framed_read)) +} + +fn create_client_config(cert_mode: &CertValidationMode) -> anyhow::Result { + let config = cert_mode.create_tls_client_config()?; + let client_config = ClientConfig::new(Arc::new( + quinn::crypto::rustls::QuicClientConfig::try_from(config) + .context("Failed to create QUIC client config")?, + )); + + Ok(client_config) +} diff --git a/vnt-core/src/tunnel_core/server/transport/tcp.rs b/vnt-core/src/tunnel_core/server/transport/tcp.rs new file mode 100644 index 0000000..8b14f89 --- /dev/null +++ b/vnt-core/src/tunnel_core/server/transport/tcp.rs @@ -0,0 +1,79 @@ +use crate::protocol::transmission::TransmissionBytes; +use crate::tunnel_core::server::transport::config::ConnectConfig; +use anyhow::{Context, bail}; +use bytes::Bytes; +use futures::{SinkExt, StreamExt}; +use std::sync::Arc; +use tokio::net::TcpStream; +use tokio_rustls::{TlsConnector, client::TlsStream}; +use tokio_util::codec::{Framed, LengthDelimitedCodec}; + +type TlsTcpStream = TlsStream; + +#[derive(Default)] +pub struct TlsTcpTransport { + framed: Option>, +} + +impl TlsTcpTransport { + pub fn new() -> Self { + Default::default() + } + pub fn disconnect(&mut self) { + self.framed = None; + } + pub async fn connect(&mut self, config: &ConnectConfig) -> anyhow::Result<()> { + if self.framed.is_some() { + bail!("Already connected"); + } + let framed = connect_tls_tcp(config).await?; + self.framed = Some(framed); + Ok(()) + } + pub async fn send(&mut self, buf: Bytes) -> anyhow::Result<()> { + let Some(framed) = self.framed.as_mut() else { + bail!("Not connected"); + }; + framed.send(buf).await.context("send to server failed") + } + pub async fn next(&mut self) -> anyhow::Result { + let Some(framed) = self.framed.as_mut() else { + bail!("Not connected"); + }; + framed + .next() + .await + .context("EOF")? + .context("receive from server failed") + .map(TransmissionBytes::from) + } +} + +pub async fn connect_tls_tcp( + config: &ConnectConfig, +) -> anyhow::Result> { + let server_addr = config.server_addr(); + let server_name = config.server_name().clone(); + + let rustls_config = config.cert_mode.create_tls_client_config()?; + let connector = TlsConnector::from(Arc::new(rustls_config)); + + let tcp_stream = TcpStream::connect(server_addr) + .await + .context("Failed to establish underlying TCP connection")?; + if let Err(e) = tcp_stream.set_nodelay(true) { + log::error!("Failed to set TCP_NODELAY: {}", e); + } + let dns_name = server_name + .try_into() + .context("Invalid server name for TLS")?; + + let tls_stream = connector + .connect(dns_name, tcp_stream) + .await + .context("Failed to perform TLS handshake")?; + + let framed = Framed::new(tls_stream, LengthDelimitedCodec::new()); + + Ok(framed) +} diff --git a/vnt-core/src/tunnel_core/server/transport/wss.rs b/vnt-core/src/tunnel_core/server/transport/wss.rs new file mode 100644 index 0000000..5ef72d9 --- /dev/null +++ b/vnt-core/src/tunnel_core/server/transport/wss.rs @@ -0,0 +1,96 @@ +use crate::protocol::transmission::TransmissionBytes; +use crate::tunnel_core::server::transport::config::ConnectConfig; +use anyhow::{Context, bail}; +use bytes::Bytes; +use futures::{SinkExt, StreamExt}; +use std::sync::Arc; +use tokio::net::TcpStream; +use tokio_rustls::{TlsConnector, client::TlsStream}; +use tokio_tungstenite::{WebSocketStream, client_async, tungstenite::Message}; + +type WssStream = WebSocketStream>; + +#[derive(Default)] +pub struct WssTransport { + stream: Option, +} + +impl WssTransport { + pub fn new() -> Self { + Default::default() + } + pub fn disconnect(&mut self) { + self.stream = None; + } + pub async fn connect(&mut self, config: &ConnectConfig) -> anyhow::Result<()> { + if self.stream.is_some() { + bail!("Already connected"); + } + let stream = connect_wss(config).await?; + self.stream = Some(stream); + Ok(()) + } + pub async fn send(&mut self, buf: Bytes) -> anyhow::Result<()> { + let Some(framed) = self.stream.as_mut() else { + bail!("Not connected"); + }; + framed + .send(Message::Binary(buf)) + .await + .context("send to server failed") + } + pub async fn next(&mut self) -> anyhow::Result { + let Some(framed) = self.stream.as_mut() else { + bail!("Not connected"); + }; + loop { + let message = framed + .next() + .await + .context("EOF")? + .context("receive from server failed")?; + match message { + Message::Binary(buf) => { + return Ok(TransmissionBytes::from(buf)); + } + Message::Close(_) => { + bail!("Disconnected"); + } + _ => { + continue; + } + } + } + } +} + +pub async fn connect_wss(config: &ConnectConfig) -> anyhow::Result { + let server_addr = config.server_addr(); + let server_name = config.server_name().clone(); + + let rustls_config = config.cert_mode.create_tls_client_config()?; + let connector = TlsConnector::from(Arc::new(rustls_config)); + + let tcp_stream = TcpStream::connect(server_addr) + .await + .context("Failed to establish underlying TCP connection")?; + if let Err(e) = tcp_stream.set_nodelay(true) { + log::error!("Failed to set TCP_NODELAY: {}", e); + } + let url = format!("wss://{}", server_name); + + let dns_name = server_name + .try_into() + .context("Invalid server name for TLS")?; + + let tls_stream = connector + .connect(dns_name, tcp_stream) + .await + .context("Failed to perform TLS handshake")?; + + let (ws_stream, _response) = client_async(url, tls_stream) + .await + .context("Failed to perform WebSocket handshake")?; + + Ok(ws_stream) +} diff --git a/vnt-core/src/utils/device_id.rs b/vnt-core/src/utils/device_id.rs new file mode 100644 index 0000000..15a337c --- /dev/null +++ b/vnt-core/src/utils/device_id.rs @@ -0,0 +1,29 @@ +use anyhow::Context; +use std::fs; +use std::path::Path; +pub fn get_device_id() -> anyhow::Result { + match machine_uid::get() { + Ok(id) => return Ok(id), + Err(e) => { + log::warn!("Failed to get system ID: {}. Using fallback.", e); + } + } + + get_fallback_id() +} +fn get_fallback_id() -> anyhow::Result { + let path = Path::new("device_id"); + + if let Ok(content) = fs::read_to_string(path) { + let id = content.trim(); + if !id.is_empty() { + return Ok(id.to_string()); + } + } + + let new_id = uuid::Uuid::new_v4().to_string(); + + fs::write(path, &new_id).context("Failed to write device_id file")?; + + Ok(new_id) +} diff --git a/vnt-core/src/utils/dns_query.rs b/vnt-core/src/utils/dns_query.rs new file mode 100644 index 0000000..e26bd7b --- /dev/null +++ b/vnt-core/src/utils/dns_query.rs @@ -0,0 +1,251 @@ +use anyhow::{Context, anyhow}; +use dns_parser::{Builder, Packet, QueryClass, QueryType, RData, ResponseCode}; +use rand::seq::SliceRandom; +use std::io; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::str::FromStr; +use std::time::Duration; +use tokio::net::UdpSocket; + +use rust_p2p_core::socket::LocalInterface; + +pub async fn dns_query_txt( + domain: &str, + mut name_servers: Vec, + default_interface: &Option, +) -> io::Result> { + let mut err: Option = None; + if name_servers.is_empty() { + name_servers.push("223.5.5.5:53".into()); + name_servers.push("114.114.114.114:53".into()); + } + for name_server in name_servers { + match txt_dns(domain, name_server, default_interface).await { + Ok(addr) => { + if !addr.is_empty() { + return Ok(addr); + } + } + Err(e) => { + err.replace(e); + } + } + continue; + } + if let Some(e) = err { + Err(e) + } else { + Err(io::Error::other(format!("DNS query failed {domain:?}"))) + } +} +pub async fn dns_query_one( + domain: &str, + name_servers: &Vec, + default_interface: &Option, +) -> anyhow::Result { + let mut vec = dns_query_all(domain, name_servers, default_interface).await?; + vec.shuffle(&mut rand::rng()); + vec.pop().context("DNS query failed") +} +pub async fn dns_query_all( + domain: &str, + name_servers: &Vec, + default_interface: &Option, +) -> anyhow::Result> { + match SocketAddr::from_str(domain) { + Ok(addr) => Ok(vec![addr]), + Err(_) => { + if name_servers.is_empty() { + let addrs: Vec = tokio::net::lookup_host(domain) + .await + .map_err(|e| io::Error::other(format!("DNS query failed: {domain:?},{e:?}")))? + .collect(); + return Ok(addrs); + } + + let mut err: Option = None; + for name_server in name_servers { + let end_index = domain + .rfind(':') + .ok_or_else(|| io::Error::other(format!("not port: {domain:?}")))?; + let host = &domain[..end_index]; + let port = u16::from_str(&domain[end_index + 1..]) + .map_err(|_| io::Error::other(format!("not port: {domain:?}")))?; + let th1 = { + let host = host.to_string(); + let name_server = name_server.clone(); + let default_interface = default_interface.clone(); + tokio::spawn(a_dns(host, name_server, default_interface.clone())) + }; + let th2 = { + let host = host.to_string(); + let name_server = name_server.clone(); + let default_interface = default_interface.clone(); + tokio::spawn(aaaa_dns(host, name_server, default_interface.clone())) + }; + let mut addr = Vec::new(); + match th1.await? { + Ok(rs) => { + for ip in rs { + addr.push(SocketAddr::new(ip.into(), port)); + } + } + Err(e) => { + err.replace(e); + } + } + match th2.await? { + Ok(rs) => { + for ip in rs { + addr.push(SocketAddr::new(ip.into(), port)); + } + } + Err(e) => { + if addr.is_empty() { + err.replace(e); + continue; + } + } + } + if addr.is_empty() { + continue; + } + return Ok(addr); + } + if let Some(e) = err { + Err(e.into()) + } else { + Err(anyhow!("DNS query failed {domain:?}")) + } + } + } +} + +async fn query<'a>( + udp: &UdpSocket, + domain: &str, + name_server: SocketAddr, + record_type: QueryType, + buf: &'a mut [u8], +) -> io::Result> { + let mut builder = Builder::new_query(1, true); + builder.add_question(domain, false, record_type, QueryClass::IN); + let packet = builder.build().unwrap(); + + udp.connect(name_server).await?; + let mut count = 0; + let len = loop { + udp.send(&packet).await?; + + match tokio::time::timeout(Duration::from_secs(3), udp.recv(buf)).await { + Ok(len) => { + break len?; + } + Err(_) => { + count += 1; + if count < 3 { + continue; + } + Err(io::Error::other(format!("DNS {name_server:?} recv error ")))? + } + }; + }; + + let pkt = Packet::parse(&buf[..len]).map_err(|e| { + io::Error::other(format!( + "domain {domain:?} DNS {name_server:?} data error: {e}" + )) + })?; + if pkt.header.response_code != ResponseCode::NoError { + return Err(io::Error::other(format!( + "response_code {} DNS {:?} domain {:?}", + pkt.header.response_code, name_server, domain + ))); + } + if pkt.answers.is_empty() { + return Err(io::Error::other(format!( + "No records received DNS {name_server:?} domain {domain:?}" + ))); + } + + Ok(pkt) +} + +pub async fn txt_dns( + domain: &str, + name_server: String, + default_interface: &Option, +) -> io::Result> { + let name_server: SocketAddr = name_server + .parse() + .map_err(|e| io::Error::other(format!("dns {name_server} is error :{e:?}")))?; + let udp = bind_udp(name_server, default_interface)?; + let mut buf = vec![0u8; 65536]; + let message = query(&udp, domain, name_server, QueryType::TXT, &mut buf).await?; + let mut rs = Vec::new(); + for record in message.answers { + if let RData::TXT(txt) = record.data { + for x in txt.iter() { + let txt = std::str::from_utf8(x) + .map_err(|_| io::Error::other("record type txt is not string"))?; + rs.push(txt.to_string()); + } + } + } + Ok(rs) +} + +fn bind_udp( + name_server: SocketAddr, + default_interface: &Option, +) -> io::Result { + let addr: SocketAddr = if name_server.is_ipv4() { + "0.0.0.0:0" + .parse() + .expect("valid IPv4 socket address literal") + } else { + "[::]:0".parse().expect("valid IPv6 socket address literal") + }; + let socket = rust_p2p_core::socket::bind_udp(addr, default_interface.as_ref())?; + UdpSocket::from_std(socket.into()) +} + +pub async fn a_dns( + domain: String, + name_server: String, + default_interface: Option, +) -> io::Result> { + let name_server: SocketAddr = name_server + .parse() + .map_err(|e| io::Error::other(format!("dns {name_server} is error :{e:?}")))?; + let udp = bind_udp(name_server, &default_interface)?; + let mut buf = vec![0u8; 65536]; + let message = query(&udp, &domain, name_server, QueryType::A, &mut buf).await?; + let mut rs = Vec::new(); + for record in message.answers { + if let RData::A(a) = record.data { + rs.push(a.0); + } + } + Ok(rs) +} + +pub async fn aaaa_dns( + domain: String, + name_server: String, + default_interface: Option, +) -> io::Result> { + let name_server: SocketAddr = name_server + .parse() + .map_err(|e| io::Error::other(format!("dns {name_server} is error :{e:?}")))?; + let udp = bind_udp(name_server, &default_interface)?; + let mut buf = vec![0u8; 65536]; + let message = query(&udp, &domain, name_server, QueryType::AAAA, &mut buf).await?; + let mut rs = Vec::new(); + for record in message.answers { + if let RData::AAAA(a) = record.data { + rs.push(a.0); + } + } + Ok(rs) +} diff --git a/vnt-core/src/utils/mod.rs b/vnt-core/src/utils/mod.rs new file mode 100644 index 0000000..bc70bbc --- /dev/null +++ b/vnt-core/src/utils/mod.rs @@ -0,0 +1,11 @@ +pub mod device_id; +pub(crate) mod dns_query; +pub mod task_control; +pub(crate) mod time { + pub fn now_ts_ms() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64 + } +} diff --git a/vnt-core/src/utils/task_control.rs b/vnt-core/src/utils/task_control.rs new file mode 100644 index 0000000..bed1d28 --- /dev/null +++ b/vnt-core/src/utils/task_control.rs @@ -0,0 +1,255 @@ +use parking_lot::Mutex; +use std::collections::HashMap; +use std::future::Future; +use std::sync::{Arc, Weak}; +use tokio::sync::Notify; +use tokio::task::{Id, JoinHandle}; + +struct TaskGroupState { + stopped: bool, + tasks: HashMap>, +} + +struct TaskGroupInner { + state: Mutex, + all_stopped_notify: Notify, +} + +impl TaskGroupInner { + fn new() -> Self { + Self { + state: Mutex::new(TaskGroupState { + stopped: false, + tasks: HashMap::new(), + }), + all_stopped_notify: Notify::new(), + } + } + + fn spawn(self: &Arc, f: F) -> Option + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + let mut state = self.state.lock(); + if state.stopped { + return None; + } + + let guard = TaskGuard { + inner: Arc::downgrade(self), + }; + + let handle = tokio::spawn(async move { + let _guard = guard; + f.await; + }); + + let task_id = handle.id(); + state.tasks.insert(task_id, handle); + Some(task_id) + } + + fn stop(&self) { + let mut state = self.state.lock(); + state.stopped = true; + for (_, handle) in state.tasks.drain() { + handle.abort(); + } + } + + fn is_stopped(&self) -> bool { + self.state.lock().stopped + } + + fn remove_task(&self, task_id: Id) { + let all_stopped = { + let mut state = self.state.lock(); + state.tasks.remove(&task_id); + if state.tasks.is_empty() { + state.stopped = true; + true + } else { + false + } + }; + if all_stopped { + self.all_stopped_notify.notify_waiters(); + } + } + + async fn abort_task(&self, task_id: Id) { + let handle = self.state.lock().tasks.remove(&task_id); + if let Some(handle) = handle { + handle.abort(); + _ = handle.await; + } + } + + async fn join_all(&self) { + let tasks = std::mem::take(&mut self.state.lock().tasks); + for (_, h) in tasks { + let _ = h.await; + } + } + + fn all_tasks_stopped(&self) -> bool { + let state = self.state.lock(); + state.stopped && state.tasks.is_empty() + } +} + +impl Drop for TaskGroupInner { + fn drop(&mut self) { + self.stop(); + } +} + +struct TaskGuard { + inner: Weak, +} + +impl Drop for TaskGuard { + fn drop(&mut self) { + if let Some(inner) = self.inner.upgrade() { + let task_id = tokio::task::id(); + inner.remove_task(task_id); + } + } +} + +#[derive(Clone)] +pub struct TaskGroup { + inner: Arc, +} + +impl TaskGroup { + fn new() -> Self { + Self { + inner: Arc::new(TaskGroupInner::new()), + } + } + + pub fn stop(&self) { + self.inner.stop(); + } + + pub fn is_stopped(&self) -> bool { + self.inner.is_stopped() + } + + pub fn spawn(&self, f: F) -> SubTask + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + match self.inner.spawn(f) { + Some(task_id) => SubTask::new(task_id, Arc::downgrade(&self.inner)), + None => SubTask::empty(), + } + } + + pub async fn join_all(&self) { + self.inner.join_all().await; + } + + pub async fn wait_all_stopped(&self) { + loop { + if self.inner.all_tasks_stopped() { + return; + } + self.inner.all_stopped_notify.notified().await; + } + } +} + +pub struct SubTask { + task_id: Option, + inner: Weak, +} + +impl SubTask { + fn new(task_id: Id, inner: Weak) -> Self { + Self { + task_id: Some(task_id), + inner, + } + } + + fn empty() -> Self { + Self { + task_id: None, + inner: Weak::new(), + } + } + + pub async fn stop(&self) { + if let Some(task_id) = self.task_id + && let Some(inner) = self.inner.upgrade() + { + inner.abort_task(task_id).await; + } + } + + pub fn is_running(&self) -> bool { + if let Some(task_id) = self.task_id + && let Some(inner) = self.inner.upgrade() + { + return inner.state.lock().tasks.contains_key(&task_id); + } + false + } + + pub fn id(&self) -> Option { + self.task_id + } +} +#[derive(Clone, Default)] +pub struct TaskGroupManager { + task_group: Arc>>, +} + +impl TaskGroupManager { + pub fn new() -> Self { + TaskGroupManager::default() + } + + pub fn is_running(&self) -> bool { + self.task_group.lock().is_some() + } + + pub fn is_stopped(&self) -> bool { + self.task_group.lock().is_none() + } + + pub fn create_task(&self) -> anyhow::Result<(TaskGroup, TaskGroupGuard)> { + let mut guard = self.task_group.lock(); + if guard.is_some() { + anyhow::bail!("运行中") + } + + let task_group = TaskGroup::new(); + guard.replace(task_group.clone()); + let stop_guard = TaskGroupGuard { + task_group: self.task_group.clone(), + }; + Ok((task_group, stop_guard)) + } + + pub fn stop(&self) { + let option = self.task_group.lock(); + if let Some(task_group) = option.as_ref() { + task_group.stop(); + } + } +} +pub struct TaskGroupGuard { + task_group: Arc>>, +} +impl Drop for TaskGroupGuard { + fn drop(&mut self) { + if let Some(task_group) = self.task_group.lock().take() { + task_group.stop(); + } + } +} diff --git a/vnt-ipc/Cargo.toml b/vnt-ipc/Cargo.toml new file mode 100644 index 0000000..96a1abf --- /dev/null +++ b/vnt-ipc/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "vnt-ipc" +version = "2.0.0" +edition = "2024" + +[dependencies] + +vnt-core = { path = "../vnt-core" } +anyhow = "1.0.100" +futures = "0.3.31" +prost = "0.14.1" +tokio = "1.48.0" +tokio-util = "0.7.17" +log = "0.4.29" +console = "0.16.2" +time = "0.3.44" +cli-table = "0.5.0" + +[build-dependencies] +prost-build = "0.14" \ No newline at end of file diff --git a/vnt-ipc/build.rs b/vnt-ipc/build.rs new file mode 100644 index 0000000..3dde738 --- /dev/null +++ b/vnt-ipc/build.rs @@ -0,0 +1,7 @@ +fn main() { + let mut config = prost_build::Config::new(); + config.protoc_arg("--experimental_allow_proto3_optional"); + config + .compile_protos(&["proto/local_ipc.proto"], &["proto"]) + .unwrap(); +} diff --git a/vnt-ipc/proto/local_ipc.proto b/vnt-ipc/proto/local_ipc.proto new file mode 100644 index 0000000..266e730 --- /dev/null +++ b/vnt-ipc/proto/local_ipc.proto @@ -0,0 +1,103 @@ +syntax = "proto3"; + +package protocol.local_ipc; + +message IpcRequest{ + oneof ipc_cmd{ + AppInfoCMD app_info = 1; + ClientIpsCMD client_ips = 2; + ClientListCMD client_list = 3; + AllRouteCMD all_route = 4; + } +} +message AppInfoCMD{ + +} +message ClientIpsCMD{ + +} +message ClientListCMD{ + +} +message AllRouteCMD{ + +} + + +message IpcResponse{ + oneof response_payload{ + AppInfo app_info = 1; + ClientIpList client_ips = 2; + ClientInfoList client_list = 3; + ClientRouteList all_route = 4; + } +} + +message AppInfo{ + repeated ServerInfo server_info = 1; + string name = 3; + string version = 4; + optional uint32 ip = 5; + uint32 online_client_num = 6; + uint32 offline_client_num = 7; + uint32 direct_client_num = 8; + string device_id = 11; + optional string nat_type = 12; + repeated string public_ipv4s = 13; + optional string public_ipv6 = 14; +} + +message ServerInfo{ + string server = 1; + bool connected = 2; + optional uint32 server_rtt = 3; + optional int64 last_connected_time = 4; +} + +message ClientInfoList{ + repeated ClientInfoItem items = 1; +} + +message ClientInfoItem{ + uint32 ip = 1; + string name = 2; + string version = 3; + bool online = 4; + bool is_direct = 5; + int64 last_connected_time = 6; + optional uint32 rtt = 7; + bool key_equal = 8; + optional PacketLoss packet_loss = 9; +} + +message PacketLoss{ + uint64 sent = 1; + uint64 received = 2; + double loss_rate = 3; +} + +message ClientRouteList{ + repeated ClientRouteItem items = 1; +} + +message ClientRouteItem{ + uint32 ip = 1; + repeated Route route_list = 2; +} + +message Route{ + string addr = 1; + uint32 metric = 2; + uint32 rtt = 3; +} + +message ClientIpList{ + repeated ClientIpItem items = 1; +} + +message ClientIpItem{ + uint32 ip = 1; + bool online = 2; + bool is_direct = 3; + optional uint32 rtt = 4; +} \ No newline at end of file diff --git a/vnt-ipc/src/client.rs b/vnt-ipc/src/client.rs new file mode 100644 index 0000000..13bfdd1 --- /dev/null +++ b/vnt-ipc/src/client.rs @@ -0,0 +1,285 @@ +use crate::message::ipc_request::IpcCmd; +use crate::message::ipc_response::ResponsePayload; +use crate::message::{ + AppInfo, ClientInfoList, ClientIpList, ClientRouteList, IpcRequest, IpcResponse, +}; +use crate::{DEFAULT_PORT, get_port_file_path}; +use anyhow::Context; +use cli_table::{Cell, Style, Table, print_stdout}; +use console::style; +use futures::{SinkExt, StreamExt}; +use prost::Message; +use std::fs; +use std::net::Ipv4Addr; +use std::time::Duration; +use time::macros::format_description; +use time::{OffsetDateTime, UtcOffset}; +use tokio::net::TcpStream; +use tokio_util::codec::{Framed, LengthDelimitedCodec}; + +pub async fn run_client(cmd: IpcCmd, port: Option) -> anyhow::Result<()> { + let path = get_port_file_path(); + let port = if let Some(port) = port { + port + } else { + match fs::read_to_string(&path) { + Ok(port_string) => port_string.trim().parse().context("parse port error")?, + Err(_) => DEFAULT_PORT, + } + }; + + let addr = format!("127.0.0.1:{}", port); + + let stream = tokio::time::timeout(Duration::from_secs(3), TcpStream::connect(&addr)) + .await + .context("Connection timed out")??; + + let mut framed = Framed::new(stream, LengthDelimitedCodec::new()); + + framed + .send(IpcRequest { ipc_cmd: Some(cmd) }.encode_to_vec().into()) + .await?; + let response = framed.next().await.context("Unexpected end of stream")??; + let response = IpcResponse::decode(response).context("decode response error")?; + match response + .response_payload + .context("response payload is empty")? + { + ResponsePayload::AppInfo(info) => { + print_app_info(info); + } + ResponsePayload::ClientIps(list) => print_client_ip_list(list)?, + ResponsePayload::ClientList(list) => { + print_client_list(list)?; + } + ResponsePayload::AllRoute(route_list) => { + print_route_list(route_list)?; + } + }; + Ok(()) +} + +fn key_style(s: &str) -> console::StyledObject<&str> { + style(s).cyan().bright().bold() +} +pub fn print_app_info(info: AppInfo) { + println!( + "\n{}", + style("--- Application Information ---").italic().dim() + ); + + for server_info in info.server_info { + let status_colored = if server_info.connected { + if let Some(rtt) = server_info.server_rtt { + style(format!("Online {rtt}ms")) + .green() + .bright() + .to_string() + } else { + style("Online").green().bright().to_string() + } + } else { + style("Offline").red().bright().to_string() + }; + + println!( + "{:24}{} ({})", + key_style("Server:"), + style(&server_info.server).white(), + status_colored + ); + + let time_str = server_info + .last_connected_time + .map(|v| style(ts_to_string(v / 1000)).white().to_string()) + .unwrap_or_else(|| "Never".to_string()); + + println!("{:24}{}", key_style("Last Connected Time:"), time_str); + } + + println!("{:24}{}", key_style("Name:"), style(&info.name).white()); + println!("{:24}{}", key_style("Id:"), style(&info.device_id).white()); + println!( + "{:24}{}", + key_style("Version:"), + style(&info.version).white() + ); + + let ip_str = info + .ip + .map(|v| { + style(Ipv4Addr::from(v).to_string()) + .yellow() + .bright() + .to_string() + }) + .unwrap_or_else(|| style("N/A").red().bright().to_string()); + + println!("{:24}{}", key_style("IP:"), ip_str); + + let total_clients = info.online_client_num + info.offline_client_num; + + println!( + "{:24}{}", + key_style("Total Clients:"), + style(total_clients).color256(213) + ); + + println!( + "{:24}{}", + key_style("Online Clients:"), + style(info.online_client_num).color256(82) + ); + + println!( + "{:24}{}", + key_style("P2P Clients:"), + style(info.direct_client_num).color256(117) + ); + println!( + "{:24}{}", + key_style("Nat Type:"), + style(info.nat_type.unwrap_or_else(|| "Unknown".to_string())).color256(117) + ); + println!( + "{:24}{}", + key_style("Ipv6:"), + style(info.public_ipv6.unwrap_or_default()).color256(117) + ); + println!( + "{:24}{}", + key_style("Public Ipv4:"), + style(info.public_ipv4s.join(",")).color256(117) + ); + println!( + "{}", + style("-------------------------------").italic().dim() + ); +} + +fn print_client_ip_list(list: ClientIpList) -> anyhow::Result<()> { + println!("\n--- Client List ({}) ---", list.items.len()); + + let table = list + .items + .iter() + .map(|item| { + vec![ + Ipv4Addr::from(item.ip).to_string().cell(), + item.online.to_string().cell(), + item.is_direct.to_string().cell(), + item.rtt.map(|v| v.to_string()).unwrap_or_default().cell(), + ] + }) + .table() + .title(vec![ + "IP".cell().bold(true), + "Online".cell().bold(true), + "P2P".cell().bold(true), + "RTT".cell().bold(true), + ]); + + print_stdout(table)?; + println!("\n"); + Ok(()) +} + +fn print_client_list(list: ClientInfoList) -> anyhow::Result<()> { + println!("\n--- Client List ({}) ---", list.items.len()); + + let table = list + .items + .iter() + .map(|item| { + let key_equal: bool = item.key_equal; + let mut ip_str = Ipv4Addr::from(item.ip).to_string(); + if !key_equal { + ip_str.push_str("(Key Mismatch)"); + } + let loss_str = item + .packet_loss + .as_ref() + .map(|v| format!("{:.1}%", v.loss_rate)) + .unwrap_or_default(); + + vec![ + ip_str.cell(), + item.name.clone().cell(), + item.version.clone().cell(), + item.online.to_string().cell(), + item.is_direct.to_string().cell(), + item.rtt.map(|v| v.to_string()).unwrap_or_default().cell(), + loss_str.cell(), + ts_to_string(item.last_connected_time).cell(), + ] + }) + .table() + .title(vec![ + "IP".cell().bold(true), + "Name".cell().bold(true), + "Version".cell().bold(true), + "Online".cell().bold(true), + "P2P".cell().bold(true), + "RTT".cell().bold(true), + "Loss".cell().bold(true), + "Last Connected Time".cell().bold(true), + ]); + + print_stdout(table)?; + println!("\n"); + Ok(()) +} + +pub fn print_route_list(route_list: ClientRouteList) -> anyhow::Result<()> { + let total_routes: usize = route_list + .items + .iter() + .map(|item| item.route_list.len()) + .sum(); + + println!("\n--- All Routes List (Total: {}) ---", total_routes); + + let mut rows = Vec::new(); + for client_route in route_list.items { + let client_ip_str = Ipv4Addr::from(client_route.ip).to_string(); + + for route in client_route.route_list { + rows.push(vec![ + client_ip_str.clone().cell(), + route.metric.to_string().cell(), + route.rtt.to_string().cell(), + route.addr.clone().cell(), + ]); + } + } + + let table = rows.table().title(vec![ + "Destination IP".cell().bold(true), + "Metric".cell().bold(true), + "RTT (ms)".cell().bold(true), + "Remote Address".cell().bold(true), + ]); + + print_stdout(table)?; + println!("\n"); + Ok(()) +} + +pub fn ts_to_string(ts_secs: i64) -> String { + let dt = match OffsetDateTime::from_unix_timestamp(ts_secs) { + Ok(dt) => dt, + Err(_) => { + return String::new(); + } + }; + let local_offset = match UtcOffset::local_offset_at(dt) { + Ok(offset) => offset, + Err(_e) => match UtcOffset::from_hms(8, 0, 0) { + Ok(offset) => offset, + Err(_) => return String::new(), + }, + }; + let dt_local = dt.to_offset(local_offset); + let format = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"); + dt_local.format(&format).unwrap() +} diff --git a/vnt-ipc/src/lib.rs b/vnt-ipc/src/lib.rs new file mode 100644 index 0000000..e9fc7aa --- /dev/null +++ b/vnt-ipc/src/lib.rs @@ -0,0 +1,12 @@ +pub mod client; +pub mod message; +pub mod server; + +pub use vnt_core::*; +const DEFAULT_PORT: u16 = 11233; + +const PORT_FILE: &str = "PORT"; + +fn get_port_file_path() -> std::path::PathBuf { + std::path::PathBuf::from(PORT_FILE) +} diff --git a/vnt-ipc/src/message.rs b/vnt-ipc/src/message.rs new file mode 100644 index 0000000..c6e193c --- /dev/null +++ b/vnt-ipc/src/message.rs @@ -0,0 +1,4 @@ +mod proto { + include!(concat!(env!("OUT_DIR"), "/protocol.local_ipc.rs")); +} +pub use proto::*; diff --git a/vnt-ipc/src/server.rs b/vnt-ipc/src/server.rs new file mode 100644 index 0000000..bfd3018 --- /dev/null +++ b/vnt-ipc/src/server.rs @@ -0,0 +1,194 @@ +use crate::{DEFAULT_PORT, get_port_file_path}; + +use crate::message::ipc_request::IpcCmd; +use crate::message::ipc_response::ResponsePayload; +use crate::message::{ + AppInfo, ClientInfoItem, ClientInfoList, ClientIpItem, ClientIpList, ClientRouteItem, + ClientRouteList, IpcRequest, IpcResponse, PacketLoss, Route, ServerInfo, +}; +use anyhow::bail; +use futures::{SinkExt, StreamExt}; +use prost::Message; +use std::fs; +use std::net::Ipv4Addr; +use tokio::io::{self}; +use tokio::net::{TcpListener, TcpStream}; +use tokio_util::codec::{Framed, LengthDelimitedCodec}; +use vnt_core::api::VntApi; + +async fn handle_connection(stream: TcpStream, vnt_api: VntApi) -> anyhow::Result<()> { + let mut framed = Framed::new(stream, LengthDelimitedCodec::new()); + + if let Some(Ok(message)) = framed.next().await { + let request = IpcRequest::decode(message.as_ref())?; + let Some(cmd) = request.ipc_cmd else { + bail!("Received an IpcRequest but it was None"); + }; + let rs = match cmd { + IpcCmd::AppInfo(_) => { + let info = app_info(&vnt_api); + ResponsePayload::AppInfo(info) + } + IpcCmd::ClientIps(_) => { + let items = vnt_api + .client_ips() + .into_iter() + .map(|v| { + let rtt = if let Some(v) = vnt_api.find_route(&v.ip) { + Some(v.rtt()) + } else { + vnt_api.server_node_rtt(&v.ip).map(|v| v * 2) + }; + ClientIpItem { + ip: v.ip.into(), + online: v.online, + is_direct: vnt_api.is_direct(&v.ip), + rtt, + } + }) + .collect(); + ResponsePayload::ClientIps(ClientIpList { items }) + } + IpcCmd::ClientList(_) => { + let client_list = vnt_api.server_rpc().client_list().await?; + let key_sign = vnt_api.get_config().and_then(|config| config.key_sign()); + let items = client_list + .list + .into_iter() + .map(|v| { + let ip = Ipv4Addr::from(v.ip); + let rtt = if let Some(v) = vnt_api.find_route(&ip) { + Some(v.rtt()) + } else { + vnt_api.server_node_rtt(&ip).map(|v| v * 2) + }; + let packet_loss = vnt_api.packet_loss_info(&ip).map(|info| PacketLoss { + sent: info.sent, + received: info.received, + loss_rate: info.loss_rate, + }); + ClientInfoItem { + ip: v.ip, + name: v.name, + version: v.version, + online: v.online, + is_direct: vnt_api.is_direct(&Ipv4Addr::from(v.ip)), + last_connected_time: v.last_connected_time, + rtt, + key_equal: key_sign == v.key_sign, + packet_loss, + } + }) + .collect(); + ResponsePayload::ClientList(ClientInfoList { items }) + } + IpcCmd::AllRoute(_) => { + let route_list = all_route(&vnt_api); + ResponsePayload::AllRoute(route_list) + } + }; + let rs = IpcResponse { + response_payload: Some(rs), + }; + framed.send(rs.encode_to_vec().into()).await?; + } + + Ok(()) +} +fn app_info(vnt_api: &VntApi) -> AppInfo { + let config = vnt_api.get_config(); + let ips = vnt_api.client_ips(); + let online_client_num = ips.iter().filter(|v| v.online).count() as _; + let offline_client_num = ips.iter().filter(|v| !v.online).count() as _; + let direct_client_num = ips.iter().filter(|ip| vnt_api.is_direct(&ip.ip)).count() as _; + let server_node_list = vnt_api.server_node_list(); + let nat_info = vnt_api.nat_info(); + AppInfo { + server_info: server_node_list + .into_iter() + .map(|v| ServerInfo { + server: v.server_addr.to_string(), + connected: v.connected, + server_rtt: v.rtt, + last_connected_time: v.last_connected_time, + }) + .collect(), + name: config + .as_ref() + .map(|v| v.device_name.clone()) + .unwrap_or_default(), + version: env!("CARGO_PKG_VERSION").to_string(), + ip: vnt_api.network().map(|v| v.ip.into()), + online_client_num, + offline_client_num, + direct_client_num, + device_id: config.map(|v| v.device_id.clone()).unwrap_or_default(), + nat_type: nat_info.as_ref().map(|v| format!("{:?}", v.nat_type)), + public_ipv4s: nat_info + .as_ref() + .map(|v| { + v.public_ips + .iter() + .map(|v| v.to_string()) + .collect::>() + }) + .unwrap_or_default(), + public_ipv6: nat_info + .as_ref() + .and_then(|v| v.ipv6.map(|v| v.to_string())), + } +} +fn all_route(vnt_api: &VntApi) -> ClientRouteList { + let vec = vnt_api.route_table(); + let mut items = Vec::with_capacity(vec.len()); + for (ip, route_list) in vec { + items.push(ClientRouteItem { + ip: ip.into(), + route_list: route_list + .into_iter() + .map(|v| Route { + addr: v.route_key().to_string(), + metric: v.metric() as _, + rtt: v.rtt(), + }) + .collect(), + }) + } + ClientRouteList { items } +} + +pub async fn run_server(bind_port: Option, vnt_api: VntApi) -> anyhow::Result<()> { + let mut port = bind_port.unwrap_or(DEFAULT_PORT); + let listener; + + loop { + match TcpListener::bind(format!("127.0.0.1:{}", port)).await { + Ok(l) => { + listener = l; + break; + } + Err(e) if bind_port.is_none() && e.kind() == io::ErrorKind::AddrInUse => { + port = 0; + } + Err(e) => bail!("bind :{e:?}"), + } + } + + let bound_addr = listener.local_addr()?; + log::info!("IPC Listening on {}", bound_addr); + let actual_port = bound_addr.port(); + + let path = get_port_file_path(); + fs::write(&path, actual_port.to_string())?; + + loop { + let (stream, peer_addr) = listener.accept().await?; + log::info!("IPC Connection from {}", peer_addr); + let vnt_api = vnt_api.clone(); + tokio::spawn(async move { + if let Err(e) = handle_connection(stream, vnt_api).await { + log::warn!("IPC Error handling client: {:?},peer_addr={peer_addr}", e); + } + }); + } +} diff --git a/vnt-web/Cargo.toml b/vnt-web/Cargo.toml new file mode 100644 index 0000000..d9535ed --- /dev/null +++ b/vnt-web/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "vnt-web" +version = "2.0.0" +edition = "2024" + +[dependencies] +vnt-core = { path = "../vnt-core" } +tokio = { version = "1", features = ["full"] } +axum = "0.8.8" +tower-http = { version = "0.6", features = ["fs", "cors", "trace"] } + +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +anyhow = "1.0.100" +log = "0.4.29" +ipnet = "2.11.0" +hostname = "0.4.2" +route_manager = "0.2.11" +toml = "0.9.8" +rust-embed = "8.0" +mime_guess = "2.0" +parking_lot = "0.12" +time = { version = "0.3.45", features = ["local-offset", "formatting", "macros"] } \ No newline at end of file diff --git a/vnt-web/src/lib.rs b/vnt-web/src/lib.rs new file mode 100644 index 0000000..44d9192 --- /dev/null +++ b/vnt-web/src/lib.rs @@ -0,0 +1,17 @@ +mod service_http; + +pub use service_http::run_http_server; + +struct ScopeGuard(Option); + +impl Drop for ScopeGuard { + fn drop(&mut self) { + if let Some(f) = self.0.take() { + f(); + } + } +} + +fn defer(f: F) -> ScopeGuard { + ScopeGuard(Some(f)) +} diff --git a/vnt-web/src/service_http.rs b/vnt-web/src/service_http.rs new file mode 100644 index 0000000..c0bcd67 --- /dev/null +++ b/vnt-web/src/service_http.rs @@ -0,0 +1,1105 @@ +use crate::defer; +use anyhow::{Context, anyhow}; +use axum::body::Body; +use axum::http::{HeaderMap, HeaderValue, StatusCode, Uri, header}; +use axum::response::IntoResponse; +use axum::{ + Json, Router, + extract::{Query, Request, State}, + middleware, + response::Response, + routing::{get, post}, +}; +use ipnet::Ipv4Net; +use mime_guess::from_path; +use parking_lot::Mutex; +use rust_embed::RustEmbed; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::{Instant, SystemTime, UNIX_EPOCH}; +use time::{OffsetDateTime, format_description}; +use tokio::fs; +use tokio::net::TcpListener; +use tower_http::cors::{Any, CorsLayer}; +use vnt_core::api::VntApi; +use vnt_core::context::config::Config as CoreConfig; +use vnt_core::core::{DEFAULT_MTU, NetworkManager}; +use vnt_core::nat::NetInput; +use vnt_core::port_mapping::PortMapping; +use vnt_core::tls::verifier::CertValidationMode; +use vnt_core::tunnel_core::server::transport::config::ProtocolAddress; +use vnt_core::utils::task_control::TaskGroupManager; + +const CONFIG_DIR: &str = "vnt_config"; +const CURRENT_CONFIG_RECORD: &str = "vnt_current_config.txt"; + +#[derive(Serialize, Clone, Copy, PartialEq, Eq, Default)] +#[serde(rename_all = "lowercase")] +enum VntStatus { + #[default] + Stopped, + Starting, + Running, +} + +#[derive(Clone)] +struct HttpAppState { + task_group_manager: TaskGroupManager, + inner: Arc>, +} + +#[derive(Default)] +struct HttpAppStateInner { + vnt: Option, + status: VntStatus, + start_logs: Vec, +} + +impl HttpAppState { + fn starting(&self) -> anyhow::Result<()> { + let mut inner = self.inner.lock(); + if inner.status != VntStatus::Stopped { + return Err(anyhow!("VNT is already starting or running")); + } + if inner.vnt.is_some() { + return Err(anyhow!("VNT is already running")); + } + inner.status = VntStatus::Starting; + inner.start_logs.clear(); + Ok(()) + } + fn stopped(&self) { + let mut inner = self.inner.lock(); + inner.vnt.take(); + inner.status = VntStatus::Stopped; + } + fn starting_to_stopped(&self) { + let mut inner = self.inner.lock(); + if inner.status != VntStatus::Starting { + return; + } + inner.vnt.take(); + inner.status = VntStatus::Stopped; + inner + .start_logs + .push(format!("[{}] 启动中断", HttpAppState::timestamp())); + } + fn starting_to_running(&self) { + let mut inner = self.inner.lock(); + if inner.status != VntStatus::Starting { + log::error!("starting_to_running VNT is not starting"); + return; + } + inner.status = VntStatus::Running; + inner.start_logs.clear(); + } + + fn record_log(&self, msg: impl Into) { + let mut inner = self.inner.lock(); + if inner.status != VntStatus::Starting { + return; + } + inner + .start_logs + .push(format!("[{}] {}", Self::timestamp(), msg.into())); + } + fn record_log_and_stopped(&self, msg: impl Into) { + let mut inner = self.inner.lock(); + if inner.status != VntStatus::Starting { + return; + } + inner + .start_logs + .push(format!("[{}] {}", Self::timestamp(), msg.into())); + inner.status = VntStatus::Stopped; + } + fn status(&self) -> VntStatus { + self.inner.lock().status + } + + fn timestamp() -> String { + let now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()); + let format = format_description::parse("[hour]:[minute]:[second]").unwrap(); + now.format(&format) + .unwrap_or_else(|_| "00:00:00".to_string()) + } +} + +struct VntHandler { + api: VntApi, + config_name: String, + config_file_name: String, +} + +#[derive(Serialize)] +struct ApiResponse { + code: i32, + msg: String, + data: Option, +} + +impl ApiResponse { + fn success(data: T) -> Self { + Self { + code: 0, + msg: "success".to_string(), + data: Some(data), + } + } + + fn error(msg: impl Into) -> Self { + Self { + code: -1, + msg: msg.into(), + data: None, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct StartConfig { + pub config_name: Option, + pub server: Vec, + pub cert_mode: Option, + pub network_code: String, + pub device_id: Option, + pub device_name: Option, + pub tun_name: Option, + pub ip: Option, + pub password: Option, + #[serde(default)] + pub no_punch: bool, + #[serde(default)] + pub compress: bool, + #[serde(default)] + pub rtx: bool, + #[serde(default)] + pub fec: bool, + #[serde(default)] + pub input: Vec, + #[serde(default)] + pub output: Vec, + #[serde(default)] + pub no_nat: bool, + #[serde(default)] + pub no_tun: bool, + pub mtu: Option, + #[serde(default)] + pub port_mapping: Vec, + #[serde(default)] + pub allow_mapping: bool, + #[serde(default)] + pub udp_stun: Vec, + #[serde(default)] + pub tcp_stun: Vec, +} + +#[derive(Deserialize)] +struct SaveConfigReq { + file_name: Option, + config: String, +} + +#[derive(Deserialize)] +struct FileReq { + file_name: String, +} + +#[derive(Serialize)] +struct ConfigSummary { + file_name: String, + config_name: String, +} + +#[derive(Serialize, Default)] +struct HttpAppInfo { + name: String, + version: String, + ip: Option, + prefix_len: Option, + gateway: Option, + device_id: String, + status: VntStatus, + current_config_name: Option, + current_config_file: Option, + online_client_num: usize, + offline_client_num: usize, + direct_client_num: usize, + server_info: Vec, + nat_type: Option, + public_ipv6: Option, + public_ipv4s: Vec, + network_code: Option, + mtu: Option, + fec: Option, + compress: Option, + encrypt: Option, + rtx: Option, +} + +#[derive(Serialize)] +struct HttpServerInfo { + server: String, + connected: bool, + server_rtt: Option, + server_version: Option, +} + +#[derive(Serialize)] +struct HttpClientItem { + ip: Ipv4Addr, + name: Option, + online: bool, + route: Option, + version: String, + last_connected_time: i64, + key_equal: i32, + nat_info: Option, + packet_loss: Option, + traffic: Option, +} + +#[derive(Serialize)] +struct HttpClientNatInfo { + nat_type: String, + public_ips: Vec, + ipv6: Option, +} + +#[derive(Serialize)] +struct HttpPacketLoss { + sent: u64, + received: u64, + loss_rate: f64, +} + +#[derive(Serialize)] +struct HttpTraffic { + tx_bytes: u64, + rx_bytes: u64, +} + +#[derive(Serialize)] +struct HttpRouteItem { + ip: Ipv4Addr, + routes: Vec, +} + +#[derive(Serialize)] +struct HttpRouteDetail { + addr: String, + protocol: String, + metric: u8, + rtt: u32, +} + +#[derive(Serialize)] +struct StartStatusResponse { + status: VntStatus, + logs: Vec, +} + +async fn get_start_status( + State(state): State, +) -> Json> { + let lock = state.inner.lock(); + Json(ApiResponse::success(StartStatusResponse { + status: lock.status, + logs: lock.start_logs.clone(), + })) +} + +async fn logging_middleware(req: Request, next: axum::middleware::Next) -> Response { + let method = req.method().clone(); + let uri = req.uri().clone(); + let start = Instant::now(); + let response = next.run(req).await; + log::info!( + "Request: {} {} | Status: {} | Took: {:?}", + method, + uri, + response.status(), + start.elapsed() + ); + response +} + +#[derive(RustEmbed)] +#[folder = "static/"] +struct Asset; + +pub async fn run_http_server( + addr: SocketAddr, + start_config_file_name: Option, +) -> anyhow::Result<()> { + fs::create_dir_all(CONFIG_DIR) + .await + .context("Failed to create config directory")?; + + let state = HttpAppState { + task_group_manager: TaskGroupManager::new(), + inner: Arc::new(Default::default()), + }; + + // 自动启动逻辑 + let auto_start_file = determine_auto_start_file(start_config_file_name).await; + + if let Some((file_name, path)) = auto_start_file { + log::info!("Auto starting VNT with config: {:?}", path); + let state_clone = state.clone(); + tokio::spawn(async move { + if let Err(e) = start_vnt_internal(&state_clone, file_name, path).await { + log::error!("Auto start failed: {:?}", e); + } + }); + } + + let cors = CorsLayer::new() + .allow_origin(Any) + .allow_methods(Any) + .allow_headers(Any); + + let app = Router::new() + .route("/api/info", get(get_info)) + .route("/api/peers", get(get_peers)) + .route("/api/routes", get(get_routes)) + .route("/api/start/status", get(get_start_status)) + .route("/api/start", post(start_vnt_handler)) + .route("/api/stop", post(stop_vnt_handler)) + .route("/api/restart", post(restart_vnt_handler)) + .route("/api/config/list", get(list_configs)) + .route( + "/api/config", + get(get_config).post(save_config).delete(delete_config), + ) + .layer(cors) + .layer(middleware::from_fn(logging_middleware)) + .with_state(state) + .fallback(static_handler); + + log::info!("HTTP API Listening on http://{}", addr); + let listener = TcpListener::bind(addr).await?; + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) + .await?; + + Ok(()) +} + +/// 确定自动启动的配置文件 +async fn determine_auto_start_file( + start_config_file_name: Option, +) -> Option<(String, PathBuf)> { + let path = if let Some(name) = start_config_file_name { + Some(name) + } else if Path::new(CURRENT_CONFIG_RECORD).exists() { + fs::read_to_string(CURRENT_CONFIG_RECORD) + .await + .ok() + .filter(|content| !content.trim().is_empty()) + .map(|content| Path::new(CONFIG_DIR).join(content.trim())) + } else { + None + }; + + path.and_then(|p| { + let file_name = p.file_name()?.to_str()?.to_string(); + if p.exists() { + Some((file_name, p)) + } else { + log::warn!("Auto start config file not found: {:?}", p); + None + } + }) +} + +fn build_headers_for_path(path: &str) -> HeaderMap { + let mut headers = HeaderMap::new(); + + let is_gz = path.ends_with(".gz"); + + let mime = if is_gz { + let original = path.trim_end_matches(".gz"); + from_path(original).first_or_octet_stream() + } else { + from_path(path).first_or_octet_stream() + }; + headers.insert( + header::CONTENT_TYPE, + HeaderValue::from_str(mime.as_ref()).unwrap(), + ); + + if is_gz { + headers.insert(header::CONTENT_ENCODING, HeaderValue::from_static("gzip")); + headers.insert(header::VARY, HeaderValue::from_static("Accept-Encoding")); + } + headers.insert( + header::CACHE_CONTROL, + HeaderValue::from_static("public, max-age=31536000, immutable"), + ); + headers +} +async fn static_handler(uri: Uri) -> impl IntoResponse { + let path = uri.path().trim_start_matches('/'); + let path = if path.is_empty() { "index.html" } else { path }; + + // 先尝试从本地文件读取 + let local_path = Path::new("static").join(path); + if local_path.is_file() + && let Ok(content) = tokio::fs::read(&local_path).await + { + log::debug!("Serving file from local filesystem: {:?}", local_path); + let mime = from_path(&local_path).first_or_octet_stream(); + return ([(header::CONTENT_TYPE, mime.as_ref())], content).into_response(); + } + + // 从内嵌数据中读取 + if let Some(content) = Asset::get(path) { + log::debug!("Serving file from embedded assets: {}", path); + let headers = build_headers_for_path(path); + return (headers, Body::from(content.data)).into_response(); + } + + (StatusCode::NOT_FOUND, "404 Not Found").into_response() +} + +/// 启动 VNT 服务的入口函数 +async fn start_vnt_internal( + state: &HttpAppState, + file_name: String, + file_path: PathBuf, +) -> anyhow::Result<()> { + log::info!("Starting VNT service: {}", file_name); + state.starting()?; + + let state_for_error = state.clone(); + let on_error_guard = defer(move || { + state_for_error.starting_to_stopped(); + }); + + state.record_log(format!("启动配置: {}", file_name)); + state.record_log("读取配置文件"); + + // 读取并解析配置 + let content = fs::read_to_string(&file_path) + .await + .with_context(|| format!("Config file not found: {:?}", file_path))?; + + state.record_log("解析配置文件内容"); + let cfg: StartConfig = toml::from_str(&content).context("Failed to parse TOML config")?; + + let config_display_name = cfg.config_name.clone().unwrap_or_else(|| file_name.clone()); + let core_config = convert_config(cfg)?; + let sub_input = core_config.input.clone(); + + state.record_log("创建异步任务组"); + let (task_group, task_group_guard) = state + .task_group_manager + .create_task() + .context("Create task failed")?; + + state.record_log("创建组网管理器"); + + let state_clone = state.clone(); + tokio::spawn(async move { + let result = start_vnt_network( + state_clone.clone(), + file_name, + config_display_name, + core_config, + sub_input, + task_group, + task_group_guard, + ) + .await; + + if let Err(e) = result { + log::error!("Failed to start VNT network: {:?}", e); + state_clone.record_log_and_stopped(format!("启动失败: {}", e)); + } + drop(on_error_guard); + }); + + Ok(()) +} + +/// 执行实际的网络启动操作 +async fn start_vnt_network( + state: HttpAppState, + file_name: String, + config_display_name: String, + core_config: CoreConfig, + sub_input: Vec, + task_group: vnt_core::utils::task_control::TaskGroup, + task_group_guard: vnt_core::utils::task_control::TaskGroupGuard, +) -> anyhow::Result<()> { + let mut network_manager = NetworkManager::create_network(Box::new(core_config), task_group.clone()) + .await + .map_err(|e| anyhow!("Create network failed: {:?}", e))?; + + let vnt_api = network_manager.vnt_api(); + + { + let mut lock = state.inner.lock(); + if lock.vnt.is_some() { + return Err(anyhow!("VNT is already running")); + } + lock.vnt = Some(VntHandler { + api: vnt_api, + config_name: config_display_name, + config_file_name: file_name.clone(), + }); + } + + let state_for_vnt_cleanup = state.clone(); + let vnt_cleanup_guard = defer(move || { + state_for_vnt_cleanup.stopped(); + }); + + state.record_log("连接服务器,执行注册"); + log::info!("Registering with server"); + + let reg_msg = network_manager + .register() + .await + .context("Registration failed")?; + + state.record_log(format!("注册成功 {}/{}", reg_msg.ip, reg_msg.prefix_len)); + log::info!("Network Started: {}/{}", reg_msg.ip, reg_msg.prefix_len); + if !network_manager.is_no_tun() { + state.record_log("正在创建 TUN 虚拟网卡"); + network_manager.start_tun().await?; + + state.record_log("创建 TUN 虚拟网卡成功,设置 IP"); + network_manager + .set_network_ip(reg_msg.ip, reg_msg.prefix_len) + .await?; + state.record_log("设置 IP 成功"); + + // 配置子网路由 + if !sub_input.is_empty() + && let Ok(if_index) = network_manager.tun_if_index().await + && let Ok(mut route_manager) = route_manager::RouteManager::new() + { + state.record_log("配置子网路由"); + for input in &sub_input { + let route = + route_manager::Route::new(input.net.network().into(), input.net.prefix_len()) + .with_gateway(input.target_ip.into()) + .with_if_index(if_index); + + if let Err(e) = route_manager.add(&route) { + log::error!("add route [{route}] error: {e:?}"); + } else { + log::info!("add route [{route}] successful"); + } + } + } + } + + state.starting_to_running(); + + // 启动网络管理任务 + task_group.spawn(async move { + network_manager.wait_all_stopped().await; + drop(task_group_guard); + drop(network_manager); + drop(vnt_cleanup_guard); + log::info!("Network manager stopped."); + }); + + // 记录当前配置 + if let Err(e) = fs::write(CURRENT_CONFIG_RECORD, &file_name).await { + log::warn!("Failed to record current config: {}", e); + } + Ok(()) +} + +fn is_valid_file_name(file_name: &str) -> bool { + !file_name.is_empty() + && !file_name.contains("..") + && !file_name.contains('/') + && !file_name.contains('\\') +} + +async fn start_vnt_handler( + State(state): State, + Json(req): Json, +) -> Json> { + if !is_valid_file_name(&req.file_name) { + return Json(ApiResponse::error("Invalid file name")); + } + + let path = Path::new(CONFIG_DIR).join(&req.file_name); + if !path.exists() { + return Json(ApiResponse::error("Config file not found")); + } + + match start_vnt_internal(&state, req.file_name, path).await { + Ok(_) => Json(ApiResponse::success(())), + Err(e) => Json(ApiResponse::error(format!("Start failed: {:?}", e))), + } +} + +async fn stop_vnt_handler(State(state): State) -> Json> { + if state.status() == VntStatus::Stopped { + return Json(ApiResponse::error("Vnt stopped")); + } + state.task_group_manager.stop(); + + let _ = fs::write(CURRENT_CONFIG_RECORD, "").await; + Json(ApiResponse::success(())) +} + +async fn restart_vnt_handler( + State(state): State, + Json(req): Json, +) -> Json> { + if !is_valid_file_name(&req.file_name) { + return Json(ApiResponse::error("Invalid file name")); + } + + let path = Path::new(CONFIG_DIR).join(&req.file_name); + if !path.exists() { + return Json(ApiResponse::error("Config file not found")); + } + + // 先停止(如果正在运行则停止,否则忽略) + if state.status() != VntStatus::Stopped { + state.task_group_manager.stop(); + // 等待停止完成 + for _ in 0..50 { + if state.status() == VntStatus::Stopped { + break; + } + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + } + + // 再启动 + match start_vnt_internal(&state, req.file_name, path).await { + Ok(_) => Json(ApiResponse::success(())), + Err(e) => Json(ApiResponse::error(format!("Restart failed: {:?}", e))), + } +} + +async fn get_info(State(state): State) -> Json> { + let lock = state.inner.lock(); + let status = lock.status; + + let info = if let Some(handler) = lock.vnt.as_ref() { + let api = &handler.api; + let config = api.get_config(); + let ips = api.client_ips(); + let server_node_list = api.server_node_list(); + let nat_info = api.nat_info(); + let network = api.network(); + + HttpAppInfo { + name: config + .as_ref() + .map(|v| v.device_name.clone()) + .unwrap_or_default(), + version: env!("CARGO_PKG_VERSION").to_string(), + ip: network.map(|v| v.ip), + prefix_len: network.map(|v| v.prefix_len), + gateway: network.map(|v| v.gateway), + device_id: config + .as_ref() + .map(|v| v.device_id.clone()) + .unwrap_or_default(), + status, + current_config_name: Some(handler.config_name.clone()), + current_config_file: Some(handler.config_file_name.clone()), + online_client_num: ips.iter().filter(|v| v.online).count(), + offline_client_num: ips.iter().filter(|v| !v.online).count(), + direct_client_num: ips.iter().filter(|ip| api.is_direct(&ip.ip)).count(), + server_info: server_node_list + .into_iter() + .map(|v| HttpServerInfo { + server: v.server_addr.to_string(), + connected: v.connected, + server_rtt: v.rtt, + server_version: v.server_version, + }) + .collect(), + nat_type: nat_info.as_ref().map(|v| format!("{:?}", v.nat_type)), + public_ipv4s: nat_info + .as_ref() + .map(|v| v.public_ips.clone()) + .unwrap_or_default(), + public_ipv6: nat_info.as_ref().and_then(|v| v.ipv6), + network_code: config.as_ref().map(|v| v.network_code.clone()), + mtu: config.as_ref().map(|v| v.mtu.unwrap_or(DEFAULT_MTU)), + fec: config.as_ref().map(|v| v.fec), + compress: config.as_ref().map(|v| v.compress), + encrypt: config.as_ref().map(|v| v.password.is_some()), + rtx: config.as_ref().map(|v| v.rtx), + } + } else { + HttpAppInfo { + version: env!("CARGO_PKG_VERSION").to_string(), + status, + ..Default::default() + } + }; + + Json(ApiResponse::success(info)) +} + +async fn list_configs() -> Json>> { + let mut result = Vec::new(); + + let Ok(mut entries) = fs::read_dir(CONFIG_DIR).await else { + return Json(ApiResponse::success(result)); + }; + + while let Ok(Some(entry)) = entries.next_entry().await { + let path = entry.path(); + + if path.extension().is_none_or(|ext| ext != "toml") { + continue; + } + + let Ok(content) = fs::read_to_string(&path).await else { + continue; + }; + + match toml::from_str::(&content) { + Ok(cfg) => { + let file_name = path + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or("") + .to_string(); + + result.push(ConfigSummary { + file_name, + config_name: cfg + .config_name + .unwrap_or_else(|| entry.file_name().to_string_lossy().to_string()), + }); + } + Err(e) => { + log::warn!("Failed to parse configuration file {:?}: {:?}", path, e); + } + } + } + + result.sort_by(|a, b| b.file_name.cmp(&a.file_name)); + Json(ApiResponse::success(result)) +} + +async fn save_config(Json(req): Json) -> Json> { + // 验证配置格式 + if let Err(e) = toml::from_str::(&req.config) { + log::warn!("Failed to parse configuration: {:?}", e); + return Json(ApiResponse::error(format!("Invalid TOML format: {}", e))); + } + + let file_name = req + .file_name + .filter(|name| !name.is_empty()) + .unwrap_or_else(|| { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(); + format!("{}.toml", now) + }); + + if !is_valid_file_name(&file_name) { + return Json(ApiResponse::error("Invalid file name")); + } + + let target_path = Path::new(CONFIG_DIR).join(&file_name); + + match fs::write(&target_path, &req.config).await { + Ok(_) => Json(ApiResponse::success(())), + Err(e) => Json(ApiResponse::error(format!("Write config failed: {}", e))), + } +} + +async fn get_config(Query(req): Query) -> Json> { + if !is_valid_file_name(&req.file_name) { + return Json(ApiResponse::error("Invalid file name")); + } + + let path = Path::new(CONFIG_DIR).join(&req.file_name); + + if !path.exists() { + return Json(ApiResponse::error("Config file not found")); + } + + match fs::read_to_string(&path).await { + Ok(content) => Json(ApiResponse::success(content)), + Err(e) => Json(ApiResponse::error(format!("Read file failed: {}", e))), + } +} + +async fn delete_config( + State(state): State, + Query(req): Query, +) -> Json> { + if !is_valid_file_name(&req.file_name) { + return Json(ApiResponse::error("Invalid file name")); + } + { + if let Some(vnt) = &state.inner.lock().vnt + && vnt.config_file_name == req.file_name + { + return Json(ApiResponse::error("此配置已被使用,不能删除")); + } + } + + let path = Path::new(CONFIG_DIR).join(&req.file_name); + + if !path.exists() { + return Json(ApiResponse::error("Config file not found")); + } + + match fs::remove_file(&path).await { + Ok(_) => Json(ApiResponse::success(())), + Err(e) => Json(ApiResponse::error(format!("Delete failed: {}", e))), + } +} + +fn convert_config(cfg: StartConfig) -> anyhow::Result { + let server_addrs: Vec = cfg + .server + .iter() + .map(|s| { + s.parse() + .map_err(|e| anyhow!("invalid server address '{}': {}", s, e)) + }) + .collect::>()?; + + let port_mapping: Vec = cfg + .port_mapping + .iter() + .map(|s| { + s.parse() + .map_err(|e| anyhow!("invalid port_mapping '{}': {}", s, e)) + }) + .collect::>()?; + + let cert_mode = match cfg.cert_mode.as_deref() { + Some(s) => s + .parse() + .map_err(|e| anyhow!("invalid cert_mode '{}': {}", s, e))?, + None => CertValidationMode::InsecureSkipVerification, + }; + + let device_id = match cfg.device_id { + Some(id) => id, + None => vnt_core::utils::device_id::get_device_id() + .map_err(|e| anyhow!("failed to get device_id: {}", e))?, + }; + + let device_name = cfg.device_name.unwrap_or_else(|| { + hostname::get() + .ok() + .and_then(|v| v.into_string().ok()) + .unwrap_or_default() + }); + let mut udp_stun = cfg.udp_stun; + for x in udp_stun.iter_mut() { + if !x.contains(':') { + x.push_str(":3478"); + } + } + let mut tcp_stun = cfg.tcp_stun; + for x in tcp_stun.iter_mut() { + if !x.contains(':') { + x.push_str(":3478"); + } + } + Ok(CoreConfig { + server_addr: server_addrs, + network_code: cfg.network_code, + ip: cfg.ip, + no_punch: cfg.no_punch, + rtx: cfg.rtx, + compress: cfg.compress, + device_id, + device_name, + tun_name: cfg.tun_name, + password: cfg.password, + cert_mode, + input: cfg.input, + output: cfg.output, + no_nat: cfg.no_nat, + no_tun: cfg.no_tun, + mtu: cfg.mtu, + port_mapping, + allow_port_mapping: cfg.allow_mapping, + udp_stun, + tcp_stun, + fec: cfg.fec, + }) +} + +async fn shutdown_signal() { + let ctrl_c = async { + tokio::signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } +} + +async fn get_peers(State(state): State) -> Json>> { + let api = state.inner.lock().vnt.as_ref().map(|v| v.api.clone()); + + let Some(api) = api else { + return Json(ApiResponse::error("VNT not running")); + }; + + let key_sign = api.get_config().and_then(|config| config.key_sign()); + + let calc_key_equal = |peer_key_sign: &Option| -> i32 { + match (&key_sign, peer_key_sign) { + (None, None) => 2, + (Some(k1), Some(k2)) if k1 == k2 => 1, + (Some(_), Some(_)) => 5, + (Some(_), None) => 3, + (None, Some(_)) => 4, + } + }; + + let build_nat_info = |ip: &Ipv4Addr| -> Option { + api.peer_nat_info(ip).map(|v| HttpClientNatInfo { + nat_type: format!("{:?}", v.nat_type), + public_ips: v.public_ips, + ipv6: v.ipv6, + }) + }; + + let build_packet_loss = |ip: &Ipv4Addr| -> Option { + api.packet_loss_info(ip).map(|v| HttpPacketLoss { + sent: v.sent, + received: v.received, + loss_rate: v.loss_rate, + }) + }; + + let build_traffic = |ip: &Ipv4Addr| -> Option { + api.traffic_info(ip).map(|v| HttpTraffic { + tx_bytes: v.tx_bytes, + rx_bytes: v.rx_bytes, + }) + }; + + let build_route = |ip: &Ipv4Addr| -> Option { + api.find_route(ip).map(|route| HttpRouteDetail { + addr: route.route_key().to_string(), + protocol: route.route_key().protocol().to_string(), + metric: route.metric(), + rtt: route.rtt(), + }) + }; + + // 先从本地获取基础数据 + let mut merged: HashMap = api + .client_ips() + .into_iter() + .map(|v| { + let ip = v.ip; + let route = build_route(&ip); + // 如果有路由,说明设备在线(可以直接通信) + let has_route = route.is_some(); + ( + ip, + HttpClientItem { + ip, + name: None, + online: v.online || has_route, + route, + version: String::new(), + last_connected_time: 0, + key_equal: 0, + nat_info: build_nat_info(&ip), + packet_loss: build_packet_loss(&ip), + traffic: build_traffic(&ip), + }, + ) + }) + .collect(); + + // 从服务器获取更详细的信息 + if let Ok(resp) = api.server_rpc().client_list().await { + for v in resp.list { + let ip = Ipv4Addr::from(v.ip); + let route = build_route(&ip); + // 如果有路由,说明设备在线(可以直接通信) + let has_route = route.is_some(); + merged.insert( + ip, + HttpClientItem { + ip, + name: Some(v.name), + online: v.online || has_route, + route, + version: v.version, + last_connected_time: v.last_connected_time, + key_equal: calc_key_equal(&v.key_sign), + nat_info: build_nat_info(&ip), + packet_loss: build_packet_loss(&ip), + traffic: build_traffic(&ip), + }, + ); + } + } else { + log::warn!("Failed to get client list from server"); + } + + let mut items: Vec = merged.into_values().collect(); + items.sort_by_key(|it| it.ip); + + Json(ApiResponse::success(items)) +} + +async fn get_routes(State(state): State) -> Json>> { + let lock = state.inner.lock(); + + let Some(handler) = lock.vnt.as_ref() else { + return Json(ApiResponse::error("VNT not running")); + }; + + let table = handler.api.route_table(); + let items: Vec = table + .into_iter() + .map(|(ip, route_list)| HttpRouteItem { + ip, + routes: route_list + .into_iter() + .map(|v| HttpRouteDetail { + addr: v.route_key().to_string(), + protocol: v.route_key().protocol().to_string(), + metric: v.metric(), + rtt: v.rtt(), + }) + .collect(), + }) + .collect(); + + Json(ApiResponse::success(items)) +} diff --git a/vnt-web/static/index.html b/vnt-web/static/index.html new file mode 100644 index 0000000..444b568 --- /dev/null +++ b/vnt-web/static/index.html @@ -0,0 +1,2884 @@ + + + + + + VNT Dashboard + + + + + + + + +

+ + + + + + + + + + + + + + + + + + diff --git a/vnt-web/static/tailwindcss3.4.17.js.gz b/vnt-web/static/tailwindcss3.4.17.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..774ef19678409b063ac9c08272fba30453e635a8 GIT binary patch literal 123080 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 literal 0 HcmV?d00001 diff --git a/vnt-web/static/vue-router.global.prod.js.gz b/vnt-web/static/vue-router.global.prod.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..0df3f3ce62316ebe8e0bf45023a6845335aec822 GIT binary patch literal 9597 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 literal 0 HcmV?d00001 diff --git a/vnt-web/static/vue.global.prod.js.gz b/vnt-web/static/vue.global.prod.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..cab7f19212bc17c82826a426a169a568a22aede5 GIT binary patch literal 58885 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 literal 0 HcmV?d00001 diff --git a/vnt/Cargo.toml b/vnt/Cargo.toml deleted file mode 100644 index 44d0ba1..0000000 --- a/vnt/Cargo.toml +++ /dev/null @@ -1,89 +0,0 @@ -[package] -name = "vnt" -version = "1.2.16" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -tun-rs = { version = "2.5.0", optional = true, features = ["interruptible"] } -packet = { path = "./packet" } -bytes = "1.5.0" -log = "0.4.17" -libc = "0.2.137" -crossbeam-utils = "0.8" -crossbeam-epoch = "0.9.15" -parking_lot = "0.12.1" -rand = "0.8.5" -sha2 = { version = "0.10.6", features = ["oid"] } -thiserror = "1.0.37" -protobuf = "=3.2.0" -socket2 = { version = "0.5.7", features = ["all"] } -aes-gcm = { version = "0.10.2", optional = true } -ring = { version = "0.17.0", optional = true } -cbc = { version = "0.1.2", optional = true } -ecb = { version = "0.1.2", optional = true } -chacha20poly1305 = { version = "0.10.1", optional = true } -chacha20 = { version = "0.9.1", optional = true } -aes = "0.8.3" -stun-format = { version = "1.0.1", features = ["fmt", "rfc3489"] } -rsa = { version = "0.9.2", features = [], optional = true } -spki = { version = "0.7.2", features = ["fingerprint", "alloc", "base64"], optional = true } -openssl-sys = { git = "https://github.com/vnt-dev/rust-openssl", optional = true } -libsm = { git = "https://github.com/vnt-dev/libsm", optional = true } -http_req = { git = "https://github.com/lmq8267/http_req.git", default-features = false, features = ["rust-tls"] } - -mio = { version = "=0.8.11", features = ["os-poll", "net", "os-ext"] } -crossbeam-queue = "0.3.11" -anyhow = "1.0.82" -dns-parser = "0.8.0" - -tokio = { version = "1.46.1", features = ["full"] } - -lz4_flex = { version = "0.11", default-features = false, optional = true } -zstd = { version = "0.13.1", optional = true } - -fnv = "1.0.7" -igd = { version = "0.12.1", optional = true } -tokio-tungstenite = { version = "0.23.1", optional = true } -rustls = { version = "0.23.0", features = ["ring", "tls12"], default-features = false, optional = true } - -network-interface = "2.0.0" - -futures-util = "0.3.30" -[target.'cfg(target_os = "windows")'.dependencies] -winreg = "0.55.0" -libloading = "0.8.0" -windows-sys = { version = "0.60.2", features = ["Win32_Foundation", - "Win32_NetworkManagement", - "Win32_NetworkManagement_IpHelper", - "Win32_Networking_WinSock", - "Win32_System_IO", - "Win32_System_Threading", - "Win32_System_WindowsProgramming", ] } - -[build-dependencies] -protobuf-codegen = "=3.2.0" -protoc-bin-vendored = "3.0.0" -cfg_aliases = "0.2.1" - -[features] -default = ["server_encrypt", "aes_gcm", "aes_cbc", "aes_ecb", "sm4_cbc", "chacha20_poly1305", "ip_proxy", "port_mapping", "lz4_compress", "zstd_compress", "integrated_tun"] -openssl = ["openssl-sys"] -# 从源码编译 -openssl-vendored = ["openssl-sys/vendored"] -ring-cipher = ["ring"] -aes_cbc = ["cbc"] -aes_ecb = ["ecb"] -sm4_cbc = ["libsm"] -aes_gcm = ["aes-gcm"] -chacha20_poly1305 = ["chacha20poly1305", "chacha20"] -server_encrypt = ["aes-gcm", "rsa", "spki"] -ip_proxy = [] -port_mapping = [] -lz4_compress = ["lz4_flex"] -zstd_compress = ["zstd"] -integrated_tun = ["tun-rs"] -upnp = ["igd"] -ws = ["tokio-tungstenite"] -wss = ["ws", "tokio-tungstenite/rustls-tls-native-roots", "tokio-tungstenite/rustls-tls-webpki-roots", "rustls"] diff --git a/vnt/LICENSE b/vnt/LICENSE deleted file mode 100644 index 261eeb9..0000000 --- a/vnt/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/vnt/build.rs b/vnt/build.rs deleted file mode 100644 index 5dd2e36..0000000 --- a/vnt/build.rs +++ /dev/null @@ -1,27 +0,0 @@ -use cfg_aliases::cfg_aliases; - -fn main() { - cfg_aliases! { - cipher: { - any(feature = "aes_gcm", - feature = "chacha20_poly1305", - feature = "server_encrypt", - feature = "aes_cbc", - feature = "aes_ecb", - feature = "sm4_cbc" - )}, - } - - std::fs::create_dir_all("src/proto").unwrap(); - protobuf_codegen::Codegen::new() - .pure() - .out_dir("src/proto") - .inputs(&["proto/message.proto"]) - .include("proto") - // .customize( - // protobuf_codegen::Customize::default() - // .tokio_bytes(true) - // ) - .run() - .expect("Codegen failed."); -} diff --git a/vnt/packet/Cargo.toml b/vnt/packet/Cargo.toml deleted file mode 100644 index e1a6166..0000000 --- a/vnt/packet/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "packet" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -byteorder = "1.4.3" diff --git a/vnt/packet/src/arp/arp.rs b/vnt/packet/src/arp/arp.rs deleted file mode 100644 index ea027a3..0000000 --- a/vnt/packet/src/arp/arp.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::{fmt, io}; - -/// 地址解析协议,由IP地址找到MAC地址 -/// https://www.ietf.org/rfc/rfc6747.txt -/* - 0 2 4 5 6 8 10 (字节) - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | 硬件类型|协议类型|硬件地址长度|协议地址长度|操作类型| - | 源MAC地址 | 源ip地址 | - | 目的MAC地址 | 目的ip地址 | -*/ - -pub struct ArpPacket { - buffer: B, -} - -impl> ArpPacket { - pub fn unchecked(buffer: B) -> Self { - Self { buffer } - } - pub fn new(buffer: B) -> io::Result { - if buffer.as_ref().len() != 28 { - Err(io::Error::from(io::ErrorKind::InvalidData))?; - } - let packet = Self::unchecked(buffer); - Ok(packet) - } -} - -impl> ArpPacket { - /// 硬件类型 以太网类型为1 - pub fn hardware_type(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[0..2].try_into().unwrap()) - } - /// 上层协议类型,ipv4是0x0800 - pub fn protocol_type(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[2..4].try_into().unwrap()) - } - /// 如果是MAC地址 则长度为6 - pub fn hardware_size(&self) -> u8 { - self.buffer.as_ref()[4] - } - /// 如果是IPv4 则长度为4 - pub fn protocol_size(&self) -> u8 { - self.buffer.as_ref()[5] - } - /// 操作类型,请求和响应 1:ARP请求,2:ARP响应,3:RARP请求,4:RARP响应 - pub fn op_code(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[6..8].try_into().unwrap()) - } - /// 发送端硬件地址,仅支持以太网 - pub fn sender_hardware_addr(&self) -> &[u8] { - &self.buffer.as_ref()[8..14] - } - /// 发送端协议地址,仅支持IPv4 - pub fn sender_protocol_addr(&self) -> &[u8] { - &self.buffer.as_ref()[14..18] - } - /// 接收端硬件地址,仅支持以太网 - pub fn target_hardware_addr(&self) -> &[u8] { - &self.buffer.as_ref()[18..24] - } - /// 接收端协议地址,仅支持IPv4 - pub fn target_protocol_addr(&self) -> &[u8] { - &self.buffer.as_ref()[24..28] - } -} - -impl + AsMut<[u8]>> ArpPacket { - /// 硬件类型 以太网类型为1 - pub fn set_hardware_type(&mut self, value: u16) { - self.buffer.as_mut()[0..2].copy_from_slice(&value.to_be_bytes()) - } - /// 上层协议类型,ipv4是0x0800 - pub fn set_protocol_type(&mut self, value: u16) { - self.buffer.as_mut()[2..4].copy_from_slice(&value.to_be_bytes()) - } - /// 如果是MAC地址 则长度为6 - pub fn set_hardware_size(&mut self, value: u8) { - self.buffer.as_mut()[4] = value - } - /// 如果是IPv4 则长度为4 - pub fn set_protocol_size(&mut self, value: u8) { - self.buffer.as_mut()[5] = value - } - /// 操作类型,请求和响应 1:ARP请求,2:ARP响应,3:RARP请求,4:RARP响应 - pub fn set_op_code(&mut self, value: u16) { - self.buffer.as_mut()[6..8].copy_from_slice(&value.to_be_bytes()) - } - /// 发送端硬件地址,仅支持以太网 - pub fn set_sender_hardware_addr(&mut self, buf: &[u8]) { - self.buffer.as_mut()[8..14].copy_from_slice(buf) - } - /// 发送端协议地址,仅支持IPv4 - pub fn set_sender_protocol_addr(&mut self, buf: &[u8]) { - self.buffer.as_mut()[14..18].copy_from_slice(buf) - } - /// 接收端硬件地址,仅支持以太网 - pub fn set_target_hardware_addr(&mut self, buf: &[u8]) { - self.buffer.as_mut()[18..24].copy_from_slice(buf) - } - /// 接收端协议地址,仅支持IPv4 - pub fn set_target_protocol_addr(&mut self, buf: &[u8]) { - self.buffer.as_mut()[24..28].copy_from_slice(buf) - } -} - -impl> fmt::Debug for ArpPacket { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("ArpPacket") - .field("hardware_type", &self.hardware_type()) - .field("protocol_type", &self.protocol_type()) - .field("hardware_size", &self.hardware_size()) - .field("protocol_size", &self.protocol_size()) - .field("op_code", &self.op_code()) - .field("sender_hardware_addr", &self.sender_hardware_addr()) - .field("sender_protocol_addr", &self.sender_protocol_addr()) - .field("target_hardware_addr", &self.target_hardware_addr()) - .field("target_protocol_addr", &self.target_protocol_addr()) - .finish() - } -} diff --git a/vnt/packet/src/arp/mod.rs b/vnt/packet/src/arp/mod.rs deleted file mode 100644 index 6a5d36a..0000000 --- a/vnt/packet/src/arp/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod arp; diff --git a/vnt/packet/src/ethernet/mod.rs b/vnt/packet/src/ethernet/mod.rs deleted file mode 100644 index c9cf115..0000000 --- a/vnt/packet/src/ethernet/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod packet; -pub mod protocol; diff --git a/vnt/packet/src/ethernet/packet.rs b/vnt/packet/src/ethernet/packet.rs deleted file mode 100644 index f54619e..0000000 --- a/vnt/packet/src/ethernet/packet.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::ethernet::protocol::Protocol; -use std::{fmt, io}; - -/// 以太网帧协议 -/// https://www.ietf.org/rfc/rfc894.txt -/* - 0 6 12 14 (字节) - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | 目的地址 | 源地址 | 类型 | -*/ -pub struct EthernetPacket { - pub buffer: B, -} - -impl> EthernetPacket { - pub fn unchecked(buffer: B) -> EthernetPacket { - EthernetPacket { buffer } - } - - pub fn new(buffer: B) -> io::Result> { - let packet = EthernetPacket::unchecked(buffer); - //头部固定14位 - if packet.buffer.as_ref().len() < 14 { - Err(io::Error::from(io::ErrorKind::InvalidData))?; - } - - Ok(packet) - } -} - -impl> EthernetPacket { - /// 目的MAC地址 - pub fn destination(&self) -> &[u8] { - &self.buffer.as_ref()[0..6] - } - /// 源MAC地址 - pub fn source(&self) -> &[u8] { - &self.buffer.as_ref()[6..12] - } - /// 3层协议 - pub fn protocol(&self) -> Protocol { - u16::from_be_bytes(self.buffer.as_ref()[12..14].try_into().unwrap()).into() - } - /// 载荷 - pub fn payload(&self) -> &[u8] { - &self.buffer.as_ref()[14..] - } -} - -impl + AsMut<[u8]>> EthernetPacket { - pub fn set_destination(&mut self, value: &[u8]) { - self.buffer.as_mut()[0..6].copy_from_slice(value); - } - - pub fn set_source(&mut self, value: &[u8]) { - self.buffer.as_mut()[6..12].copy_from_slice(value); - } - - pub fn set_protocol(&mut self, value: Protocol) { - let p: u16 = value.into(); - self.buffer.as_mut()[12..14].copy_from_slice(&p.to_be_bytes()) - } - pub fn payload_mut(&mut self) -> &mut [u8] { - &mut self.buffer.as_mut()[14..] - } -} - -impl> fmt::Debug for EthernetPacket { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("EthernetPacket") - .field("destination", &self.destination()) - .field("source", &self.source()) - .field("protocol", &self.protocol()) - .field("payload", &self.payload()) - .finish() - } -} diff --git a/vnt/packet/src/ethernet/protocol.rs b/vnt/packet/src/ethernet/protocol.rs deleted file mode 100644 index e775a28..0000000 --- a/vnt/packet/src/ethernet/protocol.rs +++ /dev/null @@ -1,141 +0,0 @@ -/// 以太网帧协议 -#[derive(Eq, PartialEq, Copy, Clone, Debug)] -pub enum Protocol { - /// - Ipv4, - - /// - Arp, - - /// - WakeOnLan, - - /// - Trill, - - /// - DecNet, - - /// - Rarp, - - /// - AppleTalk, - - /// - Aarp, - - /// - Ipx, - - /// - Qnx, - - /// - Ipv6, - - /// - FlowControl, - - /// - CobraNet, - - /// - Mpls, - - /// - MplsMulticast, - - /// - PppoeDiscovery, - - /// - PppoeSession, - - /// - Vlan, - - /// - PBridge, - - /// - Lldp, - - /// - Ptp, - - /// - Cfm, - - /// - QinQ, - - /// - Unknown(u16), -} - -impl From for Protocol { - fn from(value: u16) -> Protocol { - use self::Protocol::*; - - match value { - 0x0800 => Ipv4, - 0x0806 => Arp, - 0x0842 => WakeOnLan, - 0x22f3 => Trill, - 0x6003 => DecNet, - 0x8035 => Rarp, - 0x809b => AppleTalk, - 0x80f3 => Aarp, - 0x8137 => Ipx, - 0x8204 => Qnx, - 0x86dd => Ipv6, - 0x8808 => FlowControl, - 0x8819 => CobraNet, - 0x8847 => Mpls, - 0x8848 => MplsMulticast, - 0x8863 => PppoeDiscovery, - 0x8864 => PppoeSession, - 0x8100 => Vlan, - 0x88a8 => PBridge, - 0x88cc => Lldp, - 0x88f7 => Ptp, - 0x8902 => Cfm, - 0x9100 => QinQ, - n => Unknown(n), - } - } -} - -impl Into for Protocol { - fn into(self) -> u16 { - use self::Protocol::*; - - match self { - Ipv4 => 0x0800, - Arp => 0x0806, - WakeOnLan => 0x0842, - Trill => 0x22f3, - DecNet => 0x6003, - Rarp => 0x8035, - AppleTalk => 0x809b, - Aarp => 0x80f3, - Ipx => 0x8137, - Qnx => 0x8204, - Ipv6 => 0x86dd, - FlowControl => 0x8808, - CobraNet => 0x8819, - Mpls => 0x8847, - MplsMulticast => 0x8848, - PppoeDiscovery => 0x8863, - PppoeSession => 0x8864, - Vlan => 0x8100, - PBridge => 0x88a8, - Lldp => 0x88cc, - Ptp => 0x88f7, - Cfm => 0x8902, - QinQ => 0x9100, - Unknown(n) => n, - } - } -} diff --git a/vnt/packet/src/icmp/icmp.rs b/vnt/packet/src/icmp/icmp.rs deleted file mode 100644 index 4ae4ab5..0000000 --- a/vnt/packet/src/icmp/icmp.rs +++ /dev/null @@ -1,167 +0,0 @@ -use crate::cal_checksum; -use crate::icmp::{Code, Kind}; -use crate::ip::ipv4::packet::IpV4Packet; -use byteorder::{BigEndian, ReadBytesExt}; -use std::{fmt, io}; - -/// icmp 协议 -/* https://www.rfc-editor.org/rfc/rfc792 - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type | Code | Checksum | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | 不同Type和Code有不同含义 | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | 数据体 不同Type和Code有不同含义 | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -*/ - -pub struct IcmpPacket { - pub buffer: B, -} - -impl> IcmpPacket { - pub fn unchecked(buffer: B) -> Self { - Self { buffer } - } - pub fn new(buffer: B) -> io::Result { - if buffer.as_ref().len() < 8 { - Err(io::Error::from(io::ErrorKind::InvalidData))?; - } - let packet = Self::unchecked(buffer); - Ok(packet) - } -} - -impl + AsMut<[u8]>> IcmpPacket { - pub fn set_kind(&mut self, kind: Kind) { - self.buffer.as_mut()[0] = kind.into(); - } - pub fn update_checksum(&mut self) { - self.buffer.as_mut()[2..4].copy_from_slice(&[0, 0]); - let checksum = cal_checksum(self.buffer.as_ref()); - self.buffer.as_mut()[2..4].copy_from_slice(&checksum.to_be_bytes()); - } -} - -impl> IcmpPacket { - pub fn kind(&self) -> Kind { - Kind::from(self.buffer.as_ref()[0]) - } - pub fn code(&self) -> Code { - Code::from(self.kind(), self.buffer.as_ref()[1]) - } - pub fn checksum(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[2..4].try_into().unwrap()) - } - pub fn is_valid(&self) -> bool { - self.checksum() == 0 || cal_checksum(self.buffer.as_ref()) == 0 - } - pub fn header_other(&self) -> HeaderOther { - match self.kind() { - Kind::EchoReply - | Kind::EchoRequest - | Kind::TimestampRequest - | Kind::TimestampReply - | Kind::InformationRequest - | Kind::InformationReply => { - let ide = u16::from_be_bytes(self.buffer.as_ref()[4..6].try_into().unwrap()); - let seq = u16::from_be_bytes(self.buffer.as_ref()[6..8].try_into().unwrap()); - HeaderOther::Identifier(ide, seq) - } - Kind::DestinationUnreachable | Kind::TimeExceeded | Kind::SourceQuench => { - let bytes = self.buffer.as_ref(); - HeaderOther::Unused(bytes[4], bytes[5], bytes[6], bytes[7]) - } - Kind::Redirect => { - let bytes = self.buffer.as_ref(); - HeaderOther::Address(bytes[4], bytes[5], bytes[6], bytes[7]) - } - Kind::ParameterProblem => HeaderOther::Pointer(self.buffer.as_ref()[4]), - _ => { - let bytes = self.buffer.as_ref(); - HeaderOther::UnKnown(bytes[4], bytes[5], bytes[6], bytes[7]) - } - } - } - pub fn payload(&self) -> &[u8] { - &self.buffer.as_ref()[8..] - } - pub fn description(&self) -> Description<&[u8]> { - use std::io::Cursor; - match self.kind() { - Kind::DestinationUnreachable - | Kind::TimeExceeded - | Kind::ParameterProblem - | Kind::SourceQuench - | Kind::Redirect => match IpV4Packet::new(self.payload()) { - Ok(d) => Description::Ip(d), - Err(_) => Description::Other(self.payload()), - }, - Kind::TimestampRequest | Kind::TimestampReply => { - let mut buffer = Cursor::new(self.payload()); - - Description::Timestamp( - buffer.read_u32::().unwrap(), - buffer.read_u32::().unwrap(), - buffer.read_u32::().unwrap(), - ) - } - _ => Description::Other(self.payload()), - } - } -} - -impl> fmt::Debug for IcmpPacket { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct(if self.is_valid() { - "icmp::Packet" - } else { - "icmp::Packet!" - }) - .field("kind", &self.kind()) - .field("code", &self.code()) - .field("checksum", &self.checksum()) - .field("payload", &self.payload()) - .finish() - } -} - -#[derive(Debug)] -pub enum HeaderOther { - /// 全零 - Unused(u8, u8, u8, u8), - /// If code = 0, identifies the octet where an error was detected. - Pointer(u8), - /// Address of the gateway to which traffic for the network specified - /// in the internet destination network field of the original - /// datagram's data should be sent. - Address(u8, u8, u8, u8), - /// Identifier | Sequence Number - Identifier(u16, u16), - UnKnown(u8, u8, u8, u8), -} - -pub enum Description { - Ip(IpV4Packet), - ///时间戳 Originate Timestamp,Receive Timestamp,Transmit Timestamp - Timestamp(u32, u32, u32), - Other(B), -} - -impl + std::fmt::Debug> fmt::Debug for Description { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Description::Ip(packet) => f.debug_struct(&format!("{:?}", packet)).finish(), - Description::Timestamp(originate, receive, transmit) => f - .debug_struct("") - .field("originate", originate) - .field("receive", receive) - .field("transmit", transmit) - .finish(), - Description::Other(bytes) => f.debug_struct(&format!("{:?}", bytes)).finish(), - } - } -} diff --git a/vnt/packet/src/icmp/mod.rs b/vnt/packet/src/icmp/mod.rs deleted file mode 100644 index dc9c214..0000000 --- a/vnt/packet/src/icmp/mod.rs +++ /dev/null @@ -1,414 +0,0 @@ -pub mod icmp; - -#[derive(Eq, PartialEq, Copy, Clone, Debug)] -pub enum Kind { - /// ping应答,type=0 - /* - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type | Code | Checksum | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Identifier | Sequence Number | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Data ... - +-+-+-+-+- - */ - EchoReply, - /// 目的地不可达,差错报文的一种,路由器收到一个不能转发的数据报,会向源地址返回这个报文,type=3 - /* - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type | Code | Checksum | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | unused | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Internet Header + 64 bits of Original Data Datagram | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - DestinationUnreachable, - /// 源抑制报文,用于防止接收端缓存溢出,接收设备发送这个来请求源设备降低发送速度,type=4 - /* - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type | Code | Checksum | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | unused | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Internet Header + 64 bits of Original Data Datagram | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - SourceQuench, - /// 重定向报文,当路由器接收包的接口正好是去往目的地的出口时,会向源地址发送重定向报文,告知源直接将数据发往自己的下一跳,type=5 - /* - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type | Code | Checksum | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Gateway Internet Address | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Internet Header + 64 bits of Original Data Datagram | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - Redirect, - /// ping请求,type=8 - EchoRequest, - /// 路由器通告,type=9, - RouterAdvertisement, - /// 路由器请求,type=10 - RouterSolicitation, - /// 报文ttl为0后,路由器会向源发送此报文,type=11 - /// Tracert工作原理: - /// 首先向目的地发送ttl=1的包,下一跳路由器收到后ttl-1,此时ttl=0,将向源发送 ICMP time exceeded - /// 再发送ttl=2的包,以此类推,直到目标主机接收到改包,此时不会回复ICMP time exceeded,代表已经探测到目的地 - /* - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type | Code | Checksum | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | unused | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Internet Header + 64 bits of Original Data Datagram | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - TimeExceeded, - /// 参数错误,数据有误、校验和不对等,type=12 - /* - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type | Code | Checksum | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Pointer | unused | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Internet Header + 64 bits of Original Data Datagram | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - 注:Pointer指示错误的位置 - */ - ParameterProblem, - /// 时间戳请求,type=13 - /* - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type | Code | Checksum | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Identifier | Sequence Number | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Originate Timestamp | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Receive Timestamp | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Transmit Timestamp | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - TimestampRequest, - /// 时间戳响应,type=14 - TimestampReply, - /// 信息请求,type=15 - /* - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type | Code | Checksum | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Identifier | Sequence Number | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - InformationRequest, - /// 信息响应,type=16 - InformationReply, - /// 地址掩码请求,type=17 - AddressMaskRequest, - /// 地址掩码应答,type=18 - AddressMaskReply, - /// - TraceRoute, - /// - Unknown(u8), -} - -impl From for Kind { - fn from(value: u8) -> Kind { - use self::Kind::*; - - match value { - 0 => EchoReply, - 3 => DestinationUnreachable, - 4 => SourceQuench, - 5 => Redirect, - 8 => EchoRequest, - 9 => RouterAdvertisement, - 10 => RouterSolicitation, - 11 => TimeExceeded, - 12 => ParameterProblem, - 13 => TimestampRequest, - 14 => TimestampReply, - 15 => InformationRequest, - 16 => InformationReply, - 17 => AddressMaskRequest, - 18 => AddressMaskReply, - 30 => TraceRoute, - v => Unknown(v), - } - } -} - -impl Into for Kind { - fn into(self) -> u8 { - use self::Kind::*; - match self { - EchoReply => 0, - DestinationUnreachable => 3, - SourceQuench => 4, - Redirect => 5, - EchoRequest => 8, - RouterAdvertisement => 9, - RouterSolicitation => 10, - TimeExceeded => 11, - ParameterProblem => 12, - TimestampRequest => 13, - TimestampReply => 14, - InformationRequest => 15, - InformationReply => 16, - AddressMaskRequest => 17, - AddressMaskReply => 18, - TraceRoute => 30, - Unknown(v) => v, - } - } -} - -#[derive(Eq, PartialEq, Copy, Clone, Debug)] -pub enum Code { - DestinationUnreachable(DestinationUnreachable), - Redirect(Redirect), - ParameterProblem(ParameterProblem), - Other(u8), -} - -impl Code { - pub fn from(kind: Kind, code: u8) -> Code { - match kind { - Kind::DestinationUnreachable => { - Code::DestinationUnreachable(DestinationUnreachable::from(code)) - } - Kind::Redirect => Code::Redirect(Redirect::from(code)), - Kind::ParameterProblem => Code::ParameterProblem(ParameterProblem::from(code)), - _ => Code::Other(code), - } - } -} - -#[derive(Eq, PartialEq, Copy, Clone, Debug)] -pub enum DestinationUnreachable { - /// 网络不可达 - DestinationNetworkUnreachable, - /// 主机不可达 - DestinationHostUnreachable, - /// 协议不可达 - DestinationProtocolUnreachable, - /// 端口不可达 - DestinationPortUnreachable, - /// 需要进行分片但设置不分片比特 - FragmentationRequired, - /// 源站选路失败 - SourceRouteFailed, - /// 目的网络未知 - DestinationNetworkUnknown, - /// 目的主机未知 - DestinationHostUnknown, - /// 源主机被隔离(作废不用) - SourceHostIsolated, - /// 目的网络被强制禁止 - NetworkAdministrativelyProhibited, - /// 目的主机被强制禁止 - HostAdministrativelyProhibited, - /// 由于服务类型TOS,网络不可达 - NetworkUnreachableForTos, - /// 由于服务类型TOS,主机不可达 - HostUnreachableForTos, - /// 由于过滤,通信被强制禁止 - CommunicationAdministrativelyProhibited, - /// 主机越权 - HostPrecedenceViolation, - /// 优先中止生效 - PrecedentCutoffInEffect, - /// - Unknown(u8), -} - -/// Codes for Redirect Message packets. -#[derive(Eq, PartialEq, Copy, Clone, Debug)] -pub enum Redirect { - /// 对网络重定向 - RedirectDatagramForNetwork, - /// 对主机重定向 - RedirectDatagramForHost, - /// 对服务类型和网络重定向 - RedirectDatagramForTosAndNetwork, - /// 对服务类型和主机重定向 - RedirectDatagramForTosAndHost, - /// - Unknown(u8), -} - -/// Codes for TimeExceeded Message packets. -#[derive(Eq, PartialEq, Copy, Clone, Debug)] -pub enum TimeExceeded { - /// TTL超时报文 - Transit, - /// 分片重组超时报文 - Reassembly, - /// - Unknown(u8), -} -/// Codes for Parameter Problem packets. -#[derive(Eq, PartialEq, Copy, Clone, Debug)] -pub enum ParameterProblem { - /// 坏的IP首部(包括各种差错) - PointerIndicatesError, - /// 缺少必需的选项 - MissingRequiredData, - /// 长度错误 - BadLength, - /// - Unknown(u8), -} - -impl From for DestinationUnreachable { - fn from(value: u8) -> Self { - use self::DestinationUnreachable::*; - - match value { - 0 => DestinationNetworkUnreachable, - 1 => DestinationHostUnreachable, - 2 => DestinationProtocolUnreachable, - 3 => DestinationPortUnreachable, - 4 => FragmentationRequired, - 5 => SourceRouteFailed, - 6 => DestinationNetworkUnknown, - 7 => DestinationHostUnknown, - 8 => SourceHostIsolated, - 9 => NetworkAdministrativelyProhibited, - 10 => HostAdministrativelyProhibited, - 11 => NetworkUnreachableForTos, - 12 => HostUnreachableForTos, - 13 => CommunicationAdministrativelyProhibited, - 14 => HostPrecedenceViolation, - 15 => PrecedentCutoffInEffect, - v => Unknown(v), - } - } -} - -impl Into for DestinationUnreachable { - fn into(self) -> u8 { - use self::DestinationUnreachable::*; - - match self { - DestinationNetworkUnreachable => 0, - DestinationHostUnreachable => 1, - DestinationProtocolUnreachable => 2, - DestinationPortUnreachable => 3, - FragmentationRequired => 4, - SourceRouteFailed => 5, - DestinationNetworkUnknown => 6, - DestinationHostUnknown => 7, - SourceHostIsolated => 8, - NetworkAdministrativelyProhibited => 9, - HostAdministrativelyProhibited => 10, - NetworkUnreachableForTos => 11, - HostUnreachableForTos => 12, - CommunicationAdministrativelyProhibited => 13, - HostPrecedenceViolation => 14, - PrecedentCutoffInEffect => 15, - Unknown(v) => v, - } - } -} - -impl From for Redirect { - fn from(value: u8) -> Self { - use self::Redirect::*; - - match value { - 0 => RedirectDatagramForNetwork, - 1 => RedirectDatagramForHost, - 2 => RedirectDatagramForTosAndNetwork, - 3 => RedirectDatagramForTosAndHost, - v => Unknown(v), - } - } -} - -impl Into for Redirect { - fn into(self) -> u8 { - use self::Redirect::*; - - match self { - RedirectDatagramForNetwork => 0, - RedirectDatagramForHost => 1, - RedirectDatagramForTosAndNetwork => 2, - RedirectDatagramForTosAndHost => 3, - Unknown(v) => v, - } - } -} - -impl From for TimeExceeded { - fn from(value: u8) -> Self { - use self::TimeExceeded::*; - - match value { - 0 => Transit, - 1 => Reassembly, - v => Unknown(v), - } - } -} - -impl Into for TimeExceeded { - fn into(self) -> u8 { - use self::TimeExceeded::*; - - match self { - Transit => 0, - Reassembly => 1, - Unknown(v) => v, - } - } -} - -impl From for ParameterProblem { - fn from(value: u8) -> Self { - use self::ParameterProblem::*; - - match value { - 0 => PointerIndicatesError, - 1 => MissingRequiredData, - 2 => BadLength, - v => Unknown(v), - } - } -} - -impl Into for ParameterProblem { - fn into(self) -> u8 { - use self::ParameterProblem::*; - - match self { - PointerIndicatesError => 0, - MissingRequiredData => 1, - BadLength => 2, - Unknown(v) => v, - } - } -} diff --git a/vnt/packet/src/igmp/igmp_v1.rs b/vnt/packet/src/igmp/igmp_v1.rs deleted file mode 100644 index 58617f6..0000000 --- a/vnt/packet/src/igmp/igmp_v1.rs +++ /dev/null @@ -1,117 +0,0 @@ -use crate::cal_checksum; -use std::net::Ipv4Addr; -use std::{fmt, io}; - -/// igmp v1 -/* https://datatracker.ietf.org/doc/html/rfc1112 - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - |Version| Type | Unused | Checksum | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Group Address | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -*/ -/// v1版本的报文 -pub struct IgmpV1Packet { - pub buffer: B, -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum IgmpV1Type { - /// 0x11 所有组224.0.0.1或者特定组 - Query, - /// 0x12 - ReportV1, - Unknown(u8), -} - -impl From for IgmpV1Type { - fn from(value: u8) -> IgmpV1Type { - use self::IgmpV1Type::*; - - match value { - 0x11 => Query, - 0x12 => ReportV1, - v => Unknown(v), - } - } -} - -impl Into for IgmpV1Type { - fn into(self) -> u8 { - match self { - IgmpV1Type::Query => 0x11, - IgmpV1Type::ReportV1 => 0x12, - IgmpV1Type::Unknown(v) => v, - } - } -} - -impl> IgmpV1Packet { - pub fn unchecked(buffer: B) -> Self { - Self { buffer } - } - pub fn new(buffer: B) -> io::Result { - if buffer.as_ref().len() != 8 { - Err(io::Error::from(io::ErrorKind::InvalidData)) - } else { - let packet = Self::unchecked(buffer); - Ok(packet) - } - } -} - -impl> IgmpV1Packet { - pub fn version(&self) -> u8 { - self.buffer.as_ref()[0] >> 4 - } - pub fn igmp_type(&self) -> IgmpV1Type { - IgmpV1Type::from(self.buffer.as_ref()[0] & 0x0F) - } - pub fn unused(&self) -> u8 { - self.buffer.as_ref()[1] - } - pub fn checksum(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[2..4].try_into().unwrap()) - } - pub fn is_valid(&self) -> bool { - self.checksum() == 0 || cal_checksum(self.buffer.as_ref()) == 0 - } - pub fn group_address(&self) -> Ipv4Addr { - let tmp: [u8; 4] = self.buffer.as_ref()[4..8].try_into().unwrap(); - Ipv4Addr::from(tmp) - } -} - -impl + AsMut<[u8]>> IgmpV1Packet { - pub fn set_version(&mut self, version: u8) { - self.buffer.as_mut()[0] = (version << 4) | 0x0F & self.buffer.as_mut()[0] - } - pub fn set_type(&mut self, igmp_type: IgmpV1Type) { - let t: u8 = igmp_type.into(); - self.buffer.as_mut()[0] = self.buffer.as_mut()[0] & 0xF0 | t - } - pub fn set_checksum(&mut self, checksum: u16) { - self.buffer.as_mut()[2..4].copy_from_slice(&checksum.to_be_bytes()); - } - pub fn update_checksum(&mut self) { - self.set_checksum(0); - self.set_checksum(cal_checksum(self.buffer.as_ref())); - } - pub fn set_group_address(&mut self, group_address: Ipv4Addr) { - self.buffer.as_mut()[4..8].copy_from_slice(&group_address.octets()); - } -} - -impl> fmt::Debug for IgmpV1Packet { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("igmp::V1") - .field("version", &self.version()) - .field("type", &self.igmp_type()) - .field("checksum", &self.checksum()) - .field("is_valid", &self.is_valid()) - .field("group_address", &self.group_address()) - .finish() - } -} diff --git a/vnt/packet/src/igmp/igmp_v2.rs b/vnt/packet/src/igmp/igmp_v2.rs deleted file mode 100644 index 13e65cd..0000000 --- a/vnt/packet/src/igmp/igmp_v2.rs +++ /dev/null @@ -1,118 +0,0 @@ -use crate::cal_checksum; -use std::net::Ipv4Addr; -use std::{fmt, io}; - -/// igmp v2 -/* https://www.rfc-editor.org/rfc/rfc2236.html - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type | Max Resp Time | Checksum | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Group Address | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -*/ - -/// v2版本的报文 -pub struct IgmpV2Packet { - pub buffer: B, -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum IgmpV2Type { - /// 0x11 所有组224.0.0.1或者特定组 - Query, - /// 0x16 - ReportV2, - LeaveV2, - Unknown(u8), -} - -impl From for IgmpV2Type { - fn from(value: u8) -> IgmpV2Type { - use self::IgmpV2Type::*; - - match value { - 0x11 => Query, - 0x16 => ReportV2, - 0x17 => LeaveV2, - v => Unknown(v), - } - } -} - -impl Into for IgmpV2Type { - fn into(self) -> u8 { - match self { - IgmpV2Type::Query => 0x11, - IgmpV2Type::ReportV2 => 0x16, - IgmpV2Type::LeaveV2 => 0x17, - IgmpV2Type::Unknown(v) => v, - } - } -} - -impl> IgmpV2Packet { - pub fn unchecked(buffer: B) -> Self { - Self { buffer } - } - pub fn new(buffer: B) -> io::Result { - if buffer.as_ref().len() != 8 { - Err(io::Error::from(io::ErrorKind::InvalidData)) - } else { - let packet = Self::unchecked(buffer); - Ok(packet) - } - } -} - -impl> IgmpV2Packet { - pub fn igmp_type(&self) -> IgmpV2Type { - IgmpV2Type::from(self.buffer.as_ref()[0]) - } - pub fn max_resp_time(&self) -> u8 { - self.buffer.as_ref()[1] - } - pub fn checksum(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[2..4].try_into().unwrap()) - } - pub fn is_valid(&self) -> bool { - self.checksum() == 0 || cal_checksum(self.buffer.as_ref()) == 0 - } - pub fn group_address(&self) -> Ipv4Addr { - let tmp: [u8; 4] = self.buffer.as_ref()[4..8].try_into().unwrap(); - Ipv4Addr::from(tmp) - } -} - -impl + AsMut<[u8]>> IgmpV2Packet { - pub fn set_type(&mut self, igmp_type: IgmpV2Type) { - self.buffer.as_mut()[0] = igmp_type.into() - } - pub fn set_max_resp_time(&mut self, resp: u8) { - self.buffer.as_mut()[1] = resp - } - pub fn set_checksum(&mut self, checksum: u16) { - self.buffer.as_mut()[2..4].copy_from_slice(&checksum.to_be_bytes()); - } - pub fn update_checksum(&mut self) { - self.set_checksum(0); - self.set_checksum(cal_checksum(self.buffer.as_ref())); - } - pub fn set_group_address(&mut self, group_address: Ipv4Addr) { - self.buffer.as_mut()[4..8].copy_from_slice(&group_address.octets()); - } -} - -impl> fmt::Debug for IgmpV2Packet { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("igmp::V2") - .field("type", &self.igmp_type()) - .field("max_resp_time", &self.max_resp_time()) - .field("checksum", &self.checksum()) - .field("is_valid", &self.is_valid()) - .field("group_address", &self.group_address()) - .finish() - } -} diff --git a/vnt/packet/src/igmp/igmp_v3.rs b/vnt/packet/src/igmp/igmp_v3.rs deleted file mode 100644 index 0ec2df9..0000000 --- a/vnt/packet/src/igmp/igmp_v3.rs +++ /dev/null @@ -1,493 +0,0 @@ -use std::net::Ipv4Addr; -use std::{fmt, io}; - -use crate::cal_checksum; - -/// igmp v3 -/* https://www.rfc-editor.org/rfc/rfc3376 -Query: - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type = 0x11 | Max Resp Code | Checksum | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Group Address | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Resv |S| QRV | QQIC | Number of Sources (N) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Source Address [1] | - +- -+ - | Source Address [2] | - +- . -+ - . . . - . . . - +- -+ - | Source Address [N] | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - ------------------------------------------------------------------------------ - -Report: - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type = 0x22 | Reserved | Checksum | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Reserved | Number of Group Records (M) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | | - . . - . Group Record [1] . - . . - | | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | | - . . - . Group Record [2] . - . . - | | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | . | - . . . - | . | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | | - . . - . Group Record [M] . - . . - | | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -Group Record: - - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Record Type | Aux Data Len | Number of Sources (N) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Multicast Address | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Source Address [1] | - +- -+ - | Source Address [2] | - +- -+ - . . . - . . . - . . . - +- -+ - | Source Address [N] | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | | - . . - . Auxiliary Data . - . . - | | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -Record Type: - 1 MODE_IS_INCLUDE 表示主机希望加入指定组播组并指定了一个或多个源地址 - 2 MODE_IS_EXCLUDE 表示主机希望加入指定组播组但排除了一个或多个源地址 - 3 CHANGE_TO_INCLUDE_MODE 表示主机正在将组播组的过滤模式从排除切换为包括,指定了一个或多个源地址 - 4 CHANGE_TO_EXCLUDE_MODE 表示主机正在将组播组的过滤模式从包括切换为排除,指定了一个或多个源地址 - 5 ALLOW_NEW_SOURCES 表示主机希望在已有的源地址列表中添加新的源地址,指定了一个或多个源地址 - 6 BLOCK_OLD_SOURCES 表示主机希望在已有的源地址列表中删除旧的源地址,指定了一个或多个源地址 - */ -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum IgmpV3Type { - /// 0x11 所有组224.0.0.1或者特定组 - Query, - /// 0x22 - ReportV3, - Unknown(u8), -} - -impl From for IgmpV3Type { - fn from(value: u8) -> IgmpV3Type { - use self::IgmpV3Type::*; - - match value { - 0x11 => Query, - 0x22 => ReportV3, - v => Unknown(v), - } - } -} - -impl Into for IgmpV3Type { - fn into(self) -> u8 { - match self { - IgmpV3Type::Query => 0x11, - IgmpV3Type::ReportV3 => 0x22, - IgmpV3Type::Unknown(v) => v, - } - } -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum IgmpV3RecordType { - //1 MODE_IS_INCLUDE 表示主机希望加入指定组播组并指定了一个或多个源地址 - ModeIsInclude, - //2 MODE_IS_EXCLUDE 表示主机希望加入指定组播组但排除了一个或多个源地址 - ModeIsExclude, - //3 CHANGE_TO_INCLUDE_MODE 表示主机正在将组播组的过滤模式从排除切换为包括,指定了一个或多个源地址 - ChangeToIncludeMode, - //4 CHANGE_TO_EXCLUDE_MODE 表示主机正在将组播组的过滤模式从包括切换为排除,指定了一个或多个源地址 - ChangeToExcludeMode, - //5 ALLOW_NEW_SOURCES 表示主机希望在已有的源地址列表中添加新的源地址,指定了一个或多个源地址 - AllowNewSources, - //6 BLOCK_OLD_SOURCES 表示主机希望在已有的源地址列表中删除旧的源地址,指定了一个或多个源地址 - BlockOldSources, - Unknown(u8), -} - -impl From for IgmpV3RecordType { - fn from(value: u8) -> IgmpV3RecordType { - use self::IgmpV3RecordType::*; - - match value { - 1 => ModeIsInclude, - 2 => ModeIsExclude, - 3 => ChangeToIncludeMode, - 4 => ChangeToExcludeMode, - 5 => AllowNewSources, - 6 => BlockOldSources, - v => Unknown(v), - } - } -} - -impl Into for IgmpV3RecordType { - fn into(self) -> u8 { - use self::IgmpV3RecordType::*; - - match self { - ModeIsInclude => 1, - ModeIsExclude => 2, - ChangeToIncludeMode => 3, - ChangeToExcludeMode => 4, - AllowNewSources => 5, - BlockOldSources => 6, - Unknown(v) => v, - } - } -} - -/// v3版本的query报文 -pub struct IgmpV3QueryPacket { - pub buffer: B, -} - -impl> IgmpV3QueryPacket { - pub fn unchecked(buffer: B) -> Self { - Self { buffer } - } - pub fn new(buffer: B) -> io::Result { - if buffer.as_ref().len() < 12 { - Err(io::Error::from(io::ErrorKind::InvalidData)) - } else { - let packet = Self::unchecked(buffer); - Ok(packet) - } - } -} - -impl + AsMut<[u8]>> IgmpV3QueryPacket { - pub fn set_igmp_type(&mut self) { - self.buffer.as_mut()[0] = IgmpV3Type::Query.into(); - } - pub fn set_max_resp_code(&mut self, code: u8) { - self.buffer.as_mut()[1] = code; - } - pub fn set_group_address(&mut self, addr: Ipv4Addr) { - self.buffer.as_mut()[4..8].copy_from_slice(&addr.octets()) - } - pub fn set_checksum(&mut self, checksum: u16) { - self.buffer.as_mut()[2..4].copy_from_slice(&checksum.to_be_bytes()) - } - pub fn set_qrv(&mut self, qrv: u8) { - self.buffer.as_mut()[8] = (self.buffer.as_ref()[8] & (!0x07)) | (qrv & 0x07) - } - pub fn set_qqic(&mut self, qqic: u8) { - self.buffer.as_mut()[9] = qqic - } - - pub fn update_checksum(&mut self) { - self.set_checksum(0); - let checksum = cal_checksum(self.buffer.as_ref()); - self.set_checksum(checksum); - } -} - -impl> IgmpV3QueryPacket { - pub fn igmp_type(&self) -> IgmpV3Type { - IgmpV3Type::from(self.buffer.as_ref()[0]) - } - pub fn max_resp_code(&self) -> u8 { - self.buffer.as_ref()[1] - } - pub fn checksum(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[2..4].try_into().unwrap()) - } - pub fn is_valid(&self) -> bool { - self.checksum() == 0 || cal_checksum(self.buffer.as_ref()) == 0 - } - pub fn group_address(&self) -> Ipv4Addr { - let tmp: [u8; 4] = self.buffer.as_ref()[4..8].try_into().unwrap(); - Ipv4Addr::from(tmp) - } - /// 保留字段,设置为0 - pub fn resv(&self) -> u8 { - self.buffer.as_ref()[8] >> 4 - } - /// 标志位 - /// 该比特位为1时,所有收到此查询报文的其他路由器不启动定时器刷新过程,但是此查询报文并不抑制查询者选举过程和路由器的主机侧处理过程;默认未置位。 - pub fn s(&self) -> u8 { - (self.buffer.as_ref()[8] & 0x0F) >> 3 - } - /// 查询者向网络通告的健壮系数 - /// 此参数可使查询者使用自己的健壮系统同步其他组播路由器的健壮系数; - /// 其他路由器接收到查询报文时,如果发现该字段非0,则将自己的健壮系数调整为该字段的值;如果发现该字段为0,则不做处理。默认健壮系数值为2。 - pub fn qrv(&self) -> u8 { - self.buffer.as_ref()[8] & 0x07 - } - /// IGMP查询者的查询间隔 - /// 非查询者收到查询报文时,如果发现该字段非0,则将自己的查询间隔参数调整为该字段的值:如果发现该字段为0,则不做处理。默认值为60。 - pub fn qqic(&self) -> u8 { - self.buffer.as_ref()[9] - } - /// 报文中包含的组播源的数量 - /// 对于普遍组查询报文和特定组查询报文,该字段为0;对于特定源组查询报文,该字段非0 - pub fn source_number(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[10..12].try_into().unwrap()) - } - pub fn source_addresses(&self) -> Option> { - let num = self.source_number(); - if num == 0 { - None - } else { - let num = num as usize; - let mut list = Vec::with_capacity(num); - let buf = self.buffer.as_ref(); - let len = buf.len(); - for index in 0..num { - let start = (12 + index * 4) as usize; - let end = start + 4; - if end > len { - return None; - } - let tmp: [u8; 4] = buf[start..end].try_into().unwrap(); - list.push(Ipv4Addr::from(tmp)); - } - Some(list) - } - } - pub fn source_address(&self, index: u16) -> Option { - if self.source_number() >= index { - None - } else { - let start = (12 + index * 4) as usize; - let end = start + 4; - let buf = self.buffer.as_ref(); - let len = buf.len(); - if end > len { - return None; - } - let tmp: [u8; 4] = buf[start..end].try_into().unwrap(); - Some(Ipv4Addr::from(tmp)) - } - } -} - -/// v3版本的query报文 -pub struct IgmpV3ReportPacket { - pub buffer: B, -} - -impl> IgmpV3ReportPacket { - pub fn unchecked(buffer: B) -> Self { - Self { buffer } - } - pub fn new(buffer: B) -> io::Result { - if buffer.as_ref().len() < 8 { - Err(io::Error::from(io::ErrorKind::InvalidData)) - } else { - let packet = Self::unchecked(buffer); - Ok(packet) - } - } -} - -impl> IgmpV3ReportPacket { - pub fn igmp_type(&self) -> IgmpV3Type { - IgmpV3Type::from(self.buffer.as_ref()[0]) - } - pub fn reserved1(&self) -> u8 { - self.buffer.as_ref()[1] - } - pub fn checksum(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[2..4].try_into().unwrap()) - } - pub fn is_valid(&self) -> bool { - self.checksum() == 0 || cal_checksum(self.buffer.as_ref()) == 0 - } - pub fn reserved2(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[4..6].try_into().unwrap()) - } - pub fn record_number(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[6..8].try_into().unwrap()) - } - pub fn group_records(&self) -> Option>> { - let num = self.record_number(); - if num == 0 { - None - } else { - let num = num as usize; - let mut list = Vec::with_capacity(num); - let mut start = 8 as usize; - let buf = self.buffer.as_ref(); - let len = buf.len(); - for _ in 0..num { - if start >= len { - return None; - } - if let Ok(record) = IgmpV3RecordPacket::new(&buf[start..]) { - let end = start - + 8 - + record.aux_data_len() as usize * 4 - + record.source_number() as usize * 4; - if end > len { - return None; - } - list.push(IgmpV3RecordPacket::new(&buf[start..end]).unwrap()); - start = end; - } else { - return None; - } - } - Some(list) - } - } -} - -/// group record -pub struct IgmpV3RecordPacket { - pub buffer: B, -} - -impl> IgmpV3RecordPacket { - pub fn unchecked(buffer: B) -> Self { - Self { buffer } - } - pub fn new(buffer: B) -> io::Result { - if buffer.as_ref().len() < 8 { - Err(io::Error::from(io::ErrorKind::InvalidData)) - } else { - let packet = Self::unchecked(buffer); - Ok(packet) - } - } -} - -impl> IgmpV3RecordPacket { - pub fn record_type(&self) -> IgmpV3RecordType { - IgmpV3RecordType::from(self.buffer.as_ref()[0]) - } - /// 辅助数据长度 以4字节为单位 - pub fn aux_data_len(&self) -> u8 { - self.buffer.as_ref()[1] - } - /// 源地址数 - pub fn source_number(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[2..4].try_into().unwrap()) - } - ///多播地址 - pub fn multicast_address(&self) -> Ipv4Addr { - let tmp: [u8; 4] = self.buffer.as_ref()[4..8].try_into().unwrap(); - Ipv4Addr::from(tmp) - } - pub fn source_addresses(&self) -> Option> { - let num = self.source_number(); - if num == 0 { - None - } else { - let num = num as usize; - let mut list = Vec::with_capacity(num); - let buf = self.buffer.as_ref(); - let len = buf.len(); - for index in 0..num { - let start = (8 + index * 4) as usize; - let end = start + 4; - if end > len { - return None; - } - let tmp: [u8; 4] = buf[start..end].try_into().unwrap(); - list.push(Ipv4Addr::from(tmp)); - } - Some(list) - } - } - pub fn source_address(&self, index: u16) -> Option { - if self.source_number() >= index { - None - } else { - let start = (8 + index * 4) as usize; - let end = start + 4; - if end > self.buffer.as_ref().len() { - return None; - } - let tmp: [u8; 4] = self.buffer.as_ref()[start..end].try_into().unwrap(); - Some(Ipv4Addr::from(tmp)) - } - } - /// 在文档中没有定义辅助数据的作用,通常应该是空的 - pub fn auxiliary_data(&self) -> &[u8] { - let start = 8 + self.source_number() as usize * 4; - let end = start + self.aux_data_len() as usize * 4; - if end > self.buffer.as_ref().len() { - return &[]; - } - &self.buffer.as_ref()[start..end] - } -} - -impl> fmt::Debug for IgmpV3QueryPacket { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("igmp::V3Query") - .field("type", &self.igmp_type()) - .field("max_resp_code", &self.max_resp_code()) - .field("checksum", &self.checksum()) - .field("is_valid", &self.is_valid()) - .field("group_address", &self.group_address()) - .field("s", &self.s()) - .field("qrv", &self.qrv()) - .field("qqic", &self.qqic()) - .field("number of sources", &self.source_number()) - .field("source_addresses", &self.source_addresses()) - .finish() - } -} - -impl> fmt::Debug for IgmpV3ReportPacket { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("igmp::V3Report") - .field("type", &self.igmp_type()) - .field("reserved1", &self.reserved1()) - .field("checksum", &self.checksum()) - .field("is_valid", &self.is_valid()) - .field("reserved2", &self.reserved2()) - .field("record_number", &self.record_number()) - .field("group_records", &self.group_records()) - .finish() - } -} - -impl> fmt::Debug for IgmpV3RecordPacket { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("igmp::V3Record") - .field("record_type", &self.record_type()) - .field("aux_data_len", &self.aux_data_len()) - .field("source_number", &self.source_number()) - .field("multicast_address", &self.multicast_address()) - .field("source_addresses", &self.source_addresses()) - .field("auxiliary_data", &self.auxiliary_data()) - .finish() - } -} diff --git a/vnt/packet/src/igmp/mod.rs b/vnt/packet/src/igmp/mod.rs deleted file mode 100644 index 113ab19..0000000 --- a/vnt/packet/src/igmp/mod.rs +++ /dev/null @@ -1,46 +0,0 @@ -pub mod igmp_v1; -pub mod igmp_v2; -pub mod igmp_v3; - -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum IgmpType { - /// 0x11 所有组224.0.0.1或者特定组 - Query, - /// 0x12 - ReportV1, - /// 0x16 - ReportV2, - /// 0x22 - ReportV3, - /// 0x17 目标组固定是 224.0.0.2 - LeaveV2, - Unknown(u8), -} - -impl From for IgmpType { - fn from(value: u8) -> IgmpType { - use self::IgmpType::*; - - match value { - 0x11 => Query, - 0x12 => ReportV1, - 0x16 => ReportV2, - 0x22 => ReportV3, - 0x17 => LeaveV2, - v => Unknown(v), - } - } -} - -impl Into for IgmpType { - fn into(self) -> u8 { - match self { - IgmpType::Query => 0x11, - IgmpType::ReportV1 => 0x12, - IgmpType::ReportV2 => 0x16, - IgmpType::ReportV3 => 0x22, - IgmpType::LeaveV2 => 0x17, - IgmpType::Unknown(v) => v, - } - } -} diff --git a/vnt/packet/src/ip/ipv4/mod.rs b/vnt/packet/src/ip/ipv4/mod.rs deleted file mode 100644 index c9cf115..0000000 --- a/vnt/packet/src/ip/ipv4/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod packet; -pub mod protocol; diff --git a/vnt/packet/src/ip/ipv4/packet.rs b/vnt/packet/src/ip/ipv4/packet.rs deleted file mode 100644 index 5a41021..0000000 --- a/vnt/packet/src/ip/ipv4/packet.rs +++ /dev/null @@ -1,235 +0,0 @@ -use std::net::Ipv4Addr; -use std::{fmt, io}; - -use crate::cal_checksum; -use crate::ip::ipv4::protocol::Protocol; - -/// ip协议 -/* -RFC: 791 https://www.ietf.org/rfc/rfc791.txt - - 0 15 31 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | 版本(4) | 头部长度(4) | 服务类型(8) | 总字节数(16) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | 标识(16) | 标志(3) | 片偏移(13) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | 生存时间(8) | 协议(8) | 头部校验和(16) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | 源ip地址(32) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | 目的ip地址(32) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | 选项 + 填充 | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - 数据体 - 注:头部长度单位是4字节,所以ip头最长60字节,选项最长40字节,选项填充按4字节对齐 -*/ - -pub struct IpV4Packet { - pub buffer: B, -} - -impl> IpV4Packet { - pub fn unchecked(buffer: B) -> Self { - Self { buffer } - } - pub fn new(buffer: B) -> io::Result { - if buffer.as_ref().len() < 20 { - Err(io::Error::new(io::ErrorKind::InvalidData, "len < 20"))?; - } - if buffer.as_ref()[0] >> 4 != 4 { - Err(io::Error::new(io::ErrorKind::InvalidData, "not ipv4"))?; - } - let packet = Self::unchecked(buffer); - if packet.buffer.as_ref().len() < packet.header_len() as usize * 4 { - Err(io::Error::new(io::ErrorKind::InvalidData, "head_len err"))?; - } - Ok(packet) - } -} - -impl> IpV4Packet { - pub fn header(&self) -> &[u8] { - &self.buffer.as_ref()[..(self.header_len() as usize * 4)] - } - pub fn payload(&self) -> &[u8] { - &self.buffer.as_ref()[(self.header_len() as usize * 4)..] - } -} - -impl + AsMut<[u8]>> IpV4Packet { - pub fn header_mut(&mut self) -> &mut [u8] { - let len = self.header_len() as usize * 4; - &mut self.buffer.as_mut()[..len] - } - pub fn payload_mut(&mut self) -> &mut [u8] { - let len = self.header_len() as usize * 4; - &mut self.buffer.as_mut()[len..] - } - pub fn set_protocol(&mut self, value: Protocol) { - self.header_mut()[9] = value.into(); - } - pub fn set_source_ip(&mut self, value: Ipv4Addr) { - self.header_mut()[12..16].copy_from_slice(&value.octets()); - } - pub fn set_destination_ip(&mut self, value: Ipv4Addr) { - self.header_mut()[16..20].copy_from_slice(&value.octets()); - } - pub fn set_flags(&mut self, flags: u8) { - self.buffer.as_mut()[6] = (self.buffer.as_ref()[6] & 0b11100000) | (flags << 5) - } - pub fn set_ttl(&mut self, ttl: u8) { - self.buffer.as_mut()[8] = ttl - } - fn set_checksum(&mut self, value: u16) { - self.header_mut()[10..12].copy_from_slice(&value.to_be_bytes()) - } - /// 更新校验和 - pub fn update_checksum(&mut self) { - //先将校验和置0 - self.set_checksum(0); - self.set_checksum(cal_checksum(self.header())) - } -} - -impl> IpV4Packet { - /// 版本号,ipv4的为4 - pub fn version(&self) -> u8 { - self.buffer.as_ref()[0] >> 4 - } - - /// 头部长度,以4字节为单位 - pub fn header_len(&self) -> u8 { - self.buffer.as_ref()[0] & 0b1111 - } - - /// 差异化服务编码点 - /// - /// 类别(3)+丢失概率(2)+用途(1) - /// - /// - /// 类别子字段值 | 名称 - /// ---|:--- - /// 000 | 常规(Routine) - /// 001 | 优先(Priority) - /// 010 | 立即(Immediate) - /// 011 | 瞬间(Flash) - /// 100 | 瞬间覆盖(Flash Override) - /// 101 | 严重(CRITIC/ECP) - /// 110 | 网间控制(Internetwork Control) - /// 111 | 网络控制(Network Control) - /// - /// - /// 参考:https://www.modb.pro/db/477116 - pub fn dscp(&self) -> u8 { - self.buffer.as_ref()[1] >> 2 - } - - /// 显示拥塞 00:发送主机不支持ECN 01或者10:发送主机支持ECN 11:路由器正在经历拥塞 - pub fn ecn(&self) -> u8 { - self.buffer.as_ref()[1] & 0b11 - } - - /// ip报总字节数 - pub fn length(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[2..4].try_into().unwrap()) - } - - /// 标识. ip报文在数据链路层可能会被拆分,同一报文的不同分组标识字段相同 - pub fn id(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[4..6].try_into().unwrap()) - } - - /// 标志 3位. - /// 第1位没有使用 - /// 第2位表示不分段位(DF) - /// 0:允许数据报分段 - /// 1:数据报不能分段 - /// 置1之后路由器不能对其分段处理,如果超过MTU值则路由器不能对其转发将其丢弃,并向源点发送错误消息 - /// 第3位表示更多段位 - /// 0:数据包后面没有包,该包为最后的包 - /// 1:数据包后面有更多的包 - pub fn flags(&self) -> u8 { - self.buffer.as_ref()[6] >> 5 - } - - /// 片偏移 13位. - /// 以字节为单位,用于指明分段起始点相对于包头起始点的偏移量 - /// 由于分段到达时可能错序,所以分段的偏移字段可以使接收者按照正确的顺序重组数据包 - pub fn offset(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[6..8].try_into().unwrap()) & 0x1fff - } - - /// 生存时间. - /// 每一跳 减1 到0了则会被丢弃 - pub fn ttl(&self) -> u8 { - self.buffer.as_ref()[8] - } - - /// 协议. - pub fn protocol(&self) -> Protocol { - self.buffer.as_ref()[9].into() - } - - /// 首部校验和 - pub fn checksum(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[10..12].try_into().unwrap()) - } - /// 验证校验和 - /// - /// TCP/IP协议栈不会自己计算校验和,而是简单地将一个空的校验和字段(零或随机填充)交给网卡硬件。 - /// 所以抓到发出去的包校验和可能是错误的 - pub fn is_valid(&self) -> bool { - self.checksum() == 0 || cal_checksum(self.header()) == 0 - } - /// 源ip. - pub fn source_ip(&self) -> Ipv4Addr { - Ipv4Addr::new( - self.buffer.as_ref()[12], - self.buffer.as_ref()[13], - self.buffer.as_ref()[14], - self.buffer.as_ref()[15], - ) - } - - /// 目标ip. - pub fn destination_ip(&self) -> Ipv4Addr { - Ipv4Addr::new( - self.buffer.as_ref()[16], - self.buffer.as_ref()[17], - self.buffer.as_ref()[18], - self.buffer.as_ref()[19], - ) - } - - /// 选项. - pub fn options(&self) -> &[u8] { - &self.buffer.as_ref()[20..(self.header_len() as usize * 4)] - } -} - -impl> fmt::Debug for IpV4Packet { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("ip::v4::Packet") - .field("version", &self.version()) - .field("header_len", &self.header_len()) - .field("dscp", &self.dscp()) - .field("ecn", &self.ecn()) - .field("length", &self.length()) - .field("id", &self.id()) - .field("flags", &self.flags()) - .field("offset", &self.offset()) - .field("ttl", &self.ttl()) - .field("protocol", &self.protocol()) - .field("checksum", &self.checksum()) - .field("is_valid", &self.is_valid()) - .field("source", &self.source_ip()) - .field("destination", &self.destination_ip()) - .field("options", &self.options()) - .field("payload", &self.payload()) - .finish() - } -} diff --git a/vnt/packet/src/ip/ipv4/protocol.rs b/vnt/packet/src/ip/ipv4/protocol.rs deleted file mode 100644 index 957fa08..0000000 --- a/vnt/packet/src/ip/ipv4/protocol.rs +++ /dev/null @@ -1,742 +0,0 @@ -#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Debug)] -pub enum Protocol { - /// - Hopopt, - - /// - Icmp, - - /// - Igmp, - - /// - Ggp, - - /// - Ipv4, - - /// - St, - - /// - Tcp, - - /// - Cbt, - - /// - Egp, - - /// - Igp, - - /// - BbnRccMon, - - /// - NvpII, - - /// - Pup, - - /// - Argus, - - /// - Emcon, - - /// - Xnet, - - /// - Chaos, - - /// - Udp, - - /// - Mux, - - /// - DcnMeas, - - /// - Hmp, - - /// - Prm, - - /// - XnsIdp, - - /// - Trunk1, - - /// - Trunk2, - - /// - Leaf1, - - /// - Leaf2, - - /// - Rdp, - - /// - Irtp, - - /// - IsoTp4, - - /// - Netblt, - - /// - MfeNsp, - - /// - MeritInp, - - /// - Dccp, - - /// - ThreePc, - - /// - Idpr, - - /// - Xtp, - - /// - Ddp, - - /// - IdprCmtp, - - /// - TpPlusPlus, - - /// - Il, - - /// - Ipv6, - - /// - Sdrp, - - /// - Ipv6Route, - - /// - Ipv6Frag, - - /// - Idrp, - - /// - Rsvp, - - /// - Gre, - - /// - Dsr, - - /// - Bna, - - /// - Esp, - - /// - Ah, - - /// - INlsp, - - /// - Swipe, - - /// - Narp, - - /// - Mobile, - - /// - Tlsp, - - /// - Skip, - - /// - Ipv6Icmp, - - /// - Ipv6NoNxt, - - /// - Ipv6Opts, - - /// - HostInternal, - - /// - Cftp, - - /// - LocalNetwork, - - /// - SatExpak, - - /// - Kryptolan, - - /// - Rvd, - - /// - Ippc, - - /// - DistributedFs, - - /// - SatMon, - - /// - Visa, - - /// - Ipcv, - - /// - Cpnx, - - /// - Cphb, - - /// - Wsn, - - /// - Pvp, - - /// - BrSatMon, - - /// - SunNd, - - /// - WbMon, - - /// - WbExpak, - - /// - IsoIp, - - /// - Vmtp, - - /// - SecureVmtp, - - /// - Vines, - - /// - TtpOrIptm, - - /// - NsfnetIgp, - - /// - Dgp, - - /// - Tcf, - - /// - Eigrp, - - /// - OspfigP, - - /// - SpriteRpc, - - /// - Larp, - - /// - Mtp, - - /// - Ax25, - - /// - IpIp, - - /// - Micp, - - /// - SccSp, - - /// - Etherip, - - /// - Encap, - - /// - PrivEncryption, - - /// - Gmtp, - - /// - Ifmp, - - /// - Pnni, - - /// - Pim, - - /// - Aris, - - /// - Scps, - - /// - Qnx, - - /// - AN, - - /// - IpComp, - - /// - Snp, - - /// - CompaqPeer, - - /// - IpxInIp, - - /// - Vrrp, - - /// - Pgm, - - /// - ZeroHop, - - /// - L2tp, - - /// - Ddx, - - /// - Iatp, - - /// - Stp, - - /// - Srp, - - /// - Uti, - - /// - Smp, - - /// - Sm, - - /// - Ptp, - - /// - IsisOverIpv4, - - /// - Fire, - - /// - Crtp, - - /// - Crudp, - - /// - Sscopmce, - - /// - Iplt, - - /// - Sps, - - /// - Pipe, - - /// - Sctp, - - /// - Fc, - - /// - RsvpE2eIgnore, - - /// - MobilityHeader, - - /// - UdpLite, - - /// - MplsInIp, - - /// - Manet, - - /// - Hip, - - /// - Shim6, - - /// - Wesp, - Rohc, - Test1, - Test2, - Unknown(u8), -} - -impl From for Protocol { - fn from(value: u8) -> Protocol { - use self::Protocol::*; - - match value { - 0 => Hopopt, - 1 => Icmp, - 2 => Igmp, - 3 => Ggp, - 4 => Ipv4, - 5 => St, - 6 => Tcp, - 7 => Cbt, - 8 => Egp, - 9 => Igp, - 10 => BbnRccMon, - 11 => NvpII, - 12 => Pup, - 13 => Argus, - 14 => Emcon, - 15 => Xnet, - 16 => Chaos, - 17 => Udp, - 18 => Mux, - 19 => DcnMeas, - 20 => Hmp, - 21 => Prm, - 22 => XnsIdp, - 23 => Trunk1, - 24 => Trunk2, - 25 => Leaf1, - 26 => Leaf2, - 27 => Rdp, - 28 => Irtp, - 29 => IsoTp4, - 30 => Netblt, - 31 => MfeNsp, - 32 => MeritInp, - 33 => Dccp, - 34 => ThreePc, - 35 => Idpr, - 36 => Xtp, - 37 => Ddp, - 38 => IdprCmtp, - 39 => TpPlusPlus, - 40 => Il, - 41 => Ipv6, - 42 => Sdrp, - 43 => Ipv6Route, - 44 => Ipv6Frag, - 45 => Idrp, - 46 => Rsvp, - 47 => Gre, - 48 => Dsr, - 49 => Bna, - 50 => Esp, - 51 => Ah, - 52 => INlsp, - 53 => Swipe, - 54 => Narp, - 55 => Mobile, - 56 => Tlsp, - 57 => Skip, - 58 => Ipv6Icmp, - 59 => Ipv6NoNxt, - 60 => Ipv6Opts, - 61 => HostInternal, - 62 => Cftp, - 63 => LocalNetwork, - 64 => SatExpak, - 65 => Kryptolan, - 66 => Rvd, - 67 => Ippc, - 68 => DistributedFs, - 69 => SatMon, - 70 => Visa, - 71 => Ipcv, - 72 => Cpnx, - 73 => Cphb, - 74 => Wsn, - 75 => Pvp, - 76 => BrSatMon, - 77 => SunNd, - 78 => WbMon, - 79 => WbExpak, - 80 => IsoIp, - 81 => Vmtp, - 82 => SecureVmtp, - 83 => Vines, - 84 => TtpOrIptm, - 85 => NsfnetIgp, - 86 => Dgp, - 87 => Tcf, - 88 => Eigrp, - 89 => OspfigP, - 90 => SpriteRpc, - 91 => Larp, - 92 => Mtp, - 93 => Ax25, - 94 => IpIp, - 95 => Micp, - 96 => SccSp, - 97 => Etherip, - 98 => Encap, - 99 => PrivEncryption, - 100 => Gmtp, - 101 => Ifmp, - 102 => Pnni, - 103 => Pim, - 104 => Aris, - 105 => Scps, - 106 => Qnx, - 107 => AN, - 108 => IpComp, - 109 => Snp, - 110 => CompaqPeer, - 111 => IpxInIp, - 112 => Vrrp, - 113 => Pgm, - 114 => ZeroHop, - 115 => L2tp, - 116 => Ddx, - 117 => Iatp, - 118 => Stp, - 119 => Srp, - 120 => Uti, - 121 => Smp, - 122 => Sm, - 123 => Ptp, - 124 => IsisOverIpv4, - 125 => Fire, - 126 => Crtp, - 127 => Crudp, - 128 => Sscopmce, - 129 => Iplt, - 130 => Sps, - 131 => Pipe, - 132 => Sctp, - 133 => Fc, - 134 => RsvpE2eIgnore, - 135 => MobilityHeader, - 136 => UdpLite, - 137 => MplsInIp, - 138 => Manet, - 139 => Hip, - 140 => Shim6, - 141 => Wesp, - 142 => Rohc, - 253 => Test1, - 254 => Test2, - p => Unknown(p), - } - } -} - -impl Into for Protocol { - fn into(self) -> u8 { - use self::Protocol::*; - - match self { - Hopopt => 0, - Icmp => 1, - Igmp => 2, - Ggp => 3, - Ipv4 => 4, - St => 5, - Tcp => 6, - Cbt => 7, - Egp => 8, - Igp => 9, - BbnRccMon => 10, - NvpII => 11, - Pup => 12, - Argus => 13, - Emcon => 14, - Xnet => 15, - Chaos => 16, - Udp => 17, - Mux => 18, - DcnMeas => 19, - Hmp => 20, - Prm => 21, - XnsIdp => 22, - Trunk1 => 23, - Trunk2 => 24, - Leaf1 => 25, - Leaf2 => 26, - Rdp => 27, - Irtp => 28, - IsoTp4 => 29, - Netblt => 30, - MfeNsp => 31, - MeritInp => 32, - Dccp => 33, - ThreePc => 34, - Idpr => 35, - Xtp => 36, - Ddp => 37, - IdprCmtp => 38, - TpPlusPlus => 39, - Il => 40, - Ipv6 => 41, - Sdrp => 42, - Ipv6Route => 43, - Ipv6Frag => 44, - Idrp => 45, - Rsvp => 46, - Gre => 47, - Dsr => 48, - Bna => 49, - Esp => 50, - Ah => 51, - INlsp => 52, - Swipe => 53, - Narp => 54, - Mobile => 55, - Tlsp => 56, - Skip => 57, - Ipv6Icmp => 58, - Ipv6NoNxt => 59, - Ipv6Opts => 60, - HostInternal => 61, - Cftp => 62, - LocalNetwork => 63, - SatExpak => 64, - Kryptolan => 65, - Rvd => 66, - Ippc => 67, - DistributedFs => 68, - SatMon => 69, - Visa => 70, - Ipcv => 71, - Cpnx => 72, - Cphb => 73, - Wsn => 74, - Pvp => 75, - BrSatMon => 76, - SunNd => 77, - WbMon => 78, - WbExpak => 79, - IsoIp => 80, - Vmtp => 81, - SecureVmtp => 82, - Vines => 83, - TtpOrIptm => 84, - NsfnetIgp => 85, - Dgp => 86, - Tcf => 87, - Eigrp => 88, - OspfigP => 89, - SpriteRpc => 90, - Larp => 91, - Mtp => 92, - Ax25 => 93, - IpIp => 94, - Micp => 95, - SccSp => 96, - Etherip => 97, - Encap => 98, - PrivEncryption => 99, - Gmtp => 100, - Ifmp => 101, - Pnni => 102, - Pim => 103, - Aris => 104, - Scps => 105, - Qnx => 106, - AN => 107, - IpComp => 108, - Snp => 109, - CompaqPeer => 110, - IpxInIp => 111, - Vrrp => 112, - Pgm => 113, - ZeroHop => 114, - L2tp => 115, - Ddx => 116, - Iatp => 117, - Stp => 118, - Srp => 119, - Uti => 120, - Smp => 121, - Sm => 122, - Ptp => 123, - IsisOverIpv4 => 124, - Fire => 125, - Crtp => 126, - Crudp => 127, - Sscopmce => 128, - Iplt => 129, - Sps => 130, - Pipe => 131, - Sctp => 132, - Fc => 133, - RsvpE2eIgnore => 134, - MobilityHeader => 135, - UdpLite => 136, - MplsInIp => 137, - Manet => 138, - Hip => 139, - Shim6 => 140, - Wesp => 141, - Rohc => 142, - Test1 => 253, - Test2 => 254, - Unknown(p) => p, - } - } -} diff --git a/vnt/packet/src/ip/mod.rs b/vnt/packet/src/ip/mod.rs deleted file mode 100644 index 7908624..0000000 --- a/vnt/packet/src/ip/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -use ipv4::packet::IpV4Packet; -use std::io; - -pub mod ipv4; - -pub enum IpPacket { - V4(IpV4Packet), -} - -impl> IpPacket { - pub fn new(buffer: B) -> io::Result { - match buffer.as_ref()[0] >> 4 { - 4 => Ok(IpPacket::V4(IpV4Packet::new(buffer)?)), - _ => Err(io::Error::from(io::ErrorKind::InvalidData)), - } - } -} diff --git a/vnt/packet/src/lib.rs b/vnt/packet/src/lib.rs deleted file mode 100644 index 56d6d8d..0000000 --- a/vnt/packet/src/lib.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::net::Ipv4Addr; - -use byteorder::BigEndian; -use byteorder::ReadBytesExt; - -pub mod arp; -pub mod ethernet; -pub mod icmp; -pub mod igmp; -pub mod ip; -pub mod tcp; -pub mod udp; -// pub enum IpUpperLayer { -// UDP(UdpPacket), -// Unknown(B), -// } -// -// impl> fmt::Debug for IpUpperLayer { -// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -// match self { -// IpUpperLayer::UDP(p) => { -// f.debug_struct("udp::Packet") -// .field("data", p).finish() -// } -// IpUpperLayer::Unknown(p) => { -// f.debug_struct("Unknown") -// .field("data", &p.as_ref()).finish() -// } -// } -// } -// } - -/// https://datatracker.ietf.org/doc/html/rfc1071 4.1节 -/// -/// 计算校验和,各协议都是通用的 -/// 计算: -/// 首先将校验和置0,然后对首部每个16位数进行二进制反码求和, -/// 得到校验和之后,持续取高16位加到低16位,直到高16位全为0 -/// 最后取反 -/// -/// 校验: -/// 在已有校验和的情况下,再计算校验和,正确的数据计算得到的值为0 -/* -unsigned short getChecksum(unsigned short * iphead, int count) -{ - unsigned long int sum = 0; - unsigned short checksum = 0; - - printf("\nStarting adress: %p\n", iphead); - - while(count > 1) { - sum += * (unsigned short *) (iphead); - count -=2; - printf("a: %p, content is: %d, new sum: %ld\n", iphead, (unsigned short) *(iphead), sum); - iphead++; - } - - if(count > 0) { - sum += * (unsigned short *) (iphead); - } - - while(sum >> 16) { - sum = (sum & 0xffff) + (sum >> 16); - } - - checksum = ~sum; - - return checksum; -} - */ -pub fn cal_checksum(buffer: &[u8]) -> u16 { - use std::io::Cursor; - let mut sum = 0; - let mut buffer = Cursor::new(buffer); - while let Ok(value) = buffer.read_u16::() { - sum += u32::from(value); - } - if let Ok(l) = buffer.read_u8() { - sum += u32c(l, 0); - } - while sum >> 16 != 0 { - sum = (sum & 0xffff) + (sum >> 16); - } - !sum as u16 -} - -/// ipv4上层协议校验和计算方式 -/// ipv4 udp伪首部 用于参与计算首部校验和 -/* - 0 7 8 15 16 23 24 31 - +--------+--------+--------+--------+ - | source address | - +--------+--------+--------+--------+ - | destination address | - +--------+--------+--------+--------+ - | zero |protocol| length | - +--------+--------+--------+--------+ -*/ -pub fn ipv4_cal_checksum( - buffer: &[u8], - src_ip: &Ipv4Addr, - dest_ip: &Ipv4Addr, - protocol: u8, -) -> u16 { - use std::io::Cursor; - let length = buffer.len(); - let mut sum = 0; - let src_ip = src_ip.octets(); - sum += u32c(src_ip[0], src_ip[1]); - sum += u32c(src_ip[2], src_ip[3]); - let dest_ip = dest_ip.octets(); - sum += u32c(dest_ip[0], dest_ip[1]); - sum += u32c(dest_ip[2], dest_ip[3]); - sum += u32c(0, protocol); - sum += length as u32; - let mut buffer = Cursor::new(buffer); - while let Ok(value) = buffer.read_u16::() { - sum += u32::from(value); - } - if let Ok(l) = buffer.read_u8() { - sum += u32c(l, 0); - } - while sum >> 16 != 0 { - sum = (sum & 0xffff) + (sum >> 16); - } - !sum as u16 -} - -#[inline] -fn u32c(x: u8, y: u8) -> u32 { - ((x as u32) << 8) | y as u32 -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let sum = cal_checksum(&[255, 255]); - println!("{:?}", sum); - } -} diff --git a/vnt/packet/src/tcp/mod.rs b/vnt/packet/src/tcp/mod.rs deleted file mode 100644 index ff641ba..0000000 --- a/vnt/packet/src/tcp/mod.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::fmt; - -pub mod tcp; - -pub struct Flags(u8); - -pub const FIN: u8 = 0b0000_0001; -pub const SYN: u8 = 0b0000_0010; -pub const RST: u8 = 0b0000_0100; -pub const PSH: u8 = 0b0000_1000; -pub const ACK: u8 = 0b0001_0000; -pub const URG: u8 = 0b0010_0000; - -impl fmt::Debug for Flags { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut str = String::with_capacity(22); - if self.0 & URG != 0 { - str.push_str("URG|"); - } - if self.0 & ACK != 0 { - str.push_str("ACK|"); - } - if self.0 & PSH != 0 { - str.push_str("PSH|"); - } - if self.0 & RST != 0 { - str.push_str("RST|"); - } - if self.0 & SYN != 0 { - str.push_str("SYN|"); - } - if self.0 & FIN != 0 { - str.push_str("FIN|"); - } - if str.is_empty() { - f.debug_struct("NULL").finish() - } else { - let len = str.len() - 1; - f.debug_struct(&str[..len]).finish() - } - } -} diff --git a/vnt/packet/src/tcp/tcp.rs b/vnt/packet/src/tcp/tcp.rs deleted file mode 100644 index a68ac9d..0000000 --- a/vnt/packet/src/tcp/tcp.rs +++ /dev/null @@ -1,178 +0,0 @@ -use std::net::Ipv4Addr; -use std::{fmt, io}; - -use crate::tcp::Flags; - -/// tcp -/* - https://www.rfc-editor.org/rfc/rfc793 - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Source Port | Destination Port | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Sequence Number | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Acknowledgment Number | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Data | |U|A|P|R|S|F| | - | Offset| Reserved |R|C|S|S|Y|I| Window | - | | |G|K|H|T|N|N| | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Checksum | Urgent Pointer | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Options | Padding | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | data | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - Source Port: 16位 源端口 - Destination Port:16位 目的端口 - Sequence Number:32位 序列号,如果存在syn标志,则为初始序列号 - Acknowledgment Number:32位 如果设置了ack标志,这个表示确认收到的序号 - Data Offset:4位 数据的开始偏移位,单位是4字节 - Reserved:6位 未使用,全零 - 控制位:6位 从左到右 - URG: 紧急指针 表示数据要优先处理 - ACK: 确认位 - PSH: 推送 要求把数据尽快的交给应用层,不做处理 - RST: 重置连接 - SYN: 同步序列号 - FIN: 结束发送 - Window: 16位 能接收的数据大小 - Checksum:16位 校验和,需要加入伪首部 - Urgent Pointer:16位 紧急指针 - Options+Padding:32位整数倍,最多40个字节 -*/ -pub struct TcpPacket { - source_ip: Ipv4Addr, - destination_ip: Ipv4Addr, - buffer: B, -} - -impl> TcpPacket { - pub fn unchecked(source_ip: Ipv4Addr, destination_ip: Ipv4Addr, buffer: B) -> TcpPacket { - TcpPacket { - source_ip, - destination_ip, - buffer, - } - } - pub fn new( - source_ip: Ipv4Addr, - destination_ip: Ipv4Addr, - buffer: B, - ) -> io::Result> { - let packet = TcpPacket::unchecked(source_ip, destination_ip, buffer); - - if packet.buffer.as_ref().len() < 20 { - Err(io::Error::from(io::ErrorKind::InvalidData))?; - } - - if packet.buffer.as_ref().len() < packet.data_offset() as usize * 4 { - Err(io::Error::from(io::ErrorKind::InvalidData))?; - } - - Ok(packet) - } -} -impl + AsMut<[u8]>> TcpPacket { - pub fn set_source_ip(&mut self, value: Ipv4Addr) { - self.source_ip = value; - } - pub fn set_destination_ip(&mut self, value: Ipv4Addr) { - self.destination_ip = value; - } -} -impl + AsMut<[u8]>> TcpPacket { - fn set_checksum(&mut self, value: u16) { - self.buffer.as_mut()[16..18].copy_from_slice(&value.to_be_bytes()) - } - pub fn set_source_port(&mut self, value: u16) { - self.buffer.as_mut()[0..2].copy_from_slice(&value.to_be_bytes()) - } - pub fn set_destination_port(&mut self, value: u16) { - self.buffer.as_mut()[2..4].copy_from_slice(&value.to_be_bytes()) - } - /// 更新校验和 - pub fn update_checksum(&mut self) { - //先将校验和置0 - self.set_checksum(0); - self.set_checksum(self.cal_checksum()) - } -} - -impl> TcpPacket { - /// 源端口 - pub fn source_port(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[0..2].try_into().unwrap()) - } - - /// 目标端口 - pub fn destination_port(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[2..4].try_into().unwrap()) - } - /// 序列号 - pub fn sequence(&self) -> u32 { - u32::from_be_bytes(self.buffer.as_ref()[4..8].try_into().unwrap()) - } - /// 确认号 - pub fn acknowledgment(&self) -> u32 { - u32::from_be_bytes(self.buffer.as_ref()[8..12].try_into().unwrap()) - } - /// 数据偏移 4字节为单位 - pub fn data_offset(&self) -> u8 { - self.buffer.as_ref()[12] >> 4 - } - pub fn flags(&self) -> Flags { - Flags(self.buffer.as_ref()[13]) - } - pub fn window(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[14..16].try_into().unwrap()) - } - pub fn checksum(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[16..18].try_into().unwrap()) - } - /// 验证校验和,ipv4中为0表示不使用校验和,ipv6校验和不能为0 - /// TCP/IP协议栈不会自己计算校验和,而是简单地将一个空的校验和字段(零或随机填充)交给网卡硬件。 - /// 所以抓到发出去的包校验和可能是错误的 - pub fn is_valid(&self) -> bool { - self.checksum() == 0 || self.cal_checksum() == 0 - } - fn cal_checksum(&self) -> u16 { - crate::ipv4_cal_checksum( - self.buffer.as_ref(), - &self.source_ip, - &self.destination_ip, - 6, - ) - } - pub fn urgent_pointer(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[18..20].try_into().unwrap()) - } - pub fn options(&self) -> &[u8] { - &self.buffer.as_ref()[20..(self.data_offset() as usize * 4)] - } - pub fn payload(&self) -> &[u8] { - &self.buffer.as_ref()[(self.data_offset() as usize * 4)..] - } -} - -impl> fmt::Debug for TcpPacket { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("tcp::Packet") - .field("source", &self.source_port()) - .field("destination", &self.destination_port()) - .field("sequence", &self.sequence()) - .field("acknowledgment", &self.acknowledgment()) - .field("offset", &self.data_offset()) - .field("flags", &self.flags()) - .field("window", &self.window()) - .field("checksum", &self.checksum()) - .field("is_valid", &self.is_valid()) - .field("pointer", &self.urgent_pointer()) - .field("options", &self.options()) - .field("payload", &self.payload()) - .finish() - } -} diff --git a/vnt/packet/src/udp/mod.rs b/vnt/packet/src/udp/mod.rs deleted file mode 100644 index 7e5aaa1..0000000 --- a/vnt/packet/src/udp/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod udp; diff --git a/vnt/packet/src/udp/udp.rs b/vnt/packet/src/udp/udp.rs deleted file mode 100644 index 3960b90..0000000 --- a/vnt/packet/src/udp/udp.rs +++ /dev/null @@ -1,150 +0,0 @@ -use std::net::Ipv4Addr; -use std::{fmt, io}; - -/// udp协议 -/// -/* -RFC 768 https://www.ietf.org/rfc/rfc768.txt - - 0 7 8 15 16 23 24 31 - +--------+--------+--------+--------+ - | 源端口(16) | 目的端口(16) | - +--------+--------+--------+--------+ - | 长度(16) | 校验和(16) | - +--------+--------+--------+--------+ - | - | 载荷 ... - +---------------- ... - - 注:1.长度包含标头和数据体,以字节为单位 - 2.伪首部和载荷参与校验和的计算,位数不够则补0 -*/ - -/// ipv6 udp伪首部 -/* https://datatracker.ietf.org/doc/html/rfc2460 - - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | | - + + - | | - + Source Address + - | | - + + - | | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | | - + + - | | - + Destination Address + - | | - + + - | | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Upper-Layer Packet Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | zero | Next Header | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -*/ - -pub struct UdpPacket { - source_ip: Ipv4Addr, - destination_ip: Ipv4Addr, - buffer: B, -} - -impl> UdpPacket { - pub fn unchecked(source_ip: Ipv4Addr, destination_ip: Ipv4Addr, buffer: B) -> UdpPacket { - UdpPacket { - source_ip, - destination_ip, - buffer, - } - } - pub fn new( - source_ip: Ipv4Addr, - destination_ip: Ipv4Addr, - buffer: B, - ) -> io::Result> { - if buffer.as_ref().len() < 8 { - Err(io::Error::from(io::ErrorKind::InvalidData))?; - } - let packet = Self::unchecked(source_ip, destination_ip, buffer); - Ok(packet) - } -} - -impl> UdpPacket { - /// 源端口 - pub fn source_port(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[0..2].try_into().unwrap()) - } - - /// 目标端口 - pub fn destination_port(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[2..4].try_into().unwrap()) - } - - /// 总字节数 - pub fn length(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[4..6].try_into().unwrap()) - } - - /// Checksum of the packet. - pub fn checksum(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[6..8].try_into().unwrap()) - } - /// 验证校验和,ipv4中为0表示不使用校验和,ipv6校验和不能为0 - pub fn is_valid(&self) -> bool { - self.checksum() == 0 || self.cal_checksum() == 0 - } - pub fn payload(&self) -> &[u8] { - &self.buffer.as_ref()[8..] - } - fn cal_checksum(&self) -> u16 { - crate::ipv4_cal_checksum( - self.buffer.as_ref(), - &self.source_ip, - &self.destination_ip, - 17, - ) - } -} - -// impl + AsMut<[u8]>> UdpPacket { -// fn header_mut(&mut self) -> &mut [u8] { -// &mut self.buffer.as_mut()[..8] -// } -// } - -impl + AsMut<[u8]>> UdpPacket { - /// 设置源端口 - pub fn set_source_port(&mut self, value: u16) { - self.buffer.as_mut()[0..2].copy_from_slice(&value.to_be_bytes()) - } - - /// 设置目的端口 - pub fn set_destination_port(&mut self, value: u16) { - self.buffer.as_mut()[2..4].copy_from_slice(&value.to_be_bytes()) - } - fn set_checksum(&mut self, value: u16) { - self.buffer.as_mut()[6..8].copy_from_slice(&value.to_be_bytes()) - } - pub fn update_checksum(&mut self) { - //先写0 - self.set_checksum(0); - self.set_checksum(self.cal_checksum()); - } -} - -impl> fmt::Debug for UdpPacket { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("udp::Packet") - .field("source", &self.source_port()) - .field("destination", &self.destination_port()) - .field("length", &self.length()) - .field("checksum", &self.checksum()) - .field("is_valid", &self.is_valid()) - .field("payload", &self.payload()) - .finish() - } -} diff --git a/vnt/proto/message.proto b/vnt/proto/message.proto deleted file mode 100644 index 948a123..0000000 --- a/vnt/proto/message.proto +++ /dev/null @@ -1,94 +0,0 @@ -syntax = "proto3"; - -message HandshakeRequest { - string version = 1; - bool secret = 2; - string key_finger = 3; -} -message HandshakeResponse { - string version = 1; - bool secret = 2; - bytes public_key = 3; - string key_finger = 4; -} -message SecretHandshakeRequest { - string token = 1; - bytes key = 2; -} -message RegistrationRequest { - string token = 1; - string device_id = 2; - string name = 3; - bool is_fast = 4; - string version = 5; - fixed32 virtual_ip = 6; - bool allow_ip_change = 7; - bool client_secret = 8; - bytes client_secret_hash = 9; -} - -message RegistrationResponse { - fixed32 virtual_ip = 1; - fixed32 virtual_gateway = 2; - fixed32 virtual_netmask = 3; - uint32 epoch = 4; - repeated DeviceInfo device_info_list = 5; - fixed32 public_ip = 6; - uint32 public_port = 7; - bytes public_ipv6 = 8; -} -message DeviceInfo { - string name = 1; - fixed32 virtual_ip = 2; - uint32 device_status = 3; - bool client_secret = 4; - bytes client_secret_hash = 5; - bool wireguard = 6; -} - -message DeviceList { - uint32 epoch = 1; - repeated DeviceInfo device_info_list = 2; -} - -message PunchInfo { - repeated fixed32 public_ip_list = 2; - uint32 public_port = 3; - uint32 public_port_range = 4; - PunchNatType nat_type = 5; - bool reply = 6; - fixed32 local_ip = 7; - uint32 local_port = 8; - bytes ipv6 = 9; - uint32 ipv6_port = 10; - uint32 tcp_port = 11; - repeated uint32 udp_ports = 12; - repeated uint32 public_ports = 13; - uint32 public_tcp_port = 14; - PunchNatModel punch_model = 15; -} -enum PunchNatType { - Symmetric = 0; - Cone = 1; -} -enum PunchNatModel { - All = 0; - IPv4 = 1; - IPv6 = 2; - IPv4Tcp = 3; - IPv4Udp = 4; - IPv6Tcp = 5; - IPv6Udp = 6; -} - -/// 向服务器上报客户端状态信息 -message ClientStatusInfo { - fixed32 source = 1; - repeated RouteItem p2p_list = 2; - uint64 up_stream = 3; - uint64 down_stream = 4; - PunchNatType nat_type = 5; -} -message RouteItem { - fixed32 next_ip = 1; -} \ No newline at end of file diff --git a/vnt/src/channel/context.rs b/vnt/src/channel/context.rs deleted file mode 100644 index 889c106..0000000 --- a/vnt/src/channel/context.rs +++ /dev/null @@ -1,559 +0,0 @@ -use fnv::FnvHashMap; -use std::net::{Ipv4Addr, SocketAddr, UdpSocket}; -use std::ops::Deref; -use std::sync::atomic::AtomicUsize; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use std::{io, thread}; - -use crossbeam_utils::atomic::AtomicCell; -use parking_lot::RwLock; -use rand::Rng; - -use crate::channel::punch::NatType; -use crate::channel::sender::{AcceptSocketSender, PacketSender}; -use crate::channel::socket::LocalInterface; -use crate::channel::{ConnectProtocol, Route, RouteKey, UseChannelType, DEFAULT_RT}; -use crate::protocol::NetPacket; -use crate::util::limit::TrafficMeterMultiAddress; - -/// 传输通道上下文,持有udp socket、tcp socket和路由信息 -#[derive(Clone)] -pub struct ChannelContext { - inner: Arc, -} - -impl ChannelContext { - pub fn new( - main_udp_socket: Vec, - v4_len: usize, - use_channel_type: UseChannelType, - first_latency: bool, - protocol: ConnectProtocol, - packet_loss_rate: Option, - packet_delay: u32, - up_traffic_meter: Option, - down_traffic_meter: Option, - default_interface: LocalInterface, - ) -> Self { - let channel_num = v4_len; - assert_ne!(channel_num, 0, "not channel"); - let packet_loss_rate = packet_loss_rate - .map(|v| { - let v = (v * PACKET_LOSS_RATE_DENOMINATOR as f64) as u32; - if v > PACKET_LOSS_RATE_DENOMINATOR { - PACKET_LOSS_RATE_DENOMINATOR - } else { - v - } - }) - .unwrap_or(0); - let inner = ContextInner { - main_udp_socket, - v4_len, - sub_udp_socket: RwLock::new(Vec::new()), - packet_map: RwLock::new(FnvHashMap::default()), - route_table: RouteTable::new(use_channel_type, first_latency, channel_num), - protocol, - packet_loss_rate, - packet_delay, - up_traffic_meter, - down_traffic_meter, - default_interface, - default_route_key: AtomicCell::default(), - }; - Self { - inner: Arc::new(inner), - } - } -} - -impl Deref for ChannelContext { - type Target = ContextInner; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -/// 对称网络增加的udp socket数目,有助于增加打洞成功率 -pub const SYMMETRIC_CHANNEL_NUM: usize = 84; -const PACKET_LOSS_RATE_DENOMINATOR: u32 = 100_0000; - -pub struct ContextInner { - // 核心udp socket - pub(crate) main_udp_socket: Vec, - v4_len: usize, - // 对称网络增加的udp socket - sub_udp_socket: RwLock>, - // tcp数据发送器 - pub(crate) packet_map: RwLock>, - // 路由信息 - pub route_table: RouteTable, - // 使用什么协议连接服务器 - protocol: ConnectProtocol, - //控制丢包率,取值v=[0,100_0000] 丢包率r=v/100_0000 - packet_loss_rate: u32, - //控制延迟 - packet_delay: u32, - pub(crate) up_traffic_meter: Option, - pub(crate) down_traffic_meter: Option, - default_interface: LocalInterface, - default_route_key: AtomicCell>, -} - -impl ContextInner { - pub fn use_channel_type(&self) -> UseChannelType { - self.route_table.use_channel_type - } - pub fn default_interface(&self) -> &LocalInterface { - &self.default_interface - } - pub fn set_default_route_key(&self, route_key: RouteKey) { - self.default_route_key.store(Some(route_key)); - } - /// 通过sub_udp_socket是否为空来判断是否为锥形网络 - pub fn is_cone(&self) -> bool { - self.sub_udp_socket.read().is_empty() - } - pub fn main_protocol(&self) -> ConnectProtocol { - self.protocol - } - pub fn is_udp_main(&self, route_key: &RouteKey) -> bool { - route_key.protocol().is_udp() && route_key.index < self.main_udp_socket.len() - } - pub fn first_latency(&self) -> bool { - self.route_table.first_latency - } - /// 切换NAT类型,不同的nat打洞模式会有不同 - pub fn switch( - &self, - nat_type: NatType, - udp_socket_sender: &AcceptSocketSender>>, - ) -> anyhow::Result<()> { - let mut write_guard = self.sub_udp_socket.write(); - match nat_type { - NatType::Symmetric => { - if !write_guard.is_empty() { - return Ok(()); - } - let mut vec = Vec::with_capacity(SYMMETRIC_CHANNEL_NUM); - for _ in 0..SYMMETRIC_CHANNEL_NUM { - let udp = crate::channel::socket::bind_udp( - "0.0.0.0:0".parse().unwrap(), - &self.default_interface, - )?; - let udp: UdpSocket = udp.into(); - vec.push(udp); - } - let mut mio_vec = Vec::with_capacity(SYMMETRIC_CHANNEL_NUM); - for udp in vec.iter() { - let udp_socket = mio::net::UdpSocket::from_std(udp.try_clone()?); - mio_vec.push(udp_socket); - } - udp_socket_sender.try_add_socket(Some(mio_vec))?; - *write_guard = vec; - } - NatType::Cone => { - if write_guard.is_empty() { - return Ok(()); - } - udp_socket_sender.try_add_socket(None)?; - *write_guard = Vec::new(); - } - } - Ok(()) - } - #[inline] - pub fn channel_num(&self) -> usize { - self.v4_len - } - #[inline] - pub fn main_len(&self) -> usize { - self.main_udp_socket.len() - } - /// 获取核心udp监听的端口,用于其他客户端连接 - pub fn main_local_udp_port(&self) -> io::Result> { - let mut ports = Vec::new(); - for udp in self.main_udp_socket[..self.v4_len].iter() { - ports.push(udp.local_addr()?.port()) - } - Ok(ports) - } - pub fn send_tcp(&self, buf: &[u8], route_key: &RouteKey) -> io::Result<()> { - if let Some(tcp) = self.packet_map.read().get(route_key) { - tcp.try_send(buf) - } else { - Err(io::Error::new( - io::ErrorKind::NotFound, - format!("dest={:?}", route_key), - )) - } - } - pub fn send_main_udp(&self, index: usize, buf: &[u8], addr: SocketAddr) -> io::Result<()> { - if let Some(udp) = self.main_udp_socket.get(index) { - udp.send_to(buf, addr)?; - Ok(()) - } else { - Err(io::Error::new(io::ErrorKind::Other, "overflow")) - } - } - /// 将数据发送到默认通道,一般发往服务器才用此方法 - pub fn send_default>( - &self, - buf: &NetPacket, - addr: SocketAddr, - ) -> io::Result<()> { - if self.protocol.is_udp() { - if addr.is_ipv4() { - self.send_main_udp(0, buf.buffer(), addr)? - } else { - self.send_main_udp(self.v4_len, buf.buffer(), addr)? - } - } else { - if let Some(key) = self.default_route_key.load() { - self.send_tcp(buf.buffer(), &key)? - } else { - return Err(io::Error::new( - io::ErrorKind::NotFound, - format!("dest={:?}", addr), - )); - } - } - if let Some(up_traffic_meter) = &self.up_traffic_meter { - up_traffic_meter.add_traffic(buf.destination(), buf.data_len()); - } - Ok(()) - } - - /// 此方法仅用于对称网络打洞 - pub fn try_send_all(&self, buf: &[u8], addr: SocketAddr) { - self.try_send_all_main(buf, addr); - for udp in self.sub_udp_socket.read().iter() { - if let Err(e) = udp.send_to(buf, addr) { - log::warn!("{:?},add={:?}", e, addr); - } - thread::sleep(Duration::from_millis(3)); - } - } - pub fn try_send_all_main(&self, buf: &[u8], addr: SocketAddr) { - for index in 0..self.channel_num() { - if let Err(e) = self.send_main_udp(index, buf, addr) { - log::warn!("{:?},add={:?}", e, addr); - } - } - } - /// 发送网络数据 - pub fn send_ipv4_by_id>( - &self, - buf: &NetPacket, - id: &Ipv4Addr, - server_addr: SocketAddr, - send_default: bool, - ) -> io::Result<()> { - if self.packet_loss_rate > 0 { - if rand::thread_rng().gen_ratio(self.packet_loss_rate, PACKET_LOSS_RATE_DENOMINATOR) { - return Ok(()); - } - } - - if self.packet_delay > 0 { - thread::sleep(Duration::from_millis(self.packet_delay as _)); - } - //优先发到直连到地址 - if let Err(e) = self.send_by_id(buf, id) { - if e.kind() != io::ErrorKind::NotFound { - log::warn!("{}:{:?}", id, e); - } - if !self.route_table.use_channel_type.is_only_p2p() && send_default { - //符合条件再发到服务器转发 - self.send_default(buf, server_addr)?; - } - } - Ok(()) - } - /// 将数据发到指定id - pub fn send_by_id>(&self, buf: &NetPacket, id: &Ipv4Addr) -> io::Result<()> { - let mut c = 0; - loop { - let route = self.route_table.get_route_by_id(c, id)?; - return if let Err(e) = self.send_by_key(buf, route.route_key()) { - //降低发送速率 - if e.kind() == io::ErrorKind::WouldBlock { - c += 1; - if c < 10 { - thread::sleep(Duration::from_micros(200)); - continue; - } - } - Err(e) - } else { - Ok(()) - }; - } - } - /// 将数据发到指定路由 - pub fn send_by_key>( - &self, - buf: &NetPacket, - route_key: RouteKey, - ) -> io::Result<()> { - match route_key.protocol() { - ConnectProtocol::UDP => { - if let Some(main_udp) = self.main_udp_socket.get(route_key.index) { - main_udp.send_to(buf.buffer(), route_key.addr)?; - } else { - if let Some(udp) = self - .sub_udp_socket - .read() - .get(route_key.index - self.main_len()) - { - udp.send_to(buf.buffer(), route_key.addr)?; - } else { - Err(io::Error::from(io::ErrorKind::NotFound))? - } - } - } - ConnectProtocol::TCP | ConnectProtocol::WS | ConnectProtocol::WSS => { - self.send_tcp(buf.buffer(), &route_key)? - } - } - if let Some(up_traffic_meter) = &self.up_traffic_meter { - up_traffic_meter.add_traffic(buf.destination(), buf.data_len()); - } - Ok(()) - } - pub fn remove_route(&self, ip: &Ipv4Addr, route_key: RouteKey) { - self.route_table.remove_route(ip, route_key) - } -} - -pub struct RouteTable { - pub(crate) route_table: - RwLock)>)>>, - first_latency: bool, - channel_num: usize, - use_channel_type: UseChannelType, -} - -impl RouteTable { - fn new(use_channel_type: UseChannelType, first_latency: bool, channel_num: usize) -> Self { - Self { - route_table: RwLock::new(FnvHashMap::with_capacity_and_hasher(64, Default::default())), - use_channel_type, - first_latency, - channel_num, - } - } -} - -impl RouteTable { - fn get_route_by_id(&self, index: usize, id: &Ipv4Addr) -> io::Result { - if let Some((_count, v)) = self.route_table.read().get(id) { - if self.first_latency { - if let Some((route, _)) = v.first() { - return Ok(*route); - } - } else { - let len = v.len(); - if len != 0 { - let route = &v[index % len].0; - // 跳过默认rt的路由(一般是刚加入的),这有助于提升稳定性 - if route.rt != DEFAULT_RT { - return Ok(*route); - } - for (route, _) in v { - if route.rt != DEFAULT_RT { - return Ok(*route); - } - } - } - } - } - Err(io::Error::new(io::ErrorKind::NotFound, "route not found")) - } - pub fn add_route_if_absent(&self, id: Ipv4Addr, route: Route) -> bool { - self.add_route_(id, route, true) - } - pub fn add_route(&self, id: Ipv4Addr, route: Route) -> bool { - self.add_route_(id, route, false) - } - fn add_route_(&self, id: Ipv4Addr, route: Route, only_if_absent: bool) -> bool { - // 限制通道类型 - match self.use_channel_type { - UseChannelType::P2p => { - if !route.is_p2p() { - return false; - } - } - _ => {} - } - let key = route.route_key(); - if only_if_absent { - if let Some((_, list)) = self.route_table.read().get(&id) { - for (x, _) in list { - if x.route_key() == key { - return true; - } - } - } - } - let mut route_table = self.route_table.write(); - let (_, list) = route_table - .entry(id) - .or_insert_with(|| (AtomicUsize::new(0), Vec::with_capacity(4))); - let mut exist = false; - for (x, time) in list.iter_mut() { - if x.metric < route.metric && !self.first_latency { - //非优先延迟的情况下 不能比当前的路径更长 - return false; - } - if x.route_key() == key { - if only_if_absent { - return true; - } - x.metric = route.metric; - x.rt = route.rt; - exist = true; - time.store(Instant::now()); - break; - } - } - if exist { - list.sort_by_key(|(k, _)| k.rt); - } else { - if !self.first_latency { - if route.is_p2p() { - //非优先延迟的情况下 添加了直连的则排除非直连的 - list.retain(|(k, _)| k.is_p2p()); - } - }; - list.sort_by_key(|(k, _)| k.rt); - list.push((route, AtomicCell::new(Instant::now()))); - } - return true; - } - // 直接移除会导致通道不稳定,所以废弃这个方法,后面改用多余通道不发心跳包,从而让通道自动过期 - // fn truncate_(&self, list: &mut Vec<(Route, AtomicCell)>, len: usize) { - // if list.len() <= len { - // return; - // } - // if self.first_latency { - // //找到第一个p2p通道 - // if let Some(index) = - // list.iter() - // .enumerate() - // .find_map(|(index, (route, _))| if route.is_p2p() { Some(index) } else { None }) - // { - // if index >= len { - // //保留第一个p2p通道 - // let route = list.remove(index); - // list.truncate(len - 1); - // list.push(route); - // return; - // } - // } - // } - // list.truncate(len); - // } - pub fn route(&self, id: &Ipv4Addr) -> Option> { - if let Some((_, v)) = self.route_table.read().get(id) { - Some(v.iter().map(|(i, _)| *i).collect()) - } else { - None - } - } - pub fn route_one(&self, id: &Ipv4Addr) -> Option { - if let Some((_, v)) = self.route_table.read().get(id) { - v.first().map(|(i, _)| *i) - } else { - None - } - } - pub fn route_one_p2p(&self, id: &Ipv4Addr) -> Option { - if let Some((_, v)) = self.route_table.read().get(id) { - for (i, _) in v { - if i.is_p2p() { - return Some(*i); - } - } - } - None - } - pub fn route_to_id(&self, route_key: &RouteKey) -> Option { - let table = self.route_table.read(); - for (k, (_, v)) in table.iter() { - for (route, _) in v { - if &route.route_key() == route_key && route.is_p2p() { - return Some(*k); - } - } - } - None - } - pub fn no_need_punch(&self, id: &Ipv4Addr) -> bool { - if let Some((_, v)) = self.route_table.read().get(id) { - //p2p的通道数符合要求 - return v.iter().filter(|(k, _)| k.is_p2p()).count() >= self.channel_num; - } - false - } - pub fn p2p_num(&self, id: &Ipv4Addr) -> usize { - if let Some((_, v)) = self.route_table.read().get(id) { - v.iter().filter(|(k, _)| k.is_p2p()).count() - } else { - 0 - } - } - /// 返回所有路由 - pub fn route_table(&self) -> Vec<(Ipv4Addr, Vec)> { - let table = self.route_table.read(); - table - .iter() - .map(|(k, (_, v))| (k.clone(), v.iter().map(|(i, _)| *i).collect())) - .collect() - } - pub fn route_table_p2p(&self) -> Vec<(Ipv4Addr, Route)> { - let table = self.route_table.read(); - let mut list = Vec::with_capacity(8); - for (ip, (_, routes)) in table.iter() { - for (route, _) in routes.iter() { - if route.is_p2p() { - list.push((*ip, *route)); - break; - } - } - } - list - } - pub fn route_table_one(&self) -> Vec<(Ipv4Addr, Route)> { - let mut list = Vec::with_capacity(8); - let table = self.route_table.read(); - for (k, (_, v)) in table.iter() { - if let Some((route, _)) = v.first() { - list.push((*k, *route)); - } - } - list - } - pub fn remove_route(&self, id: &Ipv4Addr, route_key: RouteKey) { - let mut write_guard = self.route_table.write(); - if let Some((_, routes)) = write_guard.get_mut(id) { - routes.retain(|(x, _)| x.route_key() != route_key); - if routes.is_empty() { - write_guard.remove(id); - } - } - } - /// 更新路由入栈包的时刻,长时间没有收到数据的路由将会被剔除 - pub fn update_read_time(&self, id: &Ipv4Addr, route_key: &RouteKey) { - if let Some((_, routes)) = self.route_table.read().get(id) { - for (route, time) in routes { - if &route.route_key() == route_key { - time.store(Instant::now()); - break; - } - } - } - } -} diff --git a/vnt/src/channel/handler.rs b/vnt/src/channel/handler.rs deleted file mode 100644 index 7c6abe7..0000000 --- a/vnt/src/channel/handler.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::channel::context::ChannelContext; -use crate::channel::RouteKey; - -pub trait RecvChannelHandler: Clone + Send + Sync + 'static { - fn handle( - &self, - buf: &mut [u8], - extend: &mut [u8], - route_key: RouteKey, - context: &ChannelContext, - ); -} diff --git a/vnt/src/channel/idle.rs b/vnt/src/channel/idle.rs deleted file mode 100644 index c145819..0000000 --- a/vnt/src/channel/idle.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::net::Ipv4Addr; -use std::time::Duration; - -use crate::channel::context::ChannelContext; -use crate::channel::Route; - -pub struct Idle { - read_idle: Duration, - context: ChannelContext, -} - -impl Idle { - pub fn new(read_idle: Duration, context: ChannelContext) -> Self { - Self { read_idle, context } - } -} - -pub enum IdleType { - Timeout(Ipv4Addr, Route), - Sleep(Duration), - None, -} - -impl Idle { - /// 获取空闲路由 - pub fn next_idle(&self) -> IdleType { - let mut max = Duration::from_secs(0); - let read_guard = self.context.route_table.route_table.read(); - if read_guard.is_empty() { - return IdleType::None; - } - for (ip, (_, routes)) in read_guard.iter() { - for (route, time) in routes { - let last_read = time.load().elapsed(); - if last_read >= self.read_idle { - return IdleType::Timeout(*ip, *route); - } else if max < last_read { - max = last_read; - } - } - } - let sleep_time = self.read_idle.checked_sub(max).unwrap_or_default(); - return IdleType::Sleep(sleep_time); - } -} diff --git a/vnt/src/channel/mod.rs b/vnt/src/channel/mod.rs deleted file mode 100644 index 3c62424..0000000 --- a/vnt/src/channel/mod.rs +++ /dev/null @@ -1,365 +0,0 @@ -use anyhow::Context; -use std::net::{SocketAddr, UdpSocket}; -use std::str::FromStr; -use tokio::sync::mpsc::channel; - -use crate::channel::context::ChannelContext; -use crate::channel::handler::RecvChannelHandler; -use crate::channel::sender::{AcceptSocketSender, ConnectUtil}; -use crate::channel::socket::{bind_udp, LocalInterface}; -use crate::channel::tcp_channel::tcp_listen; -use crate::channel::udp_channel::udp_listen; -#[cfg(feature = "ws")] -use crate::channel::ws_channel::ws_connect_accept; -use crate::util::limit::TrafficMeterMultiAddress; -use crate::util::StopManager; - -pub mod context; -pub mod handler; -pub mod idle; -pub mod notify; -pub mod punch; -pub mod sender; -pub mod socket; -pub mod tcp_channel; -pub mod udp_channel; -#[cfg(feature = "ws")] -pub mod ws_channel; - -pub const BUFFER_SIZE: usize = 1024 * 64; -// 这里留个坑,tcp是支持_TCP_MAX_PACKET_SIZE长度的, -// 但是缓存只用BUFFER_SIZE,会导致多余的数据接收不了 -const TCP_MAX_PACKET_SIZE: usize = (1 << 24) - 1; - -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum UseChannelType { - Relay, - P2p, - All, -} - -impl UseChannelType { - pub fn is_only_relay(&self) -> bool { - self == &UseChannelType::Relay - } - pub fn is_only_p2p(&self) -> bool { - self == &UseChannelType::P2p - } - pub fn is_all(&self) -> bool { - self == &UseChannelType::All - } -} - -impl FromStr for UseChannelType { - type Err = String; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().trim() { - "relay" => Ok(UseChannelType::Relay), - "p2p" => Ok(UseChannelType::P2p), - "all" => Ok(UseChannelType::All), - _ => Err(format!("not match '{}', enum: relay/p2p/all", s)), - } - } -} - -impl Default for UseChannelType { - fn default() -> Self { - UseChannelType::All - } -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum ConnectProtocol { - UDP, - TCP, - WS, - WSS, -} - -impl ConnectProtocol { - #[inline] - pub fn is_tcp(&self) -> bool { - self == &ConnectProtocol::TCP - } - #[inline] - pub fn is_udp(&self) -> bool { - self == &ConnectProtocol::UDP - } - #[inline] - pub fn is_ws(&self) -> bool { - self == &ConnectProtocol::WS - } - #[inline] - pub fn is_wss(&self) -> bool { - self == &ConnectProtocol::WSS - } - pub fn is_transport(&self) -> bool { - self.is_tcp() || self.is_udp() - } - pub fn is_base_tcp(&self) -> bool { - self.is_tcp() || self.is_ws() || self.is_wss() - } -} - -#[derive(Copy, Clone, Debug)] -pub struct Route { - pub protocol: ConnectProtocol, - index: usize, - pub addr: SocketAddr, - pub metric: u8, - pub rt: i64, -} - -#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)] -pub struct RouteSortKey { - pub metric: u8, - pub rt: i64, -} - -const DEFAULT_RT: i64 = 9999; - -impl Route { - pub fn new( - protocol: ConnectProtocol, - index: usize, - addr: SocketAddr, - metric: u8, - rt: i64, - ) -> Self { - Self { - protocol, - index, - addr, - metric, - rt, - } - } - pub fn from(route_key: RouteKey, metric: u8, rt: i64) -> Self { - Self { - protocol: route_key.protocol, - index: route_key.index, - addr: route_key.addr, - metric, - rt, - } - } - pub fn from_default_rt(route_key: RouteKey, metric: u8) -> Self { - Self { - protocol: route_key.protocol, - index: route_key.index, - addr: route_key.addr, - metric, - rt: DEFAULT_RT, - } - } - pub fn route_key(&self) -> RouteKey { - RouteKey { - protocol: self.protocol, - index: self.index, - addr: self.addr, - } - } - pub fn sort_key(&self) -> RouteSortKey { - RouteSortKey { - metric: self.metric, - rt: self.rt, - } - } - pub fn is_p2p(&self) -> bool { - self.metric == 1 - } -} - -#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)] -pub struct RouteKey { - protocol: ConnectProtocol, - index: usize, - pub addr: SocketAddr, -} - -impl RouteKey { - pub(crate) const fn new(protocol: ConnectProtocol, index: usize, addr: SocketAddr) -> Self { - Self { - protocol, - index, - addr, - } - } - #[inline] - pub fn protocol(&self) -> ConnectProtocol { - self.protocol - } - #[inline] - pub fn index(&self) -> usize { - self.index - } -} - -pub(crate) fn init_context( - ports: Vec, - use_channel_type: UseChannelType, - first_latency: bool, - protocol: ConnectProtocol, - packet_loss_rate: Option, - packet_delay: u32, - default_interface: LocalInterface, - up_traffic_meter: Option, - down_traffic_meter: Option, -) -> anyhow::Result<(ChannelContext, std::net::TcpListener)> { - assert!(!ports.is_empty(), "not channel"); - let mut main_udp_socket_v4 = Vec::with_capacity(ports.len()); - let mut main_udp_socket_v6 = Vec::with_capacity(ports.len()); - //检查系统是否支持ipv6 - let use_ipv6 = match socket2::Socket::new(socket2::Domain::IPV6, socket2::Type::DGRAM, None) { - Ok(_) => true, - Err(e) => { - log::warn!("{:?}", e); - false - } - }; - for port in &ports { - let addr_v4: SocketAddr = format!("0.0.0.0:{}", port).parse().unwrap(); - if use_ipv6 { - let (main_channel_v4, main_channel_v6) = bind_udp_v4_and_v6(*port, &default_interface)?; - main_udp_socket_v4.push(main_channel_v4); - main_udp_socket_v6.push(main_channel_v6); - } else { - let socket = bind_udp(addr_v4, &default_interface)?; - let main_channel_v4: UdpSocket = socket.into(); - main_udp_socket_v4.push(main_channel_v4); - } - } - let mut main_udp_socket = - Vec::with_capacity(main_udp_socket_v4.len() + main_udp_socket_v6.len()); - let v4_len = main_udp_socket_v4.len(); - main_udp_socket.append(&mut main_udp_socket_v4); - main_udp_socket.append(&mut main_udp_socket_v6); - let context = ChannelContext::new( - main_udp_socket, - v4_len, - use_channel_type, - first_latency, - protocol, - packet_loss_rate, - packet_delay, - up_traffic_meter, - down_traffic_meter, - default_interface, - ); - - let port = context.main_local_udp_port()?[0]; - //监听v6+v4双栈,tcp通道使用异步io - let (socket, address) = if use_ipv6 { - let address: SocketAddr = format!("[::]:{}", port).parse().unwrap(); - let socket = socket2::Socket::new(socket2::Domain::IPV6, socket2::Type::STREAM, None)?; - socket - .set_only_v6(false) - .with_context(|| format!("set_only_v6 failed: {}", &address))?; - (socket, address) - } else { - let address: SocketAddr = format!("0.0.0.0:{}", port).parse().unwrap(); - let socket = socket2::Socket::new(socket2::Domain::IPV4, socket2::Type::STREAM, None)?; - (socket, address) - }; - socket - .set_reuse_address(true) - .context("set_reuse_address")?; - #[cfg(unix)] - if let Err(e) = socket.set_reuse_port(true) { - log::warn!("set_reuse_port {:?}", e) - } - if let Err(e) = socket.bind(&address.into()) { - if ports[0] == 0 { - //端口可能冲突,则使用任意端口 - log::warn!("监听tcp端口失败 {:?},重试一次", address); - let address: SocketAddr = if use_ipv6 { - format!("[::]:{}", 0).parse().unwrap() - } else { - format!("0.0.0.0:{}", port).parse().unwrap() - }; - socket - .bind(&address.into()) - .with_context(|| format!("bind failed: {}", &address))?; - } else { - //手动指定的ip,直接报错 - Err(anyhow::anyhow!("{:?},bind failed: {}", e, address))?; - } - } - socket.listen(128)?; - socket.set_nonblocking(true)?; - socket.set_nodelay(true)?; - Ok((context, socket.into())) -} -fn bind_udp_v4_and_v6( - port: u16, - default_interface: &LocalInterface, -) -> anyhow::Result<(UdpSocket, UdpSocket)> { - let mut count = 0; - loop { - let addr_v4: SocketAddr = format!("0.0.0.0:{}", port).parse().unwrap(); - let socket = bind_udp(addr_v4, default_interface)?; - if let Err(e) = socket.set_recv_buffer_size(2 * 1024 * 1024) { - log::warn!("set_recv_buffer_size {:?}", e); - } - let main_channel_v4: UdpSocket = socket.into(); - let addr = main_channel_v4.local_addr()?; - let addr_v6: SocketAddr = format!("[::]:{}", addr.port()).parse().unwrap(); - let socket = if port == 0 { - match bind_udp(addr_v6, default_interface) { - Ok(socket) => socket, - Err(e) => { - if count > 10 { - return Err(e); - } - if let Some(e) = e.downcast_ref::() { - if e.kind() == std::io::ErrorKind::AddrInUse { - count += 1; - continue; - } - } - Err(e)? - } - } - } else { - bind_udp(addr_v6, default_interface)? - }; - if let Err(e) = socket.set_recv_buffer_size(2 * 1024 * 1024) { - log::warn!("set_recv_buffer_size {:?}", e); - } - let main_channel_v6: UdpSocket = socket.into(); - return Ok((main_channel_v4, main_channel_v6)); - } -} - -pub(crate) fn init_channel( - tcp_listener: std::net::TcpListener, - context: ChannelContext, - stop_manager: StopManager, - recv_handler: H, -) -> anyhow::Result<( - AcceptSocketSender>>, - ConnectUtil, -)> -where - H: RecvChannelHandler, -{ - let (tcp_connect_s, tcp_connect_r) = channel(16); - let (ws_connect_s, _ws_connect_r) = channel(16); - let connect_util = ConnectUtil::new(tcp_connect_s, ws_connect_s); - // udp监听,udp_socket_sender 用于NAT类型切换 - let udp_socket_sender = - udp_listen(stop_manager.clone(), recv_handler.clone(), context.clone())?; - // 建立tcp监听,tcp_socket_sender 用于tcp 直连 - tcp_listen( - tcp_listener, - tcp_connect_r, - recv_handler.clone(), - context.clone(), - stop_manager.clone(), - )?; - #[cfg(feature = "ws")] - ws_connect_accept(_ws_connect_r, recv_handler, context.clone(), stop_manager)?; - - Ok((udp_socket_sender, connect_util)) -} diff --git a/vnt/src/channel/notify.rs b/vnt/src/channel/notify.rs deleted file mode 100644 index 5a6dc57..0000000 --- a/vnt/src/channel/notify.rs +++ /dev/null @@ -1,126 +0,0 @@ -use mio::{Token, Waker}; -use parking_lot::Mutex; -use std::io; -use std::ops::Deref; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; - -#[derive(Clone)] -pub struct WritableNotify { - inner: Arc, -} - -impl WritableNotify { - pub fn new(waker: Waker) -> Self { - Self { - inner: Arc::new(WritableNotifyInner { - waker, - state: AtomicUsize::new(0), - tokens: Mutex::new(Vec::with_capacity(8)), - }), - } - } -} - -impl Deref for WritableNotify { - type Target = WritableNotifyInner; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -pub struct WritableNotifyInner { - waker: Waker, - state: AtomicUsize, - tokens: Mutex>, -} - -impl WritableNotifyInner { - pub fn notify(&self, token: Token, state: bool) -> io::Result<()> { - { - let mut guard = self.tokens.lock(); - if guard.is_empty() || !guard.contains(&(token, state)) { - guard.push((token, state)); - } - drop(guard); - } - self.need_write() - } - - pub fn stop(&self) -> io::Result<()> { - self.state.store(0b001, Ordering::Release); - self.waker.wake() - } - pub fn need_write(&self) -> io::Result<()> { - self.state.fetch_or(0b010, Ordering::AcqRel); - self.waker.wake() - } - pub fn add_socket(&self) -> io::Result<()> { - self.state.fetch_or(0b100, Ordering::AcqRel); - self.waker.wake() - } - pub fn take_all(&self) -> Option> { - let mut guard = self.tokens.lock(); - if guard.is_empty() { - None - } else { - Some(guard.drain(..).collect()) - } - } - pub fn is_stop(&self) -> bool { - self.state.load(Ordering::Acquire) & 0b001 == 0b001 - } - pub fn is_need_write(&self) -> bool { - self.state.fetch_and(!0b010, Ordering::AcqRel) & 0b010 == 0b010 - } - pub fn is_add_socket(&self) -> bool { - self.state.fetch_and(!0b100, Ordering::AcqRel) & 0b100 == 0b100 - } -} - -#[derive(Clone)] -pub struct AcceptNotify { - inner: Arc, -} - -impl AcceptNotify { - pub fn new(waker: Waker) -> Self { - Self { - inner: Arc::new(AcceptNotifyInner { - waker, - state: AtomicUsize::new(0), - }), - } - } -} - -impl Deref for AcceptNotify { - type Target = AcceptNotifyInner; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -pub struct AcceptNotifyInner { - waker: Waker, - state: AtomicUsize, -} - -impl AcceptNotifyInner { - pub fn is_stop(&self) -> bool { - self.state.load(Ordering::Acquire) & 0b001 == 0b001 - } - pub fn is_add_socket(&self) -> bool { - self.state.fetch_and(!0b100, Ordering::AcqRel) & 0b100 == 0b100 - } - pub fn stop(&self) -> io::Result<()> { - self.state.store(0b001, Ordering::Release); - self.waker.wake() - } - pub fn add_socket(&self) -> io::Result<()> { - self.state.fetch_or(0b100, Ordering::AcqRel); - self.waker.wake() - } -} diff --git a/vnt/src/channel/punch.rs b/vnt/src/channel/punch.rs deleted file mode 100644 index 3775134..0000000 --- a/vnt/src/channel/punch.rs +++ /dev/null @@ -1,508 +0,0 @@ -use std::collections::HashMap; -use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; -use std::ops::{Div, Mul}; -use std::str::FromStr; -use std::sync::Arc; -use std::time::Duration; -use std::{io, thread}; - -use crossbeam_utils::atomic::AtomicCell; -use rand::prelude::SliceRandom; -use rand::Rng; - -use crate::channel::context::ChannelContext; -use crate::channel::sender::ConnectUtil; -use crate::handle::CurrentDeviceInfo; -use crate::nat::{is_ipv4_global, NatTest}; -use crate::proto::message::{PunchNatModel, PunchNatType}; - -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub enum PunchModel { - All, - IPv4, - IPv6, - IPv4Tcp, - IPv4Udp, - IPv6Tcp, - IPv6Udp, -} - -impl PunchModel { - pub fn use_tcp(&self) -> bool { - self != &PunchModel::IPv4Udp && self != &PunchModel::IPv6Udp - } - pub fn use_udp(&self) -> bool { - self != &PunchModel::IPv4Tcp && self != &PunchModel::IPv6Tcp - } - pub fn use_ipv6(&self) -> bool { - self == &PunchModel::All - || self == &PunchModel::IPv6 - || self == &PunchModel::IPv6Tcp - || self == &PunchModel::IPv6Udp - } - pub fn use_ipv4(&self) -> bool { - self == &PunchModel::All - || self == &PunchModel::IPv4 - || self == &PunchModel::IPv4Tcp - || self == &PunchModel::IPv4Udp - } -} - -impl FromStr for PunchModel { - type Err = String; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().trim() { - "ipv4" => Ok(PunchModel::IPv4), - "ipv6" => Ok(PunchModel::IPv6), - "ipv4-tcp" => Ok(PunchModel::IPv4Tcp), - "ipv4-udp" => Ok(PunchModel::IPv4Udp), - "ipv6-tcp" => Ok(PunchModel::IPv6Tcp), - "ipv6-udp" => Ok(PunchModel::IPv6Udp), - "all" => Ok(PunchModel::All), - _ => Err(format!( - "not match '{}', enum: ipv4/ipv4-tcp/ipv4-udp/ipv6/ipv6-tcp/ipv6-udp/all", - s - )), - } - } -} - -impl Default for PunchModel { - fn default() -> Self { - PunchModel::All - } -} -impl From for PunchNatModel { - fn from(value: PunchModel) -> Self { - match value { - PunchModel::All => PunchNatModel::All, - PunchModel::IPv4 => PunchNatModel::IPv4, - PunchModel::IPv6 => PunchNatModel::IPv6, - PunchModel::IPv4Tcp => PunchNatModel::IPv4Tcp, - PunchModel::IPv4Udp => PunchNatModel::IPv4Udp, - PunchModel::IPv6Tcp => PunchNatModel::IPv6Tcp, - PunchModel::IPv6Udp => PunchNatModel::IPv6Udp, - } - } -} - -impl Into for PunchNatModel { - fn into(self) -> PunchModel { - match self { - PunchNatModel::All => PunchModel::All, - PunchNatModel::IPv4 => PunchModel::IPv4, - PunchNatModel::IPv6 => PunchModel::IPv6, - PunchNatModel::IPv4Tcp => PunchModel::IPv4Tcp, - PunchNatModel::IPv4Udp => PunchModel::IPv4Udp, - PunchNatModel::IPv6Tcp => PunchModel::IPv6Tcp, - PunchNatModel::IPv6Udp => PunchModel::IPv6Udp, - } - } -} - -#[derive(Clone, Debug)] -pub struct NatInfo { - pub public_ips: Vec, - pub public_ports: Vec, - pub public_port_range: u16, - pub nat_type: NatType, - pub(crate) local_ipv4: Option, - pub(crate) ipv6: Option, - pub udp_ports: Vec, - pub tcp_port: u16, - pub public_tcp_port: u16, - pub punch_model: PunchModel, -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] -pub enum NatType { - Symmetric, - Cone, -} - -impl NatType { - pub fn is_cone(&self) -> bool { - self == &NatType::Cone - } -} -impl From for PunchNatType { - fn from(value: NatType) -> Self { - match value { - NatType::Symmetric => PunchNatType::Symmetric, - NatType::Cone => PunchNatType::Cone, - } - } -} - -impl Into for PunchNatType { - fn into(self) -> NatType { - match self { - PunchNatType::Symmetric => NatType::Symmetric, - PunchNatType::Cone => NatType::Cone, - } - } -} - -impl NatInfo { - pub fn new( - mut public_ips: Vec, - public_ports: Vec, - public_port_range: u16, - mut local_ipv4: Option, - mut ipv6: Option, - udp_ports: Vec, - tcp_port: u16, - public_tcp_port: u16, - mut nat_type: NatType, - punch_model: PunchModel, - ) -> Self { - public_ips.retain(|ip| { - !ip.is_multicast() - && !ip.is_broadcast() - && !ip.is_unspecified() - && !ip.is_loopback() - && !ip.is_private() - }); - if public_ips.len() > 1 { - nat_type = NatType::Symmetric; - } - if let Some(ip) = local_ipv4 { - if ip.is_multicast() || ip.is_broadcast() || ip.is_unspecified() || ip.is_loopback() { - local_ipv4 = None - } - } - if let Some(ip) = ipv6 { - if ip.is_multicast() || ip.is_unspecified() || ip.is_loopback() { - ipv6 = None - } - } - Self { - public_ips, - public_ports, - public_port_range, - local_ipv4, - ipv6, - udp_ports, - tcp_port, - public_tcp_port, - nat_type, - punch_model, - } - } - pub fn update_addr(&mut self, index: usize, ip: Ipv4Addr, port: u16) -> bool { - let mut updated = false; - if port != 0 { - if let Some(public_port) = self.public_ports.get_mut(index) { - if *public_port != port { - updated = true; - log::info!("端口变化={}:{} index={}", ip, port, index) - } - *public_port = port; - } - } - if is_ipv4_global(&ip) { - if !self.public_ips.contains(&ip) { - self.public_ips.push(ip); - updated = true; - log::info!("ip变化={},{:?}", ip, self.public_ips) - } - } - updated - } - pub fn update_tcp_port(&mut self, port: u16) { - self.public_tcp_port = port; - } - pub fn local_ipv4(&self) -> Option { - self.local_ipv4 - } - pub fn ipv6(&self) -> Option { - self.ipv6 - } - pub fn local_udp_ipv4addr(&self, index: usize) -> Option { - let len = self.udp_ports.len(); - if len == 0 { - return None; - } - if let Some(local_ipv4) = self.local_ipv4 { - Some(SocketAddr::V4(SocketAddrV4::new( - local_ipv4, - self.udp_ports[index % len], - ))) - } else { - None - } - } - pub fn local_udp_ipv6addr(&self, index: usize) -> Option { - let len = self.udp_ports.len(); - if len == 0 { - return None; - } - if let Some(ipv6) = self.ipv6 { - Some(SocketAddr::V6(SocketAddrV6::new( - ipv6, - self.udp_ports[index % len], - 0, - 0, - ))) - } else { - None - } - } - - pub fn local_tcp_ipv6addr(&self) -> Option { - if self.tcp_port == 0 { - return None; - } - if let Some(ipv6) = self.ipv6 { - Some(SocketAddr::V6(SocketAddrV6::new(ipv6, self.tcp_port, 0, 0))) - } else { - None - } - } - pub fn local_tcp_ipv4addr(&self) -> Option { - if self.tcp_port == 0 { - return None; - } - if let Some(ipv4) = self.local_ipv4 { - Some(SocketAddr::V4(SocketAddrV4::new(ipv4, self.tcp_port))) - } else { - None - } - } -} - -#[derive(Clone)] -pub struct Punch { - context: ChannelContext, - port_vec: Vec, - port_index: HashMap, - punch_model: PunchModel, - connect_util: ConnectUtil, - nat_test: NatTest, - current_device: Arc>, -} - -impl Punch { - pub fn new( - context: ChannelContext, - punch_model: PunchModel, - connect_util: ConnectUtil, - nat_test: NatTest, - current_device: Arc>, - ) -> Self { - let mut port_vec: Vec = (1..65535).collect(); - port_vec.push(65535); - let mut rng = rand::thread_rng(); - port_vec.shuffle(&mut rng); - Punch { - context, - port_vec, - port_index: HashMap::new(), - punch_model, - connect_util, - nat_test, - current_device, - } - } -} - -impl Punch { - fn connect_tcp(&self, buf: &[u8], addr: SocketAddr) { - if self.nat_test.is_local_address(true, addr) { - return; - } - if addr.ip().is_unspecified() || addr.port() == 0 { - return; - } - self.connect_util.try_connect_tcp_punch(buf.to_vec(), addr); - } - pub fn punch( - &mut self, - buf: &[u8], - id: Ipv4Addr, - mut nat_info: NatInfo, - punch_tcp: bool, - count: usize, - ) -> io::Result<()> { - if self.context.route_table.no_need_punch(&id) { - log::info!("已打洞成功,无需打洞:{:?}", id); - return Ok(()); - } - let device_info = self.current_device.load(); - - nat_info - .public_ips - .retain(|ip| is_ipv4_global(ip) && device_info.not_in_network(*ip)); - nat_info.public_ports.retain(|port| *port != 0); - nat_info.udp_ports.retain(|port| *port != 0); - - nat_info.local_ipv4 = nat_info - .local_ipv4 - .filter(|ip| device_info.not_in_network(*ip)); - if punch_tcp && self.punch_model.use_tcp() && nat_info.punch_model.use_tcp() { - //向tcp发起连接 - if self.punch_model.use_ipv6() && nat_info.punch_model.use_ipv6() { - if let Some(ipv6_addr) = nat_info.local_tcp_ipv6addr() { - self.connect_tcp(buf, ipv6_addr) - } - } - if self.punch_model.use_ipv4() && nat_info.punch_model.use_ipv4() { - if let Some(ipv4_addr) = nat_info.local_tcp_ipv4addr() { - self.connect_tcp(buf, ipv4_addr) - } - for ip in &nat_info.public_ips { - let addr = SocketAddr::V4(SocketAddrV4::new(*ip, nat_info.tcp_port)); - self.connect_tcp(buf, addr); - } - if nat_info.nat_type.is_cone() && nat_info.public_tcp_port != 0 { - for ip in &nat_info.public_ips { - let addr = SocketAddr::V4(SocketAddrV4::new(*ip, nat_info.public_tcp_port)); - self.connect_tcp(buf, addr); - } - } - } - } - if !self.punch_model.use_udp() || !nat_info.punch_model.use_udp() { - return Ok(()); - } - let channel_num = self.context.channel_num(); - let main_len = self.context.main_len(); - - if self.punch_model.use_ipv6() && nat_info.punch_model.use_ipv6() { - for index in channel_num..main_len { - if let Some(ipv6_addr) = nat_info.local_udp_ipv6addr(index) { - if !self.nat_test.is_local_address(false, ipv6_addr) { - let rs = self.context.send_main_udp(index, buf, ipv6_addr); - log::info!("发送到ipv6地址:{:?},rs={:?} {}", ipv6_addr, rs, id); - } - } - } - } - if !self.punch_model.use_ipv4() || !nat_info.punch_model.use_ipv4() { - return Ok(()); - } - for index in 0..channel_num { - if let Some(ipv4_addr) = nat_info.local_udp_ipv4addr(index) { - if !self.nat_test.is_local_address(false, ipv4_addr) { - let _ = self.context.send_main_udp(index, buf, ipv4_addr); - } - } - } - // 可能是开放了端口的,需要打洞 - for index in 0..channel_num { - for port in &nat_info.udp_ports { - if *port == 0 { - continue; - } - for ip in &nat_info.public_ips { - if ip.is_unspecified() { - continue; - } - let addr = SocketAddrV4::new(*ip, *port); - let _ = self.context.send_main_udp(index, buf, addr.into()); - thread::sleep(Duration::from_millis(3)); - } - } - } - - match nat_info.nat_type { - NatType::Symmetric => { - // 假设对方绑定n个端口,通过NAT对外映射出n个 公网ip:公网端口,自己随机尝试k次的情况下 - // 猜中的概率 p = 1-((65535-n)/65535)*((65535-n-1)/(65535-1))*...*((65535-n-k+1)/(65535-k+1)) - // n取76,k取600,猜中的概率就超过50%了 - // 前提 自己是锥形网络,否则猜中了也通信不了 - - //预测范围内最多发送max_k1个包 - let max_k1 = 60; - //全局最多发送max_k2个包 - let mut max_k2: usize = rand::thread_rng().gen_range(600..800); - if count > 2 { - //递减探测规模 - max_k2 = max_k2.mul(2).div(count).max(max_k1 as usize); - } - let port = nat_info.public_ports.get(0).map(|e| *e).unwrap_or(0); - if nat_info.public_port_range < max_k1 * 3 { - //端口变化不大时,在预测的范围内随机发送 - let min_port = if port > nat_info.public_port_range { - port - nat_info.public_port_range - } else { - 1 - }; - let (max_port, overflow) = port.overflowing_add(nat_info.public_port_range); - let max_port = if overflow { 65535 } else { max_port }; - let k = if max_port - min_port + 1 > max_k1 { - max_k1 as usize - } else { - (max_port - min_port + 1) as usize - }; - let mut nums: Vec = (min_port..=max_port).collect(); - nums.shuffle(&mut rand::thread_rng()); - self.punch_symmetric(&nums[..k], buf, &nat_info.public_ips, max_k1 as usize)?; - } - let start = *self.port_index.entry(id.clone()).or_insert(0); - let mut end = start + max_k2; - if end > self.port_vec.len() { - end = self.port_vec.len(); - } - let mut index = start - + self.punch_symmetric( - &self.port_vec[start..end], - buf, - &nat_info.public_ips, - max_k2, - )?; - if index >= self.port_vec.len() { - index = 0 - } - self.port_index.insert(id, index); - } - NatType::Cone => { - let is_cone = self.context.is_cone(); - 'a: for index in 0..nat_info.public_ports.len().min(channel_num) { - for ip in &nat_info.public_ips { - let port = nat_info.public_ports[index]; - if port == 0 || ip.is_unspecified() { - continue; - } - let addr = SocketAddr::V4(SocketAddrV4::new(*ip, port)); - if is_cone { - self.context.send_main_udp(index, buf, addr)?; - } else { - //只有一方是对称,则对称方要使用全部端口发送数据,符合上述计算的概率 - self.context.try_send_all(buf, addr); - } - thread::sleep(Duration::from_millis(2)); - } - if !is_cone { - //对称网络数据只发一遍 - break 'a; - } - } - } - } - Ok(()) - } - - fn punch_symmetric( - &self, - ports: &[u16], - buf: &[u8], - ips: &Vec, - max: usize, - ) -> io::Result { - let mut count = 0; - for (index, port) in ports.iter().enumerate() { - for pub_ip in ips { - count += 1; - if count == max { - return Ok(index); - } - let addr = SocketAddr::V4(SocketAddrV4::new(*pub_ip, *port)); - self.context.send_main_udp(0, buf, addr)?; - thread::sleep(Duration::from_millis(3)); - } - } - Ok(ports.len()) - } -} diff --git a/vnt/src/channel/sender.rs b/vnt/src/channel/sender.rs deleted file mode 100644 index 00255c8..0000000 --- a/vnt/src/channel/sender.rs +++ /dev/null @@ -1,272 +0,0 @@ -use std::collections::HashMap; -use std::io; -use std::net::{Ipv4Addr, SocketAddr}; -use std::sync::mpsc::{SyncSender, TrySendError}; -use std::sync::Arc; - -use crossbeam_utils::atomic::AtomicCell; -use parking_lot::Mutex; -use tokio::sync::mpsc::Sender; - -use crate::channel::context::ChannelContext; -use crate::channel::notify::AcceptNotify; -use crate::cipher::Cipher; -use crate::compression::Compressor; -use crate::external_route::ExternalRoute; -use crate::handle::{CurrentDeviceInfo, PeerDeviceInfo}; -use crate::protocol; -use crate::protocol::{ip_turn_packet, NetPacket}; - -#[derive(Clone)] -pub struct IpPacketSender { - context: ChannelContext, - current_device: Arc>, - compressor: Compressor, - client_cipher: Cipher, - server_cipher: Cipher, - ip_route: ExternalRoute, - device_map: Arc)>>, - allow_wire_guard: bool, -} - -impl IpPacketSender { - pub fn new( - context: ChannelContext, - current_device: Arc>, - compressor: Compressor, - client_cipher: Cipher, - server_cipher: Cipher, - ip_route: ExternalRoute, - device_map: Arc)>>, - allow_wire_guard: bool, - ) -> Self { - Self { - context, - current_device, - compressor, - client_cipher, - server_cipher, - ip_route, - device_map, - allow_wire_guard, - } - } - pub fn self_virtual_ip(&self) -> Ipv4Addr { - self.current_device.load().virtual_ip - } - pub fn send_ip( - &self, - buf: &mut [u8], - data_len: usize, - auxiliary_buf: &mut [u8], - mut dest_ip: Ipv4Addr, - ) -> anyhow::Result<()> { - let device_info = self.current_device.load(); - let src_ip = device_info.virtual_ip; - if src_ip.is_unspecified() { - return Ok(()); - } - if let Some(v) = self.ip_route.route(&dest_ip) { - dest_ip = v; - } - if dest_ip.is_multicast() { - //广播 - dest_ip = Ipv4Addr::BROADCAST; - } - let mut net_packet = NetPacket::new0(data_len, buf)?; - net_packet.set_default_version(); - net_packet.set_protocol(protocol::Protocol::IpTurn); - net_packet.set_transport_protocol(ip_turn_packet::Protocol::Ipv4.into()); - net_packet.first_set_ttl(6); - net_packet.set_source(src_ip); - net_packet.set_destination(dest_ip); - if self.allow_wire_guard { - if dest_ip.is_broadcast() || dest_ip == device_info.broadcast_ip { - let exists_wg = self - .device_map - .lock() - .1 - .values() - .any(|v| v.status.is_online() && v.wireguard); - if exists_wg { - send_to_wg_broadcast( - &self.context, - &net_packet, - &self.server_cipher, - &device_info, - )?; - } - } else { - let guard = self.device_map.lock(); - if let Some(peer_info) = guard.1.get(&dest_ip) { - if peer_info.wireguard { - if peer_info.status.is_offline() { - return Ok(()); - } - drop(guard); - send_to_wg( - &self.context, - &mut net_packet, - &self.server_cipher, - &device_info, - )?; - return Ok(()); - } - } - } - } - - let mut auxiliary = NetPacket::new(auxiliary_buf)?; - - let mut net_packet = if self.compressor.compress(&net_packet, &mut auxiliary)? { - auxiliary.set_default_version(); - auxiliary.set_protocol(protocol::Protocol::IpTurn); - auxiliary.set_transport_protocol(ip_turn_packet::Protocol::Ipv4.into()); - auxiliary.first_set_ttl(6); - auxiliary.set_source(src_ip); - auxiliary.set_destination(dest_ip); - auxiliary - } else { - net_packet - }; - self.client_cipher.encrypt_ipv4(&mut net_packet)?; - if dest_ip.is_broadcast() || dest_ip == device_info.broadcast_ip { - //走服务端广播 - self.context - .send_default(&net_packet, device_info.connect_server)?; - return Ok(()); - } - - if device_info.not_in_network(dest_ip) { - //不是一个网段的直接忽略 - return Ok(()); - } - self.context.send_ipv4_by_id( - &net_packet, - &dest_ip, - device_info.connect_server, - device_info.status.online(), - )?; - Ok(()) - } -} - -pub fn send_to_wg_broadcast( - sender: &ChannelContext, - net_packet: &NetPacket<&mut [u8]>, - server_cipher: &Cipher, - current_device: &CurrentDeviceInfo, -) -> anyhow::Result<()> { - let mut copy_packet = NetPacket::new0(net_packet.data_len(), [0; 65536])?; - copy_packet.set_default_version(); - copy_packet.set_protocol(protocol::Protocol::IpTurn); - copy_packet.set_transport_protocol(ip_turn_packet::Protocol::WGIpv4.into()); - copy_packet.first_set_ttl(6); - copy_packet.set_source(net_packet.source()); - copy_packet.set_destination(net_packet.destination()); - copy_packet.set_gateway_flag(true); - copy_packet.set_payload(net_packet.payload())?; - server_cipher.encrypt_ipv4(&mut copy_packet)?; - sender.send_default(©_packet, current_device.connect_server)?; - - Ok(()) -} -pub fn send_to_wg( - sender: &ChannelContext, - net_packet: &mut NetPacket<&mut [u8]>, - server_cipher: &Cipher, - current_device: &CurrentDeviceInfo, -) -> anyhow::Result<()> { - net_packet.set_transport_protocol(ip_turn_packet::Protocol::WGIpv4.into()); - net_packet.set_gateway_flag(true); - server_cipher.encrypt_ipv4(net_packet)?; - sender.send_default(&net_packet, current_device.connect_server)?; - - Ok(()) -} - -pub struct AcceptSocketSender { - sender: SyncSender, - notify: AcceptNotify, -} - -impl Clone for AcceptSocketSender { - fn clone(&self) -> Self { - Self { - sender: self.sender.clone(), - notify: self.notify.clone(), - } - } -} - -impl AcceptSocketSender { - pub fn new(notify: AcceptNotify, sender: SyncSender) -> Self { - Self { sender, notify } - } - pub fn try_add_socket(&self, t: T) -> io::Result<()> { - match self.sender.try_send(t) { - Ok(_) => self.notify.add_socket(), - Err(e) => match e { - TrySendError::Full(_) => Err(io::Error::from(io::ErrorKind::WouldBlock)), - TrySendError::Disconnected(_) => Err(io::Error::from(io::ErrorKind::WriteZero)), - }, - } - } -} -#[derive(Clone)] -pub struct PacketSender { - sender: Sender>, -} - -impl PacketSender { - pub fn new(sender: Sender>) -> Self { - Self { sender } - } - pub fn try_send(&self, buf: &[u8]) -> io::Result<()> { - match self.sender.try_send(buf.to_vec()) { - Ok(_) => Ok(()), - Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => Err(io::Error::new( - io::ErrorKind::WouldBlock, - "通道已满,发生丢包", - )), - Err(_) => Err(io::Error::new( - io::ErrorKind::ConnectionRefused, - "通道关闭,发生丢包", - )), - } - } -} - -#[derive(Clone)] -pub struct ConnectUtil { - connect_tcp: Sender<(Vec, Option, SocketAddr)>, - connect_ws: Sender<(Vec, String)>, -} - -impl ConnectUtil { - pub fn new( - connect_tcp: Sender<(Vec, Option, SocketAddr)>, - connect_ws: Sender<(Vec, String)>, - ) -> Self { - Self { - connect_tcp, - connect_ws, - } - } - pub fn try_connect_tcp(&self, buf: Vec, addr: SocketAddr) { - if self.connect_tcp.try_send((buf, None, addr)).is_err() { - log::warn!("try_connect_tcp failed {}", addr); - } - } - pub fn try_connect_tcp_punch(&self, buf: Vec, addr: SocketAddr) { - // 打洞的连接可以绑定随机端口 - if self.connect_tcp.try_send((buf, Some(0), addr)).is_err() { - log::warn!("try_connect_tcp failed {}", addr); - } - } - pub fn try_connect_ws(&self, buf: Vec, addr: String) { - if self.connect_ws.try_send((buf, addr)).is_err() { - log::warn!("try_connect_ws failed"); - } - } -} diff --git a/vnt/src/channel/socket/mod.rs b/vnt/src/channel/socket/mod.rs deleted file mode 100644 index ccd88ae..0000000 --- a/vnt/src/channel/socket/mod.rs +++ /dev/null @@ -1,138 +0,0 @@ -use anyhow::{anyhow, Context}; -use network_interface::{NetworkInterface, NetworkInterfaceConfig}; -use socket2::Protocol; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; - -#[cfg(unix)] -mod unix; -#[cfg(windows)] -mod windows; - -pub trait VntSocketTrait { - fn set_ip_unicast_if(&self, _interface: &LocalInterface) -> anyhow::Result<()> { - Ok(()) - } -} - -#[derive(Clone, Debug, Default)] -pub struct LocalInterface { - index: u32, - #[cfg(unix)] - name: Option, -} - -pub async fn connect_tcp( - addr: SocketAddr, - bind_port: u16, - default_interface: &LocalInterface, -) -> anyhow::Result { - let socket = create_tcp0(addr.is_ipv4(), bind_port, default_interface)?; - Ok(socket.connect(addr).await?) -} -pub fn create_tcp( - v4: bool, - default_interface: &LocalInterface, -) -> anyhow::Result { - create_tcp0(v4, 0, default_interface) -} -pub fn create_tcp0( - v4: bool, - bind_port: u16, - default_interface: &LocalInterface, -) -> anyhow::Result { - let socket = if v4 { - socket2::Socket::new( - socket2::Domain::IPV4, - socket2::Type::STREAM, - Some(Protocol::TCP), - )? - } else { - socket2::Socket::new( - socket2::Domain::IPV6, - socket2::Type::STREAM, - Some(Protocol::TCP), - )? - }; - if v4 { - if let Err(e) = socket.set_ip_unicast_if(default_interface) { - log::warn!("set_ip_unicast_if {:?}", e) - } - } - if bind_port != 0 { - socket - .set_reuse_address(true) - .context("set_reuse_address")?; - #[cfg(unix)] - if let Err(e) = socket.set_reuse_port(true) { - log::warn!("set_reuse_port {:?}", e) - } - if v4 { - let addr: SocketAddr = format!("0.0.0.0:{}", bind_port).parse().unwrap(); - socket.bind(&addr.into())?; - } else { - socket.set_only_v6(true)?; - let addr: SocketAddr = format!("[::]:{}", bind_port).parse().unwrap(); - socket.bind(&addr.into())?; - } - } - socket.set_nonblocking(true)?; - socket.set_nodelay(true)?; - Ok(tokio::net::TcpSocket::from_std_stream(socket.into())) -} -pub fn bind_udp_ops( - addr: SocketAddr, - only_v6: bool, - default_interface: &LocalInterface, -) -> anyhow::Result { - let socket = if addr.is_ipv4() { - let socket = socket2::Socket::new( - socket2::Domain::IPV4, - socket2::Type::DGRAM, - Some(Protocol::UDP), - )?; - if let Err(e) = socket.set_ip_unicast_if(default_interface) { - log::warn!("set_ip_unicast_if {:?}", e) - } - socket - } else { - let socket = socket2::Socket::new( - socket2::Domain::IPV6, - socket2::Type::DGRAM, - Some(Protocol::UDP), - )?; - socket - .set_only_v6(only_v6) - .with_context(|| format!("set_only_v6 failed: {}", &addr))?; - socket - }; - socket.set_nonblocking(true)?; - socket.bind(&addr.into())?; - Ok(socket) -} -pub fn bind_udp( - addr: SocketAddr, - default_interface: &LocalInterface, -) -> anyhow::Result { - bind_udp_ops(addr, true, default_interface).with_context(|| format!("{}", addr)) -} - -pub fn get_interface(dest_name: String) -> anyhow::Result<(LocalInterface, Ipv4Addr)> { - let network_interfaces = NetworkInterface::show()?; - for iface in network_interfaces { - if iface.name == dest_name { - for addr in iface.addr { - if let IpAddr::V4(ip) = addr.ip() { - return Ok(( - LocalInterface { - index: iface.index, - #[cfg(unix)] - name: Some(iface.name), - }, - ip, - )); - } - } - } - } - Err(anyhow!("No network card with name {} found", dest_name)) -} diff --git a/vnt/src/channel/socket/unix.rs b/vnt/src/channel/socket/unix.rs deleted file mode 100644 index a10807f..0000000 --- a/vnt/src/channel/socket/unix.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::channel::socket::{LocalInterface, VntSocketTrait}; -#[cfg(any(target_os = "linux", target_os = "macos"))] -use anyhow::Context; - -#[cfg(target_os = "linux")] -impl VntSocketTrait for socket2::Socket { - fn set_ip_unicast_if(&self, interface: &LocalInterface) -> anyhow::Result<()> { - if let Some(name) = &interface.name { - self.bind_device(Some(name.as_bytes())) - .context("bind_device")?; - } - Ok(()) - } -} -#[cfg(target_os = "macos")] -impl VntSocketTrait for socket2::Socket { - fn set_ip_unicast_if(&self, interface: &LocalInterface) -> anyhow::Result<()> { - if interface.index != 0 { - self.bind_device_by_index_v4(std::num::NonZeroU32::new(interface.index)) - .with_context(|| format!("bind_device_by_index_v4 {:?}", interface))?; - } - Ok(()) - } -} -#[cfg(target_os = "android")] -impl VntSocketTrait for socket2::Socket { - fn set_ip_unicast_if(&self, _interface: &LocalInterface) -> anyhow::Result<()> { - Ok(()) - } -} - -// #[cfg(any(target_os = "linux", target_os = "macos"))] -// pub fn get_best_interface(dest_ip: Ipv4Addr) -> anyhow::Result { -// match get_interface(dest_ip) { -// Ok(iface) => return Ok(iface), -// Err(e) => { -// log::warn!("not find interface e={:?},ip={}", e, dest_ip); -// } -// } -// // 应该再查路由表找到默认路由的 -// Ok(LocalInterface::default()) -// } -// #[cfg(target_os = "android")] -// pub fn get_best_interface(_dest_ip: Ipv4Addr) -> anyhow::Result { -// Ok(LocalInterface::default()) -// } diff --git a/vnt/src/channel/socket/windows.rs b/vnt/src/channel/socket/windows.rs deleted file mode 100644 index 5322099..0000000 --- a/vnt/src/channel/socket/windows.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::mem; -use std::os::windows::io::AsRawSocket; - -use windows_sys::core::PCSTR; -use windows_sys::Win32::Networking::WinSock::{ - htonl, setsockopt, IPPROTO_IP, IP_UNICAST_IF, SOCKET_ERROR, -}; - -use crate::channel::socket::{LocalInterface, VntSocketTrait}; - -impl VntSocketTrait for socket2::Socket { - fn set_ip_unicast_if(&self, interface: &LocalInterface) -> anyhow::Result<()> { - let index = interface.index; - if index == 0 { - return Ok(()); - } - let raw_socket = self.as_raw_socket(); - let result = unsafe { - let best_interface = htonl(index); - setsockopt( - raw_socket as usize, - IPPROTO_IP, - IP_UNICAST_IF, - &best_interface as *const _ as PCSTR, - mem::size_of_val(&best_interface) as i32, - ) - }; - if result == SOCKET_ERROR { - Err(anyhow::anyhow!( - "Failed to set IP_UNICAST_IF: {:?} {}", - std::io::Error::last_os_error(), - index - ))?; - } - Ok(()) - } -} - -// pub fn get_best_interface(dest_ip: Ipv4Addr) -> anyhow::Result { -// // 获取最佳接口 -// let index = unsafe { -// let mut dest: SOCKADDR_IN = mem::zeroed(); -// dest.sin_family = AF_INET as u16; -// dest.sin_addr.S_un.S_addr = u32::from_ne_bytes(dest_ip.octets()); -// -// let mut index: u32 = 0; -// if GetBestInterfaceEx(&dest as *const _ as *mut SOCKADDR, &mut index) != 0 { -// Err(anyhow::anyhow!( -// "Failed to GetBestInterfaceEx: {:?}", -// std::io::Error::last_os_error() -// ))?; -// } -// index -// }; -// Ok(LocalInterface { index }) -// } diff --git a/vnt/src/channel/tcp_channel.rs b/vnt/src/channel/tcp_channel.rs deleted file mode 100644 index 4f6eb52..0000000 --- a/vnt/src/channel/tcp_channel.rs +++ /dev/null @@ -1,212 +0,0 @@ -use anyhow::{anyhow, Context}; -use std::net::SocketAddr; -#[cfg(unix)] -use std::os::unix::io::AsRawFd; -#[cfg(windows)] -use std::os::windows::io::AsRawSocket; -use std::thread; -use std::time::Duration; -use tokio::io::{AsyncReadExt, AsyncWrite, AsyncWriteExt}; -use tokio::net::tcp::OwnedReadHalf; -use tokio::net::{TcpListener, TcpStream}; -use tokio::sync::mpsc::{channel, Receiver}; - -use crate::channel::context::ChannelContext; -use crate::channel::handler::RecvChannelHandler; -use crate::channel::sender::PacketSender; -use crate::channel::socket::create_tcp0; -use crate::channel::{ConnectProtocol, RouteKey, BUFFER_SIZE, TCP_MAX_PACKET_SIZE}; -use crate::util::StopManager; - -/// 监听tcp端口,等待客户端连接 -pub fn tcp_listen( - tcp_server: std::net::TcpListener, - receiver: Receiver<(Vec, Option, SocketAddr)>, - recv_handler: H, - context: ChannelContext, - stop_manager: StopManager, -) -> anyhow::Result<()> -where - H: RecvChannelHandler, -{ - let (stop_sender, stop_receiver) = tokio::sync::oneshot::channel::<()>(); - let worker = stop_manager.add_listener("tcpChannel".into(), move || { - let _ = stop_sender.send(()); - })?; - let bind_port = tcp_server.local_addr()?.port(); - let runtime = tokio::runtime::Builder::new_multi_thread() - .worker_threads(2) - .enable_all() - .build() - .context("tcp tokio runtime build failed")?; - thread::Builder::new() - .name("tcpChannel".into()) - .spawn(move || { - runtime.spawn(async move { - { - let recv_handler = recv_handler.clone(); - let context = context.clone(); - tokio::spawn(async move { - if let Err(e) = tcp_accept(tcp_server, recv_handler, context).await { - log::warn!("tcp_listen {:?}", e); - } - }); - } - tokio::spawn(async move { - connect_tcp_handle(receiver, recv_handler, context, bind_port).await - }); - }); - runtime.block_on(async { - let _ = stop_receiver.await; - }); - runtime.shutdown_background(); - worker.stop_all(); - }) - .context("tcp thread build failed")?; - Ok(()) -} - -async fn connect_tcp_handle( - mut receiver: Receiver<(Vec, Option, SocketAddr)>, - recv_handler: H, - context: ChannelContext, - listener_bind_port: u16, -) where - H: RecvChannelHandler, -{ - while let Some((data, bind_port, addr)) = receiver.recv().await { - let recv_handler = recv_handler.clone(); - let context = context.clone(); - let bind_port = if let Some(bind_port) = bind_port { - bind_port - } else { - listener_bind_port - }; - tokio::spawn(async move { - if let Err(e) = connect_tcp0(data, addr, recv_handler, context, bind_port).await { - log::warn!("连接失败,链接终止:{:?},{:?}", addr, e); - } - }); - } -} - -async fn connect_tcp0( - data: Vec, - addr: SocketAddr, - recv_handler: H, - context: ChannelContext, - bind_port: u16, -) -> anyhow::Result<()> -where - H: RecvChannelHandler, -{ - let socket = if bind_port != 0 { - match create_tcp0(addr.is_ipv4(), bind_port, context.default_interface()) { - Ok(socket) => socket, - Err(e) => { - log::warn!("{:?}", e); - create_tcp0(addr.is_ipv4(), 0, context.default_interface())? - } - } - } else { - create_tcp0(addr.is_ipv4(), 0, context.default_interface())? - }; - let mut stream = tokio::time::timeout(Duration::from_secs(3), socket.connect(addr)).await??; - tcp_write(&mut stream, &data).await?; - - tcp_stream_handle(stream, addr, recv_handler, context).await; - Ok(()) -} - -async fn tcp_accept( - tcp_server: std::net::TcpListener, - recv_handler: H, - context: ChannelContext, -) -> anyhow::Result<()> -where - H: RecvChannelHandler, -{ - let tcp_server = TcpListener::from_std(tcp_server)?; - - loop { - let (stream, addr) = tcp_server.accept().await?; - - tcp_stream_handle(stream, addr, recv_handler.clone(), context.clone()).await; - } -} - -pub async fn tcp_stream_handle( - stream: TcpStream, - addr: SocketAddr, - recv_handler: H, - context: ChannelContext, -) where - H: RecvChannelHandler, -{ - let _ = stream.set_nodelay(true); - let local = stream.local_addr(); - #[cfg(windows)] - let index = stream.as_raw_socket() as usize; - #[cfg(unix)] - let index = stream.as_raw_fd() as usize; - let route_key = RouteKey::new(ConnectProtocol::TCP, index, addr); - let (r, mut w) = stream.into_split(); - let (sender, mut receiver) = channel::>(100); - context - .packet_map - .write() - .insert(route_key, PacketSender::new(sender)); - tokio::spawn(async move { - while let Some(data) = receiver.recv().await { - if let Err(e) = tcp_write(&mut w, &data).await { - log::info!("发送失败,tcp链接终止:{:?},{:?}", addr, e); - break; - } - } - let _ = w.shutdown().await; - }); - tokio::spawn(async move { - if let Err(e) = tcp_read(r, addr, &context, recv_handler, route_key).await { - log::warn!("tcp_read {:?} {local:?}-{addr}", e) - } - context.packet_map.write().remove(&route_key); - }); -} - -async fn tcp_write(w: &mut W, buf: &[u8]) -> anyhow::Result<()> { - let len = buf.len(); - if len > TCP_MAX_PACKET_SIZE { - return Err(anyhow!("超过了tcp的最大长度传输")); - } - w.write_all(&[0, (len >> 16) as u8, (len >> 8) as u8, len as u8]) - .await?; - w.write_all(&buf).await?; - Ok(()) -} - -async fn tcp_read( - mut read: OwnedReadHalf, - addr: SocketAddr, - context: &ChannelContext, - recv_handler: H, - route_key: RouteKey, -) -> anyhow::Result<()> -where - H: RecvChannelHandler, -{ - let mut head = [0; 4]; - let mut buf = [0; BUFFER_SIZE]; - let mut extend = [0; BUFFER_SIZE]; - loop { - read.read_exact(&mut head).await?; - if head[0] != 0 { - return Err(anyhow!("tcp数据流错误 {}", addr)); - } - let len = ((head[1] as usize) << 16) | ((head[2] as usize) << 8) | head[3] as usize; - if len < 12 || len > buf.len() { - return Err(anyhow!("tcp数据长度无效 {}", addr)); - } - read.read_exact(&mut buf[..len]).await?; - recv_handler.handle(&mut buf[..len], &mut extend, route_key, context); - } -} diff --git a/vnt/src/channel/udp_channel.rs b/vnt/src/channel/udp_channel.rs deleted file mode 100644 index 213de42..0000000 --- a/vnt/src/channel/udp_channel.rs +++ /dev/null @@ -1,370 +0,0 @@ -use std::sync::mpsc::{sync_channel, Receiver}; -use std::{io, thread}; - -use mio::event::Source; -use mio::net::UdpSocket; -use mio::{Events, Interest, Poll, Token, Waker}; - -use crate::channel::context::ChannelContext; -use crate::channel::handler::RecvChannelHandler; -use crate::channel::notify::AcceptNotify; -use crate::channel::sender::AcceptSocketSender; -use crate::channel::{ConnectProtocol, RouteKey, BUFFER_SIZE}; -use crate::util::StopManager; - -pub fn udp_listen( - stop_manager: StopManager, - recv_handler: H, - context: ChannelContext, -) -> anyhow::Result>>> -where - H: RecvChannelHandler, -{ - main_udp_listen(stop_manager.clone(), recv_handler.clone(), context.clone())?; - sub_udp_listen(stop_manager, recv_handler, context) -} - -const NOTIFY: Token = Token(0); - -fn sub_udp_listen( - stop_manager: StopManager, - recv_handler: H, - context: ChannelContext, -) -> anyhow::Result>>> -where - H: RecvChannelHandler, -{ - let (udp_sender, udp_receiver) = sync_channel(64); - let poll = Poll::new()?; - let waker = AcceptNotify::new(Waker::new(poll.registry(), NOTIFY)?); - let worker = { - let waker = waker.clone(); - stop_manager.add_listener("sub_udp_listen".into(), move || { - if let Err(e) = waker.stop() { - log::error!("{:?}", e); - } - })? - }; - let accept = AcceptSocketSender::new(waker.clone(), udp_sender); - thread::Builder::new() - .name("subUdp".into()) - .spawn(move || { - if let Err(e) = sub_udp_listen0(poll, recv_handler, context, waker, udp_receiver) { - log::error!("{:?}", e); - } - worker.stop_all(); - })?; - Ok(accept) -} - -fn sub_udp_listen0( - mut poll: Poll, - recv_handler: H, - context: ChannelContext, - accept_notify: AcceptNotify, - accept_receiver: Receiver>>, -) -> io::Result<()> -where - H: RecvChannelHandler, -{ - let mut events = Events::with_capacity(1024); - let mut buf = [0; BUFFER_SIZE]; - let mut extend = [0; BUFFER_SIZE]; - let mut list: Vec = Vec::with_capacity(100); - let main_len = context.main_len(); - loop { - if let Err(e) = poll.poll(&mut events, None) { - crate::ignore_io_interrupted(e)?; - continue; - } - for event in events.iter() { - match event.token() { - NOTIFY => { - if accept_notify.is_stop() { - return Ok(()); - } - if accept_notify.is_add_socket() { - while let Ok(option) = accept_receiver.try_recv() { - match option { - None => { - log::info!("切换成锥形模式"); - for mut udp_socket in list.drain(..) { - if let Err(e) = udp_socket.deregister(poll.registry()) { - log::error!("{:?}", e); - } - } - } - Some(socket_list) => { - for mut udp_socket in list.drain(..) { - if let Err(e) = udp_socket.deregister(poll.registry()) { - log::error!("deregister {:?}", e); - } - } - log::info!("切换成对称模式 监听端口数:{}", socket_list.len()); - for (index, mut udp_socket) in - socket_list.into_iter().enumerate() - { - poll.registry().register( - &mut udp_socket, - Token(index), - Interest::READABLE, - )?; - list.push(udp_socket); - } - } - } - } - } - } - Token(index) => { - if let Some(udp_socket) = list.get(index) { - loop { - match udp_socket.recv_from(&mut buf) { - Ok((len, addr)) => { - recv_handler.handle( - &mut buf[..len], - &mut extend, - RouteKey::new(ConnectProtocol::UDP, index + main_len, addr), - &context, - ); - } - Err(e) => { - if e.kind() == io::ErrorKind::WouldBlock { - break; - } - log::error!("{:?}", e); - } - } - } - } - } - } - } - } -} - -// /// 阻塞 -// fn main_udp_listen( -// stop_manager: StopManager, -// recv_handler: H, -// context: Context, -// ) -> io::Result<()> -// where -// H: RecvChannelHandler, -// { -// for index in 0..context.main_udp_socket.len() { -// let stop_manager = stop_manager.clone(); -// let context = context.clone(); -// let recv_handler = recv_handler.clone(); -// thread::Builder::new() -// .name(format!("mainUdp{}", index)) -// .spawn(move || { -// if let Err(e) = main_udp_listen0(stop_manager, index, recv_handler, context) { -// log::error!("{:?}", e); -// } -// })?; -// } -// Ok(()) -// } -// -// pub fn main_udp_listen0( -// stop_manager: StopManager, -// index: usize, -// mut recv_handler: H, -// context: Context, -// ) -> io::Result<()> -// where -// H: RecvChannelHandler, -// { -// use std::time::Duration; -// let udp_socket = &context.main_udp_socket[index]; -// udp_socket.set_read_timeout(Some(Duration::from_secs(5)))?; -// udp_socket.set_write_timeout(Some(Duration::from_secs(1)))?; -// let local_addr = udp_socket.local_addr()?; -// let worker = stop_manager.add_listener(format!("main_udp_{}", index), move || { -// if let Ok(udp) = std::net::UdpSocket::bind("0.0.0.0:0") { -// let _ = udp.send_to(b"stop", format!("127.0.0.1:{}", local_addr.port())); -// } -// })?; -// -// let mut buf = [0; BUFFER_SIZE]; -// loop { -// match udp_socket.recv_from(&mut buf) { -// Ok((len, addr)) => { -// if &buf[..len] == b"stop" { -// if stop_manager.is_stop() { -// break; -// } -// } -// recv_handler.handle(&mut buf[..len], RouteKey::new(false, index, addr), &context); -// } -// Err(e) => { -// if stop_manager.is_stop() { -// break; -// } -// log::error!("index={},{:?},{}", index, udp_socket.local_addr(), e) -// } -// } -// } -// worker.stop_all(); -// Ok(()) -// } - -/// 非阻塞 -fn main_udp_listen( - stop_manager: StopManager, - recv_handler: H, - context: ChannelContext, -) -> anyhow::Result<()> -where - H: RecvChannelHandler, -{ - use std::sync::Arc; - let poll = Poll::new()?; - let waker = Arc::new(Waker::new(poll.registry(), NOTIFY)?); - let _waker = waker.clone(); - let worker = stop_manager.add_listener("main_udp".into(), move || { - if let Err(e) = waker.wake() { - log::error!("{:?}", e); - } - })?; - thread::Builder::new() - .name("mainUdp".into()) - .spawn(move || { - if let Err(e) = main_udp_listen0(poll, recv_handler, context) { - log::error!("{:?}", e); - } - drop(_waker); - worker.stop_all(); - })?; - Ok(()) -} - -pub fn main_udp_listen0( - mut poll: Poll, - recv_handler: H, - context: ChannelContext, -) -> io::Result<()> -where - H: RecvChannelHandler, -{ - let mut buf = [0; BUFFER_SIZE]; - let mut udps = Vec::with_capacity(context.main_udp_socket.len()); - - for (index, udp) in context.main_udp_socket.iter().enumerate() { - let udp_socket = udp.try_clone()?; - udp_socket.set_nonblocking(true)?; - let mut mio_udp = UdpSocket::from_std(udp_socket); - poll.registry() - .register(&mut mio_udp, Token(index + 1), Interest::READABLE)?; - udps.push(mio_udp); - } - - let mut events = Events::with_capacity(udps.len()); - let mut extend = [0; BUFFER_SIZE]; - loop { - if let Err(e) = poll.poll(&mut events, None) { - crate::ignore_io_interrupted(e)?; - continue; - } - for x in events.iter() { - let index = match x.token() { - NOTIFY => return Ok(()), - // 0的位置留给NOTIFY了,这里要再减回去,因为路由是通过index来找到对应udp的 - Token(index) => index - 1, - }; - let udp = if let Some(udp) = udps.get(index) { - udp - } else { - log::error!("{:?}", x); - continue; - }; - loop { - match udp.recv_from(&mut buf) { - Ok((len, addr)) => { - recv_handler.handle( - &mut buf[..len], - &mut extend, - RouteKey::new(ConnectProtocol::UDP, index, addr), - &context, - ); - } - Err(e) => { - if e.kind() == io::ErrorKind::WouldBlock { - break; - } - log::error!("main_udp_listen_{}={:?}", index, e); - } - } - } - } - } -} -// /// 用recvmmsg没什么帮助,这里记录下,以下是完整代码 -// #[cfg(unix)] -// pub fn main_udp_listen0(index: usize, mut recv_handler: H, context: Context) -> io::Result<()> -// where -// H: RecvChannelHandler, -// { -// use libc::{c_uint, mmsghdr, sockaddr_storage, socklen_t, timespec}; -// use std::os::fd::AsRawFd; -// -// let udp_socket = context.main_udp_socket[index].try_clone()?; -// let fd = udp_socket.as_raw_fd(); -// const MAX_MESSAGES: usize = 16; -// let mut iov: [libc::iovec; MAX_MESSAGES] = unsafe { std::mem::zeroed() }; -// let mut buf: [[u8; BUFFER_SIZE]; MAX_MESSAGES] = [[0; BUFFER_SIZE]; MAX_MESSAGES]; -// let mut msgs: [mmsghdr; MAX_MESSAGES] = unsafe { std::mem::zeroed() }; -// let mut addrs: [sockaddr_storage; MAX_MESSAGES] = unsafe { std::mem::zeroed() }; -// for i in 0..MAX_MESSAGES { -// iov[i].iov_base = buf[i].as_mut_ptr() as *mut libc::c_void; -// iov[i].iov_len = BUFFER_SIZE; -// msgs[i].msg_hdr.msg_iov = &mut iov[i]; -// msgs[i].msg_hdr.msg_iovlen = 1; -// msgs[i].msg_hdr.msg_name = &mut addrs[i] as *const _ as *mut libc::c_void; -// msgs[i].msg_hdr.msg_namelen = std::mem::size_of::() as socklen_t; -// } -// let mut time: timespec = unsafe { std::mem::zeroed() }; -// loop { -// if context.is_stop() { -// return Ok(()); -// } -// let res = -// unsafe { libc::recvmmsg(fd, msgs.as_mut_ptr(), MAX_MESSAGES as c_uint, 0, &mut time) }; -// if res == -1 { -// log::error!("main_udp_listen_{}={:?}", index, io::Error::last_os_error()); -// continue; -// } -// -// let nmsgs = res as usize; -// for i in 0..nmsgs { -// let msg = &mut buf[i][0..msgs[i].msg_len as usize]; -// let addr = sockaddr_to_socket_addr(&addrs[i], msgs[i].msg_hdr.msg_namelen); -// if msg == b"stop" { -// if context.is_stop() { -// return Ok(()); -// } -// } -// recv_handler.handle(msg, RouteKey::new(false, index, addr), &context); -// } -// } -// } -// -// #[cfg(unix)] -// fn sockaddr_to_socket_addr(addr: &libc::sockaddr_storage, _len: libc::socklen_t) -> SocketAddr { -// match addr.ss_family as libc::c_int { -// libc::AF_INET => { -// let addr_in = unsafe { *(addr as *const _ as *const libc::sockaddr_in) }; -// let ip = u32::from_be(addr_in.sin_addr.s_addr); -// let port = u16::from_be(addr_in.sin_port); -// SocketAddr::V4(std::net::SocketAddrV4::new(Ipv4Addr::from(ip), port)) -// } -// libc::AF_INET6 => { -// let addr_in6 = unsafe { *(addr as *const _ as *const libc::sockaddr_in6) }; -// let ip = std::net::Ipv6Addr::from(addr_in6.sin6_addr.s6_addr); -// let port = u16::from_be(addr_in6.sin6_port); -// SocketAddr::V6(std::net::SocketAddrV6::new(ip, port, 0, 0)) -// } -// _ => panic!("Unsupported address family"), -// } -// } diff --git a/vnt/src/channel/ws_channel.rs b/vnt/src/channel/ws_channel.rs deleted file mode 100644 index b11eed8..0000000 --- a/vnt/src/channel/ws_channel.rs +++ /dev/null @@ -1,173 +0,0 @@ -use crate::channel::{ConnectProtocol, RouteKey, BUFFER_SIZE}; -use anyhow::Context; -use futures_util::stream::SplitStream; -use futures_util::{SinkExt, StreamExt}; -use std::convert::Into; -use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; -use std::thread; -use std::time::Duration; -use tokio::net::TcpStream; -use tokio::sync::mpsc::{channel, Receiver}; -use tokio_tungstenite::tungstenite::http::StatusCode; -use tokio_tungstenite::tungstenite::{Error, Message}; -use tokio_tungstenite::{connect_async, MaybeTlsStream, WebSocketStream}; - -use crate::channel::context::ChannelContext; -use crate::channel::handler::RecvChannelHandler; -use crate::channel::sender::PacketSender; -use crate::util::StopManager; - -/// ws协议, -/// 暂时只允许用ws连服务端,不能用ws打洞/连客户端 -pub fn ws_connect_accept( - receiver: Receiver<(Vec, String)>, - recv_handler: H, - context: ChannelContext, - stop_manager: StopManager, -) -> anyhow::Result<()> -where - H: RecvChannelHandler, -{ - let (stop_sender, stop_receiver) = tokio::sync::oneshot::channel::<()>(); - let worker = stop_manager.add_listener("wsChannel".into(), move || { - let _ = stop_sender.send(()); - })?; - let runtime = tokio::runtime::Builder::new_multi_thread() - .worker_threads(2) - .enable_all() - .build() - .context("ws tokio runtime build failed")?; - thread::Builder::new() - .name("wsChannel".into()) - .spawn(move || { - runtime.spawn(async move { connect_ws_handle(receiver, recv_handler, context).await }); - runtime.block_on(async { - let _ = stop_receiver.await; - }); - runtime.shutdown_background(); - worker.stop_all(); - }) - .context("ws thread build failed")?; - Ok(()) -} - -async fn connect_ws_handle( - mut receiver: Receiver<(Vec, String)>, - recv_handler: H, - context: ChannelContext, -) where - H: RecvChannelHandler, -{ - let mut index = 0; - while let Some((data, url)) = receiver.recv().await { - let recv_handler = recv_handler.clone(); - let context = context.clone(); - tokio::spawn(async move { - if let Err(e) = connect_ws(data, url, recv_handler, context, index).await { - log::warn!("发送失败,ws链接终止:{:?}", e); - } - }); - index += 1; - } -} -const WS_ADDR: SocketAddr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)); - -async fn connect_ws( - data: Vec, - mut url: String, - recv_handler: H, - context: ChannelContext, - index: usize, -) -> anyhow::Result<()> -where - H: RecvChannelHandler, -{ - let mut count = 0; - log::info!("尝试建立连接 {:?}", url); - let (mut ws, response) = loop { - count += 1; - if count > 3 { - Err(anyhow::anyhow!("发生多次重定向,链接终止"))? - } - match tokio::time::timeout(Duration::from_secs(3), connect_async(url)).await? { - Ok(rs) => break rs, - Err(e) => { - if let Error::Http(res) = &e { - if res.status() == StatusCode::MOVED_PERMANENTLY - || res.status() == StatusCode::FOUND - || res.status() == StatusCode::SEE_OTHER - || res.status() == StatusCode::TEMPORARY_REDIRECT - || res.status() == StatusCode::PERMANENT_REDIRECT - { - if let Some(v) = res.headers().get("Location") { - if let Ok(redirect) = v.to_str() { - log::info!("url重定向响应头 {:?}", res.headers()); - log::info!("url重定向地址 {}", redirect); - // 替换协议前缀 - if redirect.starts_with("http://") { - url = redirect.replacen("http://", "ws://", 1); - } else if redirect.starts_with("https://") { - url = redirect.replacen("https://", "wss://", 1); - } else { - url = redirect.to_string(); - } - println!("Location:{}", url); - log::info!("修改后的重定向地址: {}", url); - continue; - } - } - } - } - return Err(e)?; - } - } - }; - log::info!("ws协议握手 {:?}", response); - ws.send(Message::Binary(data)).await?; - let (mut ws_write, ws_read) = ws.split(); - let (sender, mut receiver) = channel::>(100); - let route_key = RouteKey::new(ConnectProtocol::WS, index, WS_ADDR); - - context - .packet_map - .write() - .insert(route_key, PacketSender::new(sender)); - tokio::spawn(async move { - while let Some(data) = receiver.recv().await { - if let Err(e) = ws_write.send(Message::Binary(data)).await { - log::warn!("websocket err {:?}", e); - break; - } - } - let _ = ws_write.close().await; - }); - if let Err(e) = ws_read_handle(ws_read, recv_handler, &context, route_key).await { - log::warn!("{:?}", e); - } - context.packet_map.write().remove(&route_key); - Ok(()) -} -async fn ws_read_handle( - mut ws_read: SplitStream>>, - recv_handler: H, - context: &ChannelContext, - route_key: RouteKey, -) -> anyhow::Result<()> -where - H: RecvChannelHandler, -{ - let mut extend = [0; BUFFER_SIZE]; - while let Some(msg) = ws_read.next().await { - let msg = msg.context("Error during WebSocket ")?; - match msg { - Message::Text(txt) => log::info!("Received text message: {}", txt), - Message::Binary(mut data) => { - recv_handler.handle(&mut data, &mut extend, route_key, context); - } - Message::Ping(_) | Message::Pong(_) => (), - Message::Close(_) => break, - _ => {} - } - } - Ok(()) -} diff --git a/vnt/src/cipher/aes_cbc/mod.rs b/vnt/src/cipher/aes_cbc/mod.rs deleted file mode 100644 index 6286a84..0000000 --- a/vnt/src/cipher/aes_cbc/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod rs_aes_cbc; -pub use rs_aes_cbc::*; diff --git a/vnt/src/cipher/aes_cbc/rs_aes_cbc.rs b/vnt/src/cipher/aes_cbc/rs_aes_cbc.rs deleted file mode 100644 index c765121..0000000 --- a/vnt/src/cipher/aes_cbc/rs_aes_cbc.rs +++ /dev/null @@ -1,153 +0,0 @@ -use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit}; -use anyhow::anyhow; -use rand::RngCore; - -use crate::cipher::Finger; -use crate::protocol::body::AesCbcSecretBody; -use crate::protocol::{NetPacket, HEAD_LEN}; - -type Aes128CbcEnc = cbc::Encryptor; -type Aes128CbcDec = cbc::Decryptor; -type Aes256CbcEnc = cbc::Encryptor; -type Aes256CbcDec = cbc::Decryptor; - -#[derive(Clone)] -pub struct AesCbcCipher { - pub(crate) cipher: AesCbcEnum, - pub(crate) finger: Option, -} - -#[derive(Clone)] -pub enum AesCbcEnum { - AES128CBC([u8; 16]), - AES256CBC([u8; 32]), -} - -impl AesCbcCipher { - pub fn key(&self) -> &[u8] { - match &self.cipher { - AesCbcEnum::AES128CBC(key) => key, - AesCbcEnum::AES256CBC(key) => key, - } - } -} - -impl AesCbcCipher { - pub fn new_128(key: [u8; 16], finger: Option) -> Self { - Self { - cipher: AesCbcEnum::AES128CBC(key), - finger, - } - } - pub fn new_256(key: [u8; 32], finger: Option) -> Self { - Self { - cipher: AesCbcEnum::AES256CBC(key), - finger, - } - } - - pub fn decrypt_ipv4 + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> anyhow::Result<()> { - if !net_packet.is_encrypt() { - //未加密的数据直接丢弃 - return Err(anyhow!("not encrypt")); - } - if net_packet.payload().len() < 16 { - log::error!("数据异常,长度{}小于{}", net_packet.payload().len(), 16); - return Err(anyhow!("aes_cbc data err")); - } - let mut iv = [0; 16]; - iv[0..12].copy_from_slice(&net_packet.head_tag()); - if let Some(finger) = &self.finger { - iv[12..16].copy_from_slice(&finger.hash[0..4]); - } - - let mut secret_body = - AesCbcSecretBody::new(net_packet.payload_mut(), self.finger.is_some())?; - if let Some(finger) = &self.finger { - let finger = finger.calculate_finger(&iv[..12], secret_body.en_body()); - if &finger != secret_body.finger() { - return Err(anyhow!("aes_cbc finger err")); - } - } - let rs = match &self.cipher { - AesCbcEnum::AES128CBC(key) => Aes128CbcDec::new(&(*key).into(), &iv.into()) - .decrypt_padded_mut::(secret_body.en_body_mut()), - AesCbcEnum::AES256CBC(key) => Aes256CbcDec::new(&(*key).into(), &iv.into()) - .decrypt_padded_mut::(secret_body.en_body_mut()), - }; - match rs { - Ok(buf) => { - let len = buf.len(); - net_packet.set_encrypt_flag(false); - //减去末尾的随机数 - net_packet.set_data_len(HEAD_LEN + len - 4)?; - Ok(()) - } - Err(e) => Err(anyhow!("aes_cbc 解密失败:{}", e)), - } - } - /// net_packet 必须预留足够长度 - /// data_len是有效载荷的长度 - pub fn encrypt_ipv4 + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> anyhow::Result<()> { - let data_len = net_packet.data_len(); - let mut iv = [0; 16]; - iv[0..12].copy_from_slice(&net_packet.head_tag()); - if let Some(finger) = &self.finger { - iv[12..16].copy_from_slice(&finger.hash[0..4]); - net_packet.set_data_len(data_len + 16)?; - } else { - net_packet.set_data_len(data_len + 4)?; - } - //先扩充随机数 - let mut secret_body = - AesCbcSecretBody::new(net_packet.payload_mut(), self.finger.is_some())?; - secret_body.set_random(rand::thread_rng().next_u32()); - let p_len = secret_body.en_body().len(); - net_packet.set_data_len_max(); - let rs = match &self.cipher { - AesCbcEnum::AES128CBC(key) => Aes128CbcEnc::new(&(*key).into(), &iv.into()) - .encrypt_padded_mut::(net_packet.payload_mut(), p_len), - AesCbcEnum::AES256CBC(key) => Aes256CbcEnc::new(&(*key).into(), &iv.into()) - .encrypt_padded_mut::(net_packet.payload_mut(), p_len), - }; - return match rs { - Ok(buf) => { - let len = buf.len(); - if let Some(finger) = &self.finger { - let finger = finger.calculate_finger(&iv[..12], buf); - //设置实际长度 - net_packet.set_data_len(HEAD_LEN + len + finger.len())?; - let mut secret_body = AesCbcSecretBody::new(net_packet.payload_mut(), true)?; - secret_body.set_finger(&finger)?; - } else { - net_packet.set_data_len(HEAD_LEN + len)?; - } - - net_packet.set_encrypt_flag(true); - Ok(()) - } - Err(e) => Err(anyhow!("aes_cbc 加密失败:{}", e)), - }; - } -} -#[test] -fn test_aes_cbc() { - let d = AesCbcCipher::new_128([0; 16], Some(Finger::new("123"))); - let mut p = NetPacket::new_encrypt([0; 100]).unwrap(); - let src = p.buffer().to_vec(); - d.encrypt_ipv4(&mut p).unwrap(); - d.decrypt_ipv4(&mut p).unwrap(); - assert_eq!(p.buffer(), &src); - let d = AesCbcCipher::new_128([0; 16], None); - let mut p = NetPacket::new_encrypt([0; 100]).unwrap(); - let src = p.buffer().to_vec(); - d.encrypt_ipv4(&mut p).unwrap(); - d.decrypt_ipv4(&mut p).unwrap(); - assert_eq!(p.buffer(), &src); -} diff --git a/vnt/src/cipher/aes_ecb/mod.rs b/vnt/src/cipher/aes_ecb/mod.rs deleted file mode 100644 index eab811f..0000000 --- a/vnt/src/cipher/aes_ecb/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[cfg(not(any(feature = "openssl-vendored", feature = "openssl")))] -mod rs_aes_ecb; -#[cfg(not(any(feature = "openssl-vendored", feature = "openssl")))] -pub use rs_aes_ecb::*; - -#[cfg(any(feature = "openssl-vendored", feature = "openssl"))] -mod openssl_aes_ecb; -#[cfg(any(feature = "openssl-vendored", feature = "openssl"))] -pub use openssl_aes_ecb::*; diff --git a/vnt/src/cipher/aes_ecb/openssl_aes_ecb.rs b/vnt/src/cipher/aes_ecb/openssl_aes_ecb.rs deleted file mode 100644 index 338865e..0000000 --- a/vnt/src/cipher/aes_ecb/openssl_aes_ecb.rs +++ /dev/null @@ -1,219 +0,0 @@ -use std::ptr; - -use anyhow::anyhow; -use libc::c_int; -use openssl_sys::EVP_CIPHER_CTX; - -use crate::cipher::Finger; -use crate::protocol::{NetPacket, HEAD_LEN}; - -pub struct AesEcbCipher { - key: Vec, - pub(crate) en_ctx: *mut EVP_CIPHER_CTX, - pub(crate) de_ctx: *mut EVP_CIPHER_CTX, - pub(crate) finger: Option, -} - -impl Drop for AesEcbCipher { - fn drop(&mut self) { - unsafe { - openssl_sys::EVP_CIPHER_CTX_free(self.de_ctx); - openssl_sys::EVP_CIPHER_CTX_free(self.en_ctx); - } - } -} - -impl Clone for AesEcbCipher { - fn clone(&self) -> Self { - if self.key.len() == 16 { - AesEcbCipher::new_128(self.key.clone().try_into().unwrap(), self.finger.clone()) - } else { - AesEcbCipher::new_256(self.key.clone().try_into().unwrap(), self.finger.clone()) - } - } -} - -unsafe impl Sync for AesEcbCipher {} - -unsafe impl Send for AesEcbCipher {} - -impl AesEcbCipher { - pub fn key(&self) -> &[u8] { - &self.key - } -} - -impl AesEcbCipher { - pub fn new_128(key: [u8; 16], finger: Option) -> Self { - unsafe { - let cipher = openssl_sys::EVP_aes_128_ecb(); - let en_ctx = openssl_sys::EVP_CIPHER_CTX_new(); - openssl_sys::EVP_EncryptInit_ex( - en_ctx, - cipher, - ptr::null_mut(), - key.as_ptr(), - ptr::null(), - ); - - let de_ctx = openssl_sys::EVP_CIPHER_CTX_new(); - openssl_sys::EVP_DecryptInit_ex( - de_ctx, - cipher, - ptr::null_mut(), - key.as_ptr(), - ptr::null(), - ); - Self { - key: key.to_vec(), - en_ctx, - de_ctx, - finger, - } - } - } - pub fn new_256(key: [u8; 32], finger: Option) -> Self { - unsafe { - let cipher = openssl_sys::EVP_aes_256_ecb(); - let en_ctx = openssl_sys::EVP_CIPHER_CTX_new(); - openssl_sys::EVP_EncryptInit_ex( - en_ctx, - cipher, - ptr::null_mut(), - key.as_ptr(), - ptr::null(), - ); - let de_ctx = openssl_sys::EVP_CIPHER_CTX_new(); - openssl_sys::EVP_DecryptInit_ex( - de_ctx, - cipher, - ptr::null_mut(), - key.as_ptr(), - ptr::null(), - ); - Self { - key: key.to_vec(), - en_ctx, - de_ctx, - finger, - } - } - } - - pub fn decrypt_ipv4 + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> anyhow::Result<()> { - if !net_packet.is_encrypt() { - //未加密的数据直接丢弃 - return Err(anyhow!("not encrypt")); - } - - if let Some(finger) = &self.finger { - let nonce_raw = net_packet.head_tag(); - let len = net_packet.payload().len(); - if len < 12 { - return Err(anyhow!("data len err")); - } - let secret_body = &net_packet.payload()[..len - 12]; - let finger = finger.calculate_finger(&nonce_raw, secret_body); - if &finger != &net_packet.payload()[len - 12..] { - return Err(anyhow!("finger err")); - } - net_packet.set_data_len(net_packet.data_len() - finger.len())?; - } - if net_packet.payload().len() < 16 { - log::error!("数据异常,长度{}小于{}", net_packet.payload().len(), 16); - return Err(anyhow!("data err")); - } - let input = net_packet.payload(); - let mut out = [0u8; 1024 * 5]; - let mut out_len = 0; - let ctx = self.de_ctx; - unsafe { - let out_ptr = out.as_mut_ptr(); - let in_len = input.len() as c_int; - openssl_sys::EVP_DecryptUpdate(ctx, out_ptr, &mut out_len, input.as_ptr(), in_len); - let mut last_len = 0; - openssl_sys::EVP_DecryptFinal_ex(ctx, out_ptr.offset(out_len as isize), &mut last_len); - out_len += last_len; - } - let out_len = out_len as usize; - let text = &out[..out_len]; - { - //校验头部 - let src_net_packet = NetPacket::new(text)?; - if src_net_packet.source() != net_packet.source() { - return Err(anyhow!("data err")); - } - if src_net_packet.destination() != net_packet.destination() { - return Err(anyhow!("data err")); - } - if src_net_packet.protocol() != net_packet.protocol() { - return Err(anyhow!("data err")); - } - if src_net_packet.transport_protocol() != net_packet.transport_protocol() { - return Err(anyhow!("data err")); - } - if src_net_packet.is_gateway() != net_packet.is_gateway() { - return Err(anyhow!("data err")); - } - if src_net_packet.source_ttl() != net_packet.source_ttl() { - return Err(anyhow!("data err")); - } - } - net_packet.set_encrypt_flag(false); - net_packet.set_data_len(out_len)?; - net_packet.set_payload(&text[12..])?; - Ok(()) - } - /// net_packet 必须预留足够长度 大于 12+16+16 - /// data_len是有效载荷的长度 - pub fn encrypt_ipv4 + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> anyhow::Result<()> { - let input = net_packet.buffer(); - let mut out = [0u8; 1024 * 5]; - let mut out_len = 0; - let ctx = self.en_ctx; - //将头部也参与加密 - unsafe { - let out_ptr = out.as_mut_ptr(); - let in_len = input.len() as c_int; - openssl_sys::EVP_EncryptUpdate(ctx, out_ptr, &mut out_len, input.as_ptr(), in_len); - let mut last_len = 0; - openssl_sys::EVP_EncryptFinal_ex(ctx, out_ptr.offset(out_len as isize), &mut last_len); - out_len += last_len; - } - let out_len = out_len as usize; - if out_len == 0 { - return Err(anyhow!("ciphertext len err")); - } - //密文 - let ciphertext = &out[..out_len]; - net_packet.set_data_len(HEAD_LEN + out_len)?; - net_packet.payload_mut().copy_from_slice(ciphertext); - net_packet.set_encrypt_flag(true); - if let Some(finger) = &self.finger { - let nonce_raw = net_packet.head_tag(); - let finger = finger.calculate_finger(&nonce_raw, ciphertext); - let src_data_len = net_packet.data_len(); - //设置实际长度 - net_packet.set_data_len(src_data_len + finger.len())?; - - net_packet.buffer_mut()[src_data_len..].copy_from_slice(&finger); - } - Ok(()) - } -} - -#[test] -fn test_openssl_aes_ecb() { - let d = AesEcbCipher::new_128([0; 16], Some(Finger::new("123"))); - let mut p = NetPacket::new_encrypt([0; 100]).unwrap(); - let src = p.buffer().to_vec(); - d.encrypt_ipv4(&mut p).unwrap(); - d.decrypt_ipv4(&mut p).unwrap(); - assert_eq!(p.buffer(), &src); -} diff --git a/vnt/src/cipher/aes_ecb/rs_aes_ecb.rs b/vnt/src/cipher/aes_ecb/rs_aes_ecb.rs deleted file mode 100644 index ab68703..0000000 --- a/vnt/src/cipher/aes_ecb/rs_aes_ecb.rs +++ /dev/null @@ -1,154 +0,0 @@ -use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyInit}; -use anyhow::anyhow; - -use crate::cipher::Finger; -use crate::protocol::{NetPacket, HEAD_LEN}; - -type Aes128EcbEnc = ecb::Encryptor; -type Aes128EcbDec = ecb::Decryptor; -type Aes256EcbEnc = ecb::Encryptor; -type Aes256EcbDec = ecb::Decryptor; - -#[derive(Clone)] -pub struct AesEcbCipher { - key: AesEcbEnum, - pub(crate) finger: Option, -} - -#[derive(Clone, Copy)] -pub enum AesEcbEnum { - AES128ECB([u8; 16]), - AES256ECB([u8; 32]), -} - -impl AesEcbCipher { - pub fn key(&self) -> &[u8] { - match &self.key { - AesEcbEnum::AES128ECB(key) => key, - AesEcbEnum::AES256ECB(key) => key, - } - } -} - -impl AesEcbCipher { - pub fn new_128(key: [u8; 16], finger: Option) -> Self { - Self { - key: AesEcbEnum::AES128ECB(key), - finger, - } - } - pub fn new_256(key: [u8; 32], finger: Option) -> Self { - Self { - key: AesEcbEnum::AES256ECB(key), - finger, - } - } - - pub fn decrypt_ipv4 + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> anyhow::Result<()> { - if !net_packet.is_encrypt() { - //未加密的数据直接丢弃 - return Err(anyhow!("not encrypt")); - } - - if let Some(finger) = &self.finger { - let nonce_raw = net_packet.head_tag(); - let len = net_packet.payload().len(); - if len < 12 { - return Err(anyhow!("payload len <12")); - } - let secret_body = &net_packet.payload()[..len - 12]; - let finger = finger.calculate_finger(&nonce_raw, secret_body); - if &finger != &net_packet.payload()[len - 12..] { - return Err(anyhow!("finger err")); - } - net_packet.set_data_len(net_packet.data_len() - finger.len())?; - } - if net_packet.payload().len() < 16 { - log::error!("数据异常,长度{}小于{}", net_packet.payload().len(), 16); - return Err(anyhow!("data err")); - } - let mut out = [0u8; 1024 * 5]; - let rs = match self.key { - AesEcbEnum::AES128ECB(key) => Aes128EcbDec::new(&key.into()) - .decrypt_padded_b2b_mut::(net_packet.payload(), &mut out), - AesEcbEnum::AES256ECB(key) => Aes256EcbDec::new(&key.into()) - .decrypt_padded_b2b_mut::(net_packet.payload(), &mut out), - }; - match rs { - Ok(buf) => { - //校验头部 - let src_net_packet = NetPacket::new(buf)?; - if src_net_packet.source() != net_packet.source() { - return Err(anyhow!("data err")); - } - if src_net_packet.destination() != net_packet.destination() { - return Err(anyhow!("data err")); - } - if src_net_packet.protocol() != net_packet.protocol() { - return Err(anyhow!("data err")); - } - if src_net_packet.transport_protocol() != net_packet.transport_protocol() { - return Err(anyhow!("data err")); - } - if src_net_packet.is_gateway() != net_packet.is_gateway() { - return Err(anyhow!("data err")); - } - if src_net_packet.source_ttl() != net_packet.source_ttl() { - return Err(anyhow!("data err")); - } - net_packet.set_data_len(buf.len())?; - net_packet.set_payload(src_net_packet.payload())?; - net_packet.set_encrypt_flag(false); - Ok(()) - } - Err(e) => Err(anyhow!("aes_ecb解密失败:{}", e)), - } - } - /// net_packet 必须预留足够长度 - /// data_len是有效载荷的长度 - pub fn encrypt_ipv4 + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> anyhow::Result<()> { - let mut out = [0u8; 1024 * 5]; - let rs = match self.key { - AesEcbEnum::AES128ECB(key) => Aes128EcbEnc::new(&key.into()) - .encrypt_padded_b2b_mut::(net_packet.buffer(), &mut out), - AesEcbEnum::AES256ECB(key) => Aes256EcbEnc::new(&key.into()) - .encrypt_padded_b2b_mut::(net_packet.buffer(), &mut out), - }; - - return match rs { - Ok(buf) => { - net_packet.set_data_len(HEAD_LEN + buf.len())?; - net_packet.set_payload(buf)?; - net_packet.set_encrypt_flag(true); - - if let Some(finger) = &self.finger { - let nonce_raw = net_packet.head_tag(); - let finger = finger.calculate_finger(&nonce_raw, buf); - let src_data_len = net_packet.data_len(); - //设置实际长度 - net_packet.set_data_len(src_data_len + finger.len())?; - - net_packet.buffer_mut()[src_data_len..].copy_from_slice(&finger); - } - Ok(()) - } - Err(e) => Err(anyhow!("aes_ecb加密失败:{}", e)), - }; - } -} - -#[test] -fn test_aes_ecb() { - let d = AesEcbCipher::new_128([0; 16], Some(Finger::new("123"))); - let mut p = NetPacket::new_encrypt([0; 100]).unwrap(); - let src = p.buffer().to_vec(); - d.encrypt_ipv4(&mut p).unwrap(); - d.decrypt_ipv4(&mut p).unwrap(); - assert_eq!(p.buffer(), &src) -} diff --git a/vnt/src/cipher/aes_gcm/aes_gcm_cipher.rs b/vnt/src/cipher/aes_gcm/aes_gcm_cipher.rs deleted file mode 100644 index 3b2ac78..0000000 --- a/vnt/src/cipher/aes_gcm/aes_gcm_cipher.rs +++ /dev/null @@ -1,132 +0,0 @@ -use aes_gcm::aead::consts::{U12, U16}; -use aes_gcm::aead::generic_array::GenericArray; -use aes_gcm::{AeadInPlace, Aes128Gcm, Aes256Gcm, Key, KeyInit, Nonce, Tag}; -use anyhow::anyhow; -use rand::RngCore; - -use crate::cipher::finger::Finger; -use crate::protocol::{body::SecretBody, body::AES_GCM_ENCRYPTION_RESERVED, NetPacket}; - -#[derive(Clone)] -pub struct AesGcmCipher { - pub(crate) cipher: AesGcmEnum, - pub(crate) finger: Option, -} - -#[derive(Clone)] -pub enum AesGcmEnum { - AES128GCM(Aes128Gcm), - AES256GCM(Aes256Gcm), -} - -impl AesGcmCipher { - pub fn new_128(key: [u8; 16], finger: Option) -> Self { - let key: &Key = &key.into(); - Self { - cipher: AesGcmEnum::AES128GCM(Aes128Gcm::new(key)), - finger, - } - } - pub fn new_256(key: [u8; 32], finger: Option) -> Self { - let key: &Key = &key.into(); - Self { - cipher: AesGcmEnum::AES256GCM(Aes256Gcm::new(key)), - finger, - } - } - - pub fn decrypt_ipv4 + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> anyhow::Result<()> { - if !net_packet.is_encrypt() { - //未加密的数据直接丢弃 - return Err(anyhow!("not encrypt")); - } - if net_packet.payload().len() < AES_GCM_ENCRYPTION_RESERVED { - log::error!("数据异常,长度小于{}", AES_GCM_ENCRYPTION_RESERVED); - return Err(anyhow!("data err")); - } - let nonce_raw = net_packet.head_tag(); - let nonce: &GenericArray = Nonce::from_slice(&nonce_raw); - - let mut secret_body = SecretBody::new(net_packet.payload_mut(), self.finger.is_some())?; - let tag = secret_body.tag(); - if let Some(finger) = &self.finger { - let finger = finger.calculate_finger(&nonce_raw, secret_body.en_body()); - if &finger != secret_body.finger() { - return Err(anyhow!("finger err")); - } - } - let tag: GenericArray = Tag::clone_from_slice(tag); - let rs = match &self.cipher { - AesGcmEnum::AES128GCM(aes_gcm) => { - aes_gcm.decrypt_in_place_detached(nonce, &[], secret_body.body_mut(), &tag) - } - AesGcmEnum::AES256GCM(aes_gcm) => { - aes_gcm.decrypt_in_place_detached(nonce, &[], secret_body.body_mut(), &tag) - } - }; - if let Err(e) = rs { - return Err(anyhow!("解密失败:{}", e)); - } - net_packet.set_encrypt_flag(false); - net_packet.set_data_len(net_packet.data_len() - AES_GCM_ENCRYPTION_RESERVED)?; - return Ok(()); - } - /// net_packet 必须预留足够长度 - /// data_len是有效载荷的长度 - pub fn encrypt_ipv4 + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> anyhow::Result<()> { - if net_packet.reserve() < AES_GCM_ENCRYPTION_RESERVED { - return Err(anyhow!("too short")); - } - let nonce_raw = net_packet.head_tag(); - let nonce: &GenericArray = Nonce::from_slice(&nonce_raw); - let data_len = net_packet.data_len() + AES_GCM_ENCRYPTION_RESERVED; - net_packet.set_data_len(data_len)?; - let mut secret_body = SecretBody::new(net_packet.payload_mut(), self.finger.is_some())?; - secret_body.set_random(rand::thread_rng().next_u32()); - let rs = match &self.cipher { - AesGcmEnum::AES128GCM(aes_gcm) => { - aes_gcm.encrypt_in_place_detached(nonce, &[], secret_body.body_mut()) - } - AesGcmEnum::AES256GCM(aes_gcm) => { - aes_gcm.encrypt_in_place_detached(nonce, &[], secret_body.body_mut()) - } - }; - return match rs { - Ok(tag) => { - secret_body.set_tag(tag.as_slice())?; - if let Some(finger) = &self.finger { - let finger = finger.calculate_finger(&nonce_raw, secret_body.en_body()); - secret_body.set_finger(&finger)?; - } - net_packet.set_encrypt_flag(true); - Ok(()) - } - Err(e) => Err(anyhow!("加密失败:{}", e)), - }; - } -} - -#[test] -fn test_aes_gcm() { - let d = AesGcmCipher::new_256([0; 32], Some(Finger::new("123"))); - let mut p = - NetPacket::new_encrypt([1; 13 + crate::protocol::body::ENCRYPTION_RESERVED]).unwrap(); - let src = p.buffer().to_vec(); - d.encrypt_ipv4(&mut p).unwrap(); - d.decrypt_ipv4(&mut p).unwrap(); - assert_eq!(p.buffer(), &src); - - let d = AesGcmCipher::new_256([0; 32], None); - let mut p = - NetPacket::new_encrypt([0; 13 + crate::protocol::body::ENCRYPTION_RESERVED]).unwrap(); - let src = p.buffer().to_vec(); - d.encrypt_ipv4(&mut p).unwrap(); - d.decrypt_ipv4(&mut p).unwrap(); - assert_eq!(p.buffer(), &src); -} diff --git a/vnt/src/cipher/aes_gcm/mod.rs b/vnt/src/cipher/aes_gcm/mod.rs deleted file mode 100644 index ade96e4..0000000 --- a/vnt/src/cipher/aes_gcm/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[cfg(feature = "ring-cipher")] -mod ring_aes_gcm_cipher; -#[cfg(feature = "ring-cipher")] -pub use ring_aes_gcm_cipher::*; - -#[cfg(not(feature = "ring-cipher"))] -mod aes_gcm_cipher; -#[cfg(not(feature = "ring-cipher"))] -pub use aes_gcm_cipher::*; diff --git a/vnt/src/cipher/aes_gcm/ring_aes_gcm_cipher.rs b/vnt/src/cipher/aes_gcm/ring_aes_gcm_cipher.rs deleted file mode 100644 index fbea94a..0000000 --- a/vnt/src/cipher/aes_gcm/ring_aes_gcm_cipher.rs +++ /dev/null @@ -1,148 +0,0 @@ -use anyhow::anyhow; -use rand::RngCore; -use ring::aead; -use ring::aead::{LessSafeKey, UnboundKey}; - -use crate::cipher::Finger; -use crate::protocol::body::{SecretBody, AES_GCM_ENCRYPTION_RESERVED}; -use crate::protocol::NetPacket; - -#[derive(Clone)] -pub struct AesGcmCipher { - pub(crate) cipher: AesGcmEnum, - pub(crate) finger: Option, -} - -pub enum AesGcmEnum { - AesGCM128(LessSafeKey, [u8; 16]), - AesGCM256(LessSafeKey, [u8; 32]), -} - -impl Clone for AesGcmEnum { - fn clone(&self) -> Self { - match &self { - AesGcmEnum::AesGCM128(_, key) => { - let c = - LessSafeKey::new(UnboundKey::new(&aead::AES_128_GCM, key.as_slice()).unwrap()); - AesGcmEnum::AesGCM128(c, *key) - } - AesGcmEnum::AesGCM256(_, key) => { - let c = - LessSafeKey::new(UnboundKey::new(&aead::AES_256_GCM, key.as_slice()).unwrap()); - AesGcmEnum::AesGCM256(c, *key) - } - } - } -} - -impl AesGcmCipher { - pub fn new_128(key: [u8; 16], finger: Option) -> Self { - let cipher = LessSafeKey::new(UnboundKey::new(&aead::AES_128_GCM, &key).unwrap()); - Self { - cipher: AesGcmEnum::AesGCM128(cipher, key), - finger, - } - } - pub fn new_256(key: [u8; 32], finger: Option) -> Self { - let cipher = LessSafeKey::new(UnboundKey::new(&aead::AES_256_GCM, &key).unwrap()); - Self { - cipher: AesGcmEnum::AesGCM256(cipher, key), - finger, - } - } - pub fn decrypt_ipv4 + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> anyhow::Result<()> { - if !net_packet.is_encrypt() { - //未加密的数据直接丢弃 - return Err(anyhow!("not encrypt")); - } - if net_packet.payload().len() < AES_GCM_ENCRYPTION_RESERVED { - log::error!("数据异常,长度小于{}", AES_GCM_ENCRYPTION_RESERVED); - return Err(anyhow!("data err")); - } - let nonce_raw = net_packet.head_tag(); - let nonce = aead::Nonce::assume_unique_for_key(nonce_raw); - let mut secret_body = SecretBody::new(net_packet.payload_mut(), self.finger.is_some())?; - if let Some(finger) = &self.finger { - let finger = finger.calculate_finger(&nonce_raw, secret_body.en_body()); - if &finger != secret_body.finger() { - return Err(anyhow!("ring aes finger err")); - } - } - - let rs = match &self.cipher { - AesGcmEnum::AesGCM128(cipher, _) => { - cipher.open_in_place(nonce, aead::Aad::empty(), secret_body.en_body_mut()) - } - AesGcmEnum::AesGCM256(cipher, _) => { - cipher.open_in_place(nonce, aead::Aad::empty(), secret_body.en_body_mut()) - } - }; - if let Err(e) = rs { - return Err(anyhow!("解密失败:{}", e)); - } - net_packet.set_encrypt_flag(false); - net_packet.set_data_len(net_packet.data_len() - AES_GCM_ENCRYPTION_RESERVED)?; - return Ok(()); - } - /// net_packet 必须预留足够长度 - /// data_len是有效载荷的长度 - /// 返回加密后载荷的长度 - pub fn encrypt_ipv4 + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> anyhow::Result<()> { - let nonce_raw = net_packet.head_tag(); - let nonce = aead::Nonce::assume_unique_for_key(nonce_raw); - let data_len = net_packet.data_len() + AES_GCM_ENCRYPTION_RESERVED; - net_packet.set_data_len(data_len)?; - let mut secret_body = SecretBody::new(net_packet.payload_mut(), self.finger.is_some())?; - secret_body.set_random(rand::thread_rng().next_u32()); - - let rs = match &self.cipher { - AesGcmEnum::AesGCM128(cipher, _) => { - cipher.seal_in_place_separate_tag(nonce, aead::Aad::empty(), secret_body.body_mut()) - } - AesGcmEnum::AesGCM256(cipher, _) => { - cipher.seal_in_place_separate_tag(nonce, aead::Aad::empty(), secret_body.body_mut()) - } - }; - return match rs { - Ok(tag) => { - let tag = tag.as_ref(); - if tag.len() != 16 { - return Err(anyhow!("加密tag长度错误:{}", tag.len())); - } - secret_body.set_tag(tag)?; - if let Some(finger) = &self.finger { - let finger = finger.calculate_finger(&nonce_raw, secret_body.en_body()); - secret_body.set_finger(&finger)?; - } - net_packet.set_encrypt_flag(true); - Ok(()) - } - Err(e) => Err(anyhow!("加密失败:{}", e)), - }; - } -} - -#[test] -fn test_aes_gcm() { - let d = AesGcmCipher::new_256([0; 32], Some(Finger::new("123"))); - let mut p = - NetPacket::new_encrypt([0; 13 + crate::protocol::body::ENCRYPTION_RESERVED]).unwrap(); - let src = p.buffer().to_vec(); - d.encrypt_ipv4(&mut p).unwrap(); - d.decrypt_ipv4(&mut p).unwrap(); - assert_eq!(p.buffer(), &src); - - let d = AesGcmCipher::new_256([0; 32], None); - let mut p = - NetPacket::new_encrypt([0; 13 + crate::protocol::body::ENCRYPTION_RESERVED]).unwrap(); - let src = p.buffer().to_vec(); - d.encrypt_ipv4(&mut p).unwrap(); - d.decrypt_ipv4(&mut p).unwrap(); - assert_eq!(p.buffer(), &src); -} diff --git a/vnt/src/cipher/chacha20/mod.rs b/vnt/src/cipher/chacha20/mod.rs deleted file mode 100644 index b249c3a..0000000 --- a/vnt/src/cipher/chacha20/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod rs_chacha20; -pub use rs_chacha20::*; diff --git a/vnt/src/cipher/chacha20/rs_chacha20.rs b/vnt/src/cipher/chacha20/rs_chacha20.rs deleted file mode 100644 index 3da6d7c..0000000 --- a/vnt/src/cipher/chacha20/rs_chacha20.rs +++ /dev/null @@ -1,107 +0,0 @@ -use aes::cipher::Iv; -use anyhow::anyhow; -use chacha20::cipher::{Key, KeyIvInit, StreamCipher}; -use chacha20::ChaCha20; - -use crate::cipher::finger::{gen_nonce, gen_random_nonce}; -use crate::cipher::Finger; -use crate::protocol::body::{ - IVSecretBody, SecretTail, SecretTailMut, FINGER_RESERVED, RANDOM_RESERVED, -}; -use crate::protocol::NetPacket; - -#[derive(Clone)] -pub struct ChaCha20Cipher { - key: [u8; 32], - pub(crate) finger: Option, -} - -impl ChaCha20Cipher { - pub fn new_256(key: [u8; 32], finger: Option) -> Self { - Self { key, finger } - } -} - -impl ChaCha20Cipher { - pub fn key(&self) -> &[u8] { - &self.key - } -} - -impl ChaCha20Cipher { - pub fn decrypt_ipv4 + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> anyhow::Result<()> { - if !net_packet.is_encrypt() { - //未加密的数据直接丢弃 - return Err(anyhow!("not encrypt")); - } - let mut head_tag = net_packet.head_tag(); - - let mut secret_body = IVSecretBody::new(net_packet.payload_mut(), self.finger.is_some())?; - if let Some(finger) = &self.finger { - let finger = finger.calculate_finger(&head_tag, secret_body.data()); - if &finger != secret_body.finger() { - return Err(anyhow!("ChaCha20 finger err")); - } - } - gen_nonce(&mut head_tag, secret_body.random_buf()); - ChaCha20::new( - Key::::from_slice(&self.key), - Iv::::from_slice(&head_tag), - ) - .apply_keystream(secret_body.data_mut()); - let len = secret_body.data().len(); - net_packet.set_encrypt_flag(false); - net_packet.set_payload_len(len)?; - Ok(()) - } - pub fn encrypt_ipv4 + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> anyhow::Result<()> { - let data_len = net_packet.data_len(); - let head_tag = net_packet.head_tag(); - if let Some(_) = &self.finger { - net_packet.set_data_len(data_len + RANDOM_RESERVED + FINGER_RESERVED)?; - } else { - net_packet.set_data_len(data_len + RANDOM_RESERVED)?; - } - let mut secret_body = IVSecretBody::new(net_packet.payload_mut(), self.finger.is_some())?; - let mut nonce = head_tag; - secret_body.set_random(&gen_random_nonce(&mut nonce)); - - ChaCha20::new( - Key::::from_slice(&self.key), - Iv::::from_slice(&nonce), - ) - .apply_keystream(secret_body.data_mut()); - if let Some(finger) = &self.finger { - let finger = finger.calculate_finger(&head_tag, secret_body.data()); - let mut secret_body = IVSecretBody::new(net_packet.payload_mut(), true)?; - secret_body.set_finger(&finger)?; - } - net_packet.set_encrypt_flag(true); - Ok(()) - } -} - -#[test] -fn test_chacha20() { - let d = ChaCha20Cipher::new_256([0; 32], Some(Finger::new("123"))); - let mut p = - NetPacket::new_encrypt([1; 13 + crate::protocol::body::ENCRYPTION_RESERVED]).unwrap(); - let src = p.buffer().to_vec(); - d.encrypt_ipv4(&mut p).unwrap(); - d.decrypt_ipv4(&mut p).unwrap(); - assert_eq!(p.buffer(), &src); - - let d = ChaCha20Cipher::new_256([0; 32], None); - let mut p = - NetPacket::new_encrypt([2; 13 + crate::protocol::body::ENCRYPTION_RESERVED]).unwrap(); - let src = p.buffer().to_vec(); - d.encrypt_ipv4(&mut p).unwrap(); - d.decrypt_ipv4(&mut p).unwrap(); - assert_eq!(p.buffer(), &src); -} diff --git a/vnt/src/cipher/chacha20_poly1305/mod.rs b/vnt/src/cipher/chacha20_poly1305/mod.rs deleted file mode 100644 index 1b4260f..0000000 --- a/vnt/src/cipher/chacha20_poly1305/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[cfg(feature = "ring-cipher")] -mod ring_chacha20_poly1305; -#[cfg(feature = "ring-cipher")] -pub use ring_chacha20_poly1305::*; - -#[cfg(not(feature = "ring-cipher"))] -mod rs_chacha20_poly1305; -#[cfg(not(feature = "ring-cipher"))] -pub use rs_chacha20_poly1305::*; diff --git a/vnt/src/cipher/chacha20_poly1305/ring_chacha20_poly1305.rs b/vnt/src/cipher/chacha20_poly1305/ring_chacha20_poly1305.rs deleted file mode 100644 index 1759a3c..0000000 --- a/vnt/src/cipher/chacha20_poly1305/ring_chacha20_poly1305.rs +++ /dev/null @@ -1,127 +0,0 @@ -use anyhow::anyhow; - -use ring::aead; -use ring::aead::{LessSafeKey, UnboundKey}; - -use crate::cipher::finger::{gen_nonce, gen_random_nonce}; -use crate::cipher::Finger; -use crate::protocol::body::{ - AEADSecretBody, SecretTail, SecretTailMut, FINGER_RESERVED, RANDOM_RESERVED, TAG_RESERVED, -}; -use crate::protocol::NetPacket; - -#[derive(Clone)] -pub struct ChaCha20Poly1305Cipher { - key: Vec, - pub(crate) cipher: LessSafeKey, - pub(crate) finger: Option, -} - -impl ChaCha20Poly1305Cipher { - pub fn new_256(key: [u8; 32], finger: Option) -> Self { - let cipher = LessSafeKey::new(UnboundKey::new(&aead::CHACHA20_POLY1305, &key).unwrap()); - Self { - key: key.to_vec(), - cipher, - finger, - } - } -} - -impl ChaCha20Poly1305Cipher { - pub fn key(&self) -> &[u8] { - &self.key - } -} - -impl ChaCha20Poly1305Cipher { - pub fn decrypt_ipv4 + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> anyhow::Result<()> { - if !net_packet.is_encrypt() { - //未加密的数据直接丢弃 - return Err(anyhow!("not encrypt")); - } - if net_packet.payload().len() < TAG_RESERVED { - log::error!("数据异常,长度小于{}", TAG_RESERVED); - return Err(anyhow!("data err")); - } - let mut head_tag = net_packet.head_tag(); - let mut secret_body = AEADSecretBody::new(net_packet.payload_mut(), self.finger.is_some())?; - if let Some(finger) = &self.finger { - let finger = finger.calculate_finger(&head_tag, secret_body.data_tag_mut()); - if &finger != secret_body.finger() { - return Err(anyhow!("ring CHACHA20_POLY1305 finger err")); - } - } - gen_nonce(&mut head_tag, secret_body.random_buf()); - let nonce = aead::Nonce::assume_unique_for_key(head_tag); - let rs = self - .cipher - .open_in_place(nonce, aead::Aad::empty(), secret_body.data_tag_mut()); - if let Err(e) = rs { - return Err(anyhow!("ring CHACHA20_POLY1305 解密失败:{}", e)); - } - let len = secret_body.data().len(); - net_packet.set_encrypt_flag(false); - net_packet.set_payload_len(len)?; - return Ok(()); - } - /// net_packet 必须预留足够长度 - /// data_len是有效载荷的长度 - /// 返回加密后载荷的长度 - pub fn encrypt_ipv4 + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> anyhow::Result<()> { - let head_tag = net_packet.head_tag(); - let data_len = net_packet.data_len(); - if self.finger.is_some() { - net_packet.set_data_len(data_len + TAG_RESERVED + RANDOM_RESERVED + FINGER_RESERVED)?; - } else { - net_packet.set_data_len(data_len + TAG_RESERVED + RANDOM_RESERVED)?; - } - let mut secret_body = AEADSecretBody::new(net_packet.payload_mut(), self.finger.is_some())?; - let mut nonce = head_tag; - secret_body.set_random(&gen_random_nonce(&mut nonce)); - let nonce = aead::Nonce::assume_unique_for_key(nonce); - let rs = self.cipher.seal_in_place_separate_tag( - nonce, - aead::Aad::empty(), - secret_body.data_mut(), - ); - match rs { - Ok(tag) => { - let tag = tag.as_ref(); - if tag.len() != 16 { - return Err(anyhow!("加密tag长度错误:{}", tag.len())); - } - secret_body.set_tag(tag)?; - if let Some(finger) = &self.finger { - let finger = finger.calculate_finger(&head_tag, secret_body.data_tag_mut()); - secret_body.set_finger(&finger)?; - } - net_packet.set_encrypt_flag(true); - Ok(()) - } - Err(e) => Err(anyhow!("ring CHACHA20_POLY1305 加密失败:{}", e)), - } - } -} - -#[test] -fn test_ring_chacha20_poly1305() { - let d = ChaCha20Poly1305Cipher::new_256([0; 32], Some(Finger::new("123"))); - let mut p = NetPacket::new_encrypt([0; 73]).unwrap(); - let src = p.buffer().to_vec(); - d.encrypt_ipv4(&mut p).unwrap(); - d.decrypt_ipv4(&mut p).unwrap(); - assert_eq!(p.buffer(), &src); - let d = ChaCha20Poly1305Cipher::new_256([0; 32], None); - let mut p = NetPacket::new_encrypt([0; 73]).unwrap(); - let src = p.buffer().to_vec(); - d.encrypt_ipv4(&mut p).unwrap(); - d.decrypt_ipv4(&mut p).unwrap(); - assert_eq!(p.buffer(), &src); -} diff --git a/vnt/src/cipher/chacha20_poly1305/rs_chacha20_poly1305.rs b/vnt/src/cipher/chacha20_poly1305/rs_chacha20_poly1305.rs deleted file mode 100644 index 39388db..0000000 --- a/vnt/src/cipher/chacha20_poly1305/rs_chacha20_poly1305.rs +++ /dev/null @@ -1,126 +0,0 @@ -use crate::cipher::finger::{gen_nonce, gen_random_nonce}; -use crate::cipher::Finger; -use crate::protocol::body::{ - AEADSecretBody, SecretTail, SecretTailMut, FINGER_RESERVED, RANDOM_RESERVED, TAG_RESERVED, -}; -use crate::protocol::NetPacket; -use anyhow::anyhow; -use chacha20poly1305::aead::{Nonce, Tag}; -use chacha20poly1305::{AeadInPlace, ChaCha20Poly1305, Key, KeyInit}; - -#[derive(Clone)] -pub struct ChaCha20Poly1305Cipher { - key: Vec, - pub(crate) cipher: ChaCha20Poly1305, - pub(crate) finger: Option, -} - -impl ChaCha20Poly1305Cipher { - pub fn new_256(key: [u8; 32], finger: Option) -> Self { - let key: &Key = &key.into(); - let cipher = ChaCha20Poly1305::new(key); - Self { - key: key.to_vec(), - cipher, - finger, - } - } -} - -impl ChaCha20Poly1305Cipher { - pub fn key(&self) -> &[u8] { - &self.key - } -} - -impl ChaCha20Poly1305Cipher { - pub fn decrypt_ipv4 + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> anyhow::Result<()> { - if !net_packet.is_encrypt() { - //未加密的数据直接丢弃 - return Err(anyhow!("not encrypt")); - } - if net_packet.payload().len() < TAG_RESERVED { - log::error!("数据异常,长度小于{}", TAG_RESERVED); - return Err(anyhow!("data err")); - } - let mut head_tag = net_packet.head_tag(); - let mut secret_body = AEADSecretBody::new(net_packet.payload_mut(), self.finger.is_some())?; - if let Some(finger) = &self.finger { - let finger = finger.calculate_finger(&head_tag, secret_body.data_tag_mut()); - if &finger != secret_body.finger() { - return Err(anyhow!("rs CHACHA20_POLY1305 finger err")); - } - } - gen_nonce(&mut head_tag, secret_body.random_buf()); - let nonce: Nonce = head_tag.into(); - let tag: Tag = - Tag::::from_slice(secret_body.tag()).clone(); - if let Err(e) = - self.cipher - .decrypt_in_place_detached(&nonce, &[], secret_body.data_mut(), &tag) - { - return Err(anyhow!("rs CHACHA20_POLY1305 decrypt_ipv4 {:?}", e)); - } - let len = secret_body.data().len(); - net_packet.set_encrypt_flag(false); - net_packet.set_payload_len(len)?; - Ok(()) - } - /// net_packet 必须预留足够长度 - /// data_len是有效载荷的长度 - /// 返回加密后载荷的长度 - pub fn encrypt_ipv4 + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> anyhow::Result<()> { - let head_tag = net_packet.head_tag(); - let data_len = net_packet.data_len(); - if self.finger.is_some() { - net_packet.set_data_len(data_len + TAG_RESERVED + RANDOM_RESERVED + FINGER_RESERVED)?; - } else { - net_packet.set_data_len(data_len + TAG_RESERVED + RANDOM_RESERVED)?; - } - let mut secret_body = AEADSecretBody::new(net_packet.payload_mut(), self.finger.is_some())?; - let mut nonce = head_tag; - secret_body.set_random(&gen_random_nonce(&mut nonce)); - let nonce = nonce.into(); - let rs = self - .cipher - .encrypt_in_place_detached(&nonce, &[], secret_body.data_mut()); - match rs { - Ok(tag) => { - let tag: &[u8] = tag.as_ref(); - if tag.len() != 16 { - return Err(anyhow!("加密tag长度错误:{}", tag.len(),)); - } - secret_body.set_tag(tag)?; - if let Some(finger) = &self.finger { - let finger = finger.calculate_finger(&head_tag, secret_body.data_tag_mut()); - secret_body.set_finger(&finger)?; - } - net_packet.set_encrypt_flag(true); - Ok(()) - } - Err(e) => Err(anyhow!("rs CHACHA20_POLY1305 加密失败:{}", e)), - } - } -} - -#[test] -fn test_rs_chacha20_poly1305() { - let d = ChaCha20Poly1305Cipher::new_256([0; 32], Some(Finger::new("123"))); - let mut p = NetPacket::new_encrypt([0; 73]).unwrap(); - let src = p.buffer().to_vec(); - d.encrypt_ipv4(&mut p).unwrap(); - d.decrypt_ipv4(&mut p).unwrap(); - assert_eq!(p.buffer(), &src); - let d = ChaCha20Poly1305Cipher::new_256([0; 32], None); - let mut p = NetPacket::new_encrypt([0; 73]).unwrap(); - let src = p.buffer().to_vec(); - d.encrypt_ipv4(&mut p).unwrap(); - d.decrypt_ipv4(&mut p).unwrap(); - assert_eq!(p.buffer(), &src); -} diff --git a/vnt/src/cipher/cipher.rs b/vnt/src/cipher/cipher.rs deleted file mode 100644 index 3613e79..0000000 --- a/vnt/src/cipher/cipher.rs +++ /dev/null @@ -1,335 +0,0 @@ -use std::fmt::Display; -use std::str::FromStr; - -use anyhow::anyhow; -#[cfg(cipher)] -use sha2::Digest; - -#[cfg(feature = "aes_cbc")] -use crate::cipher::aes_cbc::AesCbcCipher; -#[cfg(feature = "aes_ecb")] -use crate::cipher::aes_ecb::AesEcbCipher; -#[cfg(any(feature = "aes_gcm", feature = "server_encrypt"))] -use crate::cipher::aes_gcm::AesGcmCipher; -#[cfg(feature = "chacha20_poly1305")] -use crate::cipher::chacha20::ChaCha20Cipher; -#[cfg(feature = "chacha20_poly1305")] -use crate::cipher::chacha20_poly1305::ChaCha20Poly1305Cipher; -#[cfg(feature = "sm4_cbc")] -use crate::cipher::sm4_cbc::Sm4CbcCipher; -use crate::cipher::xor::XORCipher; -#[cfg(cipher)] -use crate::cipher::Finger; -use crate::protocol::NetPacket; - -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub enum CipherModel { - #[cfg(any(feature = "aes_gcm", feature = "server_encrypt"))] - AesGcm, - #[cfg(feature = "chacha20_poly1305")] - Chacha20Poly1305, - #[cfg(feature = "chacha20_poly1305")] - Chacha20, - #[cfg(feature = "aes_cbc")] - AesCbc, - #[cfg(feature = "aes_ecb")] - AesEcb, - #[cfg(feature = "sm4_cbc")] - Sm4Cbc, - Xor, - None, -} - -impl Display for CipherModel { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let str = match self { - #[cfg(any(feature = "aes_gcm", feature = "server_encrypt"))] - CipherModel::AesGcm => "aes_gcm".to_string(), - #[cfg(feature = "chacha20_poly1305")] - CipherModel::Chacha20Poly1305 => "chacha20_poly1305".to_string(), - #[cfg(feature = "chacha20_poly1305")] - CipherModel::Chacha20 => "chacha20".to_string(), - #[cfg(feature = "aes_cbc")] - CipherModel::AesCbc => "aes_cbc".to_string(), - #[cfg(feature = "aes_ecb")] - CipherModel::AesEcb => "aes_ecb".to_string(), - #[cfg(feature = "sm4_cbc")] - CipherModel::Sm4Cbc => "sm4_cbc".to_string(), - CipherModel::Xor => "xor".to_string(), - CipherModel::None => "none".to_string(), - }; - write!(f, "{}", str) - } -} - -impl FromStr for CipherModel { - type Err = String; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().trim() { - #[cfg(any(feature = "aes_gcm", feature = "server_encrypt"))] - "aes_gcm" => Ok(CipherModel::AesGcm), - #[cfg(feature = "chacha20_poly1305")] - "chacha20_poly1305" => Ok(CipherModel::Chacha20Poly1305), - #[cfg(feature = "chacha20_poly1305")] - "chacha20" => Ok(CipherModel::Chacha20), - #[cfg(feature = "aes_cbc")] - "aes_cbc" => Ok(CipherModel::AesCbc), - #[cfg(feature = "aes_ecb")] - "aes_ecb" => Ok(CipherModel::AesEcb), - #[cfg(feature = "sm4_cbc")] - "sm4_cbc" => Ok(CipherModel::Sm4Cbc), - "xor" => Ok(CipherModel::Xor), - _ => { - let mut enums = String::new(); - #[cfg(any(feature = "aes_gcm", feature = "server_encrypt"))] - enums.push_str("/aes_gcm"); - #[cfg(feature = "chacha20_poly1305")] - enums.push_str("/chacha20_poly1305/chacha20"); - #[cfg(feature = "aes_cbc")] - enums.push_str("/aes_cbc"); - #[cfg(feature = "aes_ecb")] - enums.push_str("/aes_ecb"); - #[cfg(feature = "sm4_cbc")] - enums.push_str("/sm4_cbc"); - enums.push_str("/xor"); - Err(format!("not match '{}', enum:{}", s, &enums[1..])) - } - } - } -} - -#[derive(Clone)] -pub enum Cipher { - #[cfg(any(feature = "aes_gcm", feature = "server_encrypt"))] - AesGcm((AesGcmCipher, Vec)), - #[cfg(feature = "chacha20_poly1305")] - Chacha20Poly1305(ChaCha20Poly1305Cipher), - #[cfg(feature = "chacha20_poly1305")] - Chacha20(ChaCha20Cipher), - #[cfg(feature = "aes_cbc")] - AesCbc(AesCbcCipher), - #[cfg(feature = "aes_ecb")] - AesEcb(AesEcbCipher), - #[cfg(feature = "sm4_cbc")] - Sm4Cbc(Sm4CbcCipher), - Xor(XORCipher), - None, -} - -impl Cipher { - pub fn new_password( - model: CipherModel, - password: Option, - token: Option, - ) -> anyhow::Result { - if let Some(password) = password { - #[cfg(cipher)] - let key: [u8; 32] = { - let mut hasher = sha2::Sha256::new(); - hasher.update(password.as_bytes()); - hasher.finalize().into() - }; - match model { - #[cfg(any(feature = "aes_gcm", feature = "server_encrypt"))] - CipherModel::AesGcm => { - let finger = token.map(|token| Finger::new(&token)); - if password.len() < 8 { - let aes = AesGcmCipher::new_128(key[..16].try_into().unwrap(), finger); - Ok(Cipher::AesGcm((aes, key[..16].to_vec()))) - } else { - let aes = AesGcmCipher::new_256(key, finger); - Ok(Cipher::AesGcm((aes, key.to_vec()))) - } - } - #[cfg(feature = "chacha20_poly1305")] - CipherModel::Chacha20Poly1305 => { - let finger = token.map(|token| Finger::new(&token)); - let chacha = ChaCha20Poly1305Cipher::new_256(key, finger); - Ok(Cipher::Chacha20Poly1305(chacha)) - } - #[cfg(feature = "chacha20_poly1305")] - CipherModel::Chacha20 => { - let finger = token.map(|token| Finger::new(&token)); - let chacha = ChaCha20Cipher::new_256(key, finger); - Ok(Cipher::Chacha20(chacha)) - } - #[cfg(feature = "aes_cbc")] - CipherModel::AesCbc => { - let finger = token.map(|token| Finger::new(&token)); - if password.len() < 8 { - let aes = AesCbcCipher::new_128(key[..16].try_into().unwrap(), finger); - Ok(Cipher::AesCbc(aes)) - } else { - let aes = AesCbcCipher::new_256(key, finger); - Ok(Cipher::AesCbc(aes)) - } - } - #[cfg(feature = "aes_ecb")] - CipherModel::AesEcb => { - let finger = token.map(|token| Finger::new(&token)); - if password.len() < 8 { - let aes = AesEcbCipher::new_128(key[..16].try_into().unwrap(), finger); - Ok(Cipher::AesEcb(aes)) - } else { - let aes = AesEcbCipher::new_256(key, finger); - Ok(Cipher::AesEcb(aes)) - } - } - #[cfg(feature = "sm4_cbc")] - CipherModel::Sm4Cbc => { - let finger = token.map(|token| Finger::new(&token)); - let aes = Sm4CbcCipher::new_128(key[..16].try_into().unwrap(), finger); - Ok(Cipher::Sm4Cbc(aes)) - } - CipherModel::Xor => { - if token.is_some() { - Err(anyhow::anyhow!( - "'finger' and 'xor' cannot be used simultaneously" - ))? - } - Ok(Cipher::Xor(XORCipher::new_256( - crate::cipher::xor::simple_hash(&password), - ))) - } - CipherModel::None => Ok(Cipher::None), - } - } else { - Ok(Cipher::None) - } - } - #[cfg(not(any(feature = "aes_gcm", feature = "server_encrypt")))] - pub fn new_key(_key: [u8; 32], _token: String) -> anyhow::Result { - Err(anyhow!("key error")) - } - #[cfg(any(feature = "aes_gcm", feature = "server_encrypt"))] - pub fn new_key(key: [u8; 32], token: String) -> anyhow::Result { - let finger = Some(Finger::new(&token)); - match key.len() { - 16 => { - let aes = AesGcmCipher::new_128(key[..16].try_into().unwrap(), finger); - Ok(Cipher::AesGcm((aes, key[..16].to_vec()))) - } - 32 => { - let aes = AesGcmCipher::new_256(key, finger); - Ok(Cipher::AesGcm((aes, key.to_vec()))) - } - _ => Err(anyhow!("key error")), - } - } - pub fn decrypt_ipv4 + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> anyhow::Result<()> { - match self { - #[cfg(any(feature = "aes_gcm", feature = "server_encrypt"))] - Cipher::AesGcm((aes_gcm, _)) => aes_gcm.decrypt_ipv4(net_packet), - #[cfg(feature = "aes_cbc")] - Cipher::AesCbc(aes_cbc) => aes_cbc.decrypt_ipv4(net_packet), - #[cfg(feature = "chacha20_poly1305")] - Cipher::Chacha20Poly1305(chacha20poly1305) => chacha20poly1305.decrypt_ipv4(net_packet), - #[cfg(feature = "chacha20_poly1305")] - Cipher::Chacha20(chacha20) => chacha20.decrypt_ipv4(net_packet), - #[cfg(feature = "aes_ecb")] - Cipher::AesEcb(aes_ecb) => aes_ecb.decrypt_ipv4(net_packet), - #[cfg(feature = "sm4_cbc")] - Cipher::Sm4Cbc(sm4_cbc) => sm4_cbc.decrypt_ipv4(net_packet), - Cipher::Xor(xor) => xor.decrypt_ipv4(net_packet), - Cipher::None => { - if net_packet.is_encrypt() { - return Err(anyhow!("not key")); - } - Ok(()) - } - } - } - pub fn encrypt_ipv4 + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> anyhow::Result<()> { - match self { - #[cfg(any(feature = "aes_gcm", feature = "server_encrypt"))] - Cipher::AesGcm((aes_gcm, _)) => aes_gcm.encrypt_ipv4(net_packet), - #[cfg(feature = "chacha20_poly1305")] - Cipher::Chacha20Poly1305(chacha20poly1305) => chacha20poly1305.encrypt_ipv4(net_packet), - #[cfg(feature = "chacha20_poly1305")] - Cipher::Chacha20(chacha20) => chacha20.encrypt_ipv4(net_packet), - #[cfg(feature = "aes_cbc")] - Cipher::AesCbc(aes_cbc) => aes_cbc.encrypt_ipv4(net_packet), - #[cfg(feature = "aes_ecb")] - Cipher::AesEcb(aes_ecb) => aes_ecb.encrypt_ipv4(net_packet), - #[cfg(feature = "sm4_cbc")] - Cipher::Sm4Cbc(sm4_cbc) => sm4_cbc.encrypt_ipv4(net_packet), - Cipher::Xor(xor) => xor.encrypt_ipv4(net_packet), - Cipher::None => Ok(()), - } - } - #[cfg(not(cipher))] - pub fn check_finger + AsMut<[u8]>>( - &self, - _net_packet: &NetPacket, - ) -> anyhow::Result<()> { - Ok(()) - } - #[cfg(cipher)] - pub fn check_finger>(&self, net_packet: &NetPacket) -> anyhow::Result<()> { - match self { - #[cfg(any(feature = "aes_gcm", feature = "server_encrypt"))] - Cipher::AesGcm((aes_gcm, _)) => aes_gcm - .finger - .as_ref() - .map(|f| f.check_finger(net_packet)) - .unwrap_or(Ok(())), - #[cfg(feature = "chacha20_poly1305")] - Cipher::Chacha20Poly1305(chacha20poly1305) => chacha20poly1305 - .finger - .as_ref() - .map(|f| f.check_finger(net_packet)) - .unwrap_or(Ok(())), - #[cfg(feature = "chacha20_poly1305")] - Cipher::Chacha20(chacha20) => chacha20 - .finger - .as_ref() - .map(|f| f.check_finger(net_packet)) - .unwrap_or(Ok(())), - #[cfg(feature = "aes_cbc")] - Cipher::AesCbc(aes_cbc) => aes_cbc - .finger - .as_ref() - .map(|f| f.check_finger(net_packet)) - .unwrap_or(Ok(())), - #[cfg(feature = "aes_ecb")] - Cipher::AesEcb(aes_ecb) => aes_ecb - .finger - .as_ref() - .map(|f| f.check_finger(net_packet)) - .unwrap_or(Ok(())), - #[cfg(feature = "sm4_cbc")] - Cipher::Sm4Cbc(sm4_cbc) => sm4_cbc - .finger - .as_ref() - .map(|f| f.check_finger(net_packet)) - .unwrap_or(Ok(())), - Cipher::Xor(_) => Ok(()), - Cipher::None => Ok(()), - } - } - pub fn key(&self) -> Option<&[u8]> { - match self { - #[cfg(any(feature = "aes_gcm", feature = "server_encrypt"))] - Cipher::AesGcm((_, key)) => Some(key), - #[cfg(feature = "chacha20_poly1305")] - Cipher::Chacha20Poly1305(chacha20poly1305) => Some(chacha20poly1305.key()), - #[cfg(feature = "chacha20_poly1305")] - Cipher::Chacha20(chacha20) => Some(chacha20.key()), - #[cfg(feature = "aes_cbc")] - Cipher::AesCbc(aes_cbc) => Some(aes_cbc.key()), - #[cfg(feature = "aes_ecb")] - Cipher::AesEcb(aes_ecb) => Some(aes_ecb.key()), - #[cfg(feature = "sm4_cbc")] - Cipher::Sm4Cbc(sm4_cbc) => Some(sm4_cbc.key()), - Cipher::Xor(xor) => Some(xor.key()), - Cipher::None => None, - } - } -} diff --git a/vnt/src/cipher/finger.rs b/vnt/src/cipher/finger.rs deleted file mode 100644 index f070a7a..0000000 --- a/vnt/src/cipher/finger.rs +++ /dev/null @@ -1,76 +0,0 @@ -use anyhow::anyhow; -use rand::RngCore; - -use sha2::Digest; - -use crate::protocol::NetPacket; - -#[derive(Clone)] -pub struct Finger { - pub(crate) hash: [u8; 32], -} - -impl Finger { - pub fn new(str: &str) -> Self { - let mut hasher = sha2::Sha256::new(); - hasher.update(str.as_bytes()); - let hash: [u8; 32] = hasher.finalize().into(); - Finger { hash } - } - pub fn check_finger>(&self, net_packet: &NetPacket) -> anyhow::Result<()> { - if !net_packet.is_encrypt() { - //未加密的数据直接丢弃 - return Err(anyhow!("not encrypt")); - } - let payload_len = net_packet.payload().len(); - if payload_len < 12 { - log::error!("数据异常,长度小于{}", 12); - return Err(anyhow!("data err")); - } - let mut nonce_raw = [0; 12]; - nonce_raw[0..4].copy_from_slice(&net_packet.source().octets()); - nonce_raw[4..8].copy_from_slice(&net_packet.destination().octets()); - nonce_raw[8] = net_packet.protocol().into(); - nonce_raw[9] = net_packet.transport_protocol(); - nonce_raw[10] = net_packet.is_gateway() as u8; - nonce_raw[11] = net_packet.source_ttl(); - let payload = net_packet.payload(); - let finger = self.calculate_finger(&nonce_raw, &payload[..payload_len - 12]); - if &finger[..] != &payload[payload_len - 12..] { - return Err(anyhow!("finger err")); - } - Ok(()) - } - pub fn calculate_finger(&self, nonce: &[u8], secret_body: &[u8]) -> [u8; 12] { - let mut hasher = sha2::Sha256::new(); - hasher.update(nonce); - hasher.update(secret_body); - hasher.update(&self.hash); - let key: [u8; 32] = hasher.finalize().into(); - return key[20..].try_into().unwrap(); - } -} -impl> NetPacket { - pub fn head_tag(&self) -> [u8; 12] { - let mut tag = [0; 12]; - tag[0..4].copy_from_slice(&self.buffer()[4..8]); - tag[4..8].copy_from_slice(&self.buffer()[8..12]); - tag[8] = self.protocol().into(); - tag[9] = self.transport_protocol(); - tag[10] = self.is_gateway() as u8; - tag[11] = self.source_ttl(); - tag - } -} -pub fn gen_nonce(tag: &mut [u8], random: &[u8]) { - tag[8] = random[0] ^ tag[8]; - tag[9] = random[1] ^ tag[9]; - tag[10] = random[2] ^ tag[10]; - tag[11] = random[3] ^ tag[11]; -} -pub fn gen_random_nonce(tag: &mut [u8; 12]) -> [u8; 4] { - let mut random = [0; 4]; - rand::thread_rng().fill_bytes(&mut random); - gen_nonce(tag, &random); - random -} diff --git a/vnt/src/cipher/mod.rs b/vnt/src/cipher/mod.rs deleted file mode 100644 index 1cc12d7..0000000 --- a/vnt/src/cipher/mod.rs +++ /dev/null @@ -1,32 +0,0 @@ -mod cipher; -#[cfg(cipher)] -mod finger; - -pub use cipher::Cipher; -pub use cipher::CipherModel; -#[cfg(cipher)] -pub use finger::Finger; -#[cfg(feature = "server_encrypt")] -mod rsa_cipher; -#[cfg(feature = "server_encrypt")] -pub use rsa_cipher::RsaCipher; - -#[cfg(any(feature = "aes_gcm", feature = "server_encrypt"))] -mod aes_gcm; - -#[cfg(feature = "chacha20_poly1305")] -mod chacha20; -#[cfg(feature = "chacha20_poly1305")] -mod chacha20_poly1305; - -#[cfg(feature = "aes_ecb")] -mod aes_ecb; - -#[cfg(feature = "aes_cbc")] -mod aes_cbc; - -#[cfg(feature = "sm4_cbc")] -mod sm4_cbc; - -mod xor; -pub use xor::simple_hash; diff --git a/vnt/src/cipher/rsa_cipher.rs b/vnt/src/cipher/rsa_cipher.rs deleted file mode 100644 index f66a230..0000000 --- a/vnt/src/cipher/rsa_cipher.rs +++ /dev/null @@ -1,112 +0,0 @@ -use std::io; - -use { - crate::protocol::body::{RsaSecretBody, RSA_ENCRYPTION_RESERVED}, - rand::Rng, - rsa::pkcs8::der::Decode, - rsa::RsaPublicKey, - sha2::Digest, - spki::{DecodePublicKey, EncodePublicKey}, -}; - -use crate::protocol::NetPacket; - -#[derive(Clone)] -pub struct RsaCipher { - inner: Inner, -} -#[derive(Clone)] -struct Inner { - public_key: RsaPublicKey, - finger: String, -} - -impl RsaCipher { - pub fn new(der: &[u8]) -> io::Result { - match RsaPublicKey::from_public_key_der(der) { - Ok(public_key) => { - let finger = finger(&public_key)?; - let inner = Inner { public_key, finger }; - Ok(Self { inner }) - } - Err(e) => Err(io::Error::new( - io::ErrorKind::Other, - format!("from_public_key_der failed {}", e), - )), - } - } - pub fn finger(&self) -> &String { - &self.inner.finger - } - pub fn public_key(&self) -> io::Result<&RsaPublicKey> { - return Ok(&self.inner.public_key); - } -} -pub fn finger(public_key: &RsaPublicKey) -> io::Result { - match public_key.to_public_key_der() { - Ok(der) => match rsa::pkcs8::SubjectPublicKeyInfoRef::from_der(der.as_bytes()) { - Ok(spki) => match spki.fingerprint_base64() { - Ok(finger) => Ok(finger), - Err(e) => Err(io::Error::new( - io::ErrorKind::Other, - format!("fingerprint_base64 error {}", e), - )), - }, - Err(e) => Err(io::Error::new( - io::ErrorKind::Other, - format!("from_der error {}", e), - )), - }, - Err(e) => Err(io::Error::new( - io::ErrorKind::Other, - format!("to_public_key_der error {}", e), - )), - } -} - -impl RsaCipher { - /// net_packet 必须预留足够长度 - pub fn encrypt + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> io::Result>> { - if net_packet.reserve() < RSA_ENCRYPTION_RESERVED { - return Err(io::Error::new(io::ErrorKind::Other, "too short")); - } - let data_len = net_packet.data_len() + RSA_ENCRYPTION_RESERVED; - net_packet.set_data_len(data_len)?; - let mut nonce_raw = [0; 12]; - nonce_raw[0..4].copy_from_slice(&net_packet.source().octets()); - nonce_raw[4..8].copy_from_slice(&net_packet.destination().octets()); - nonce_raw[8] = net_packet.protocol().into(); - nonce_raw[9] = net_packet.transport_protocol(); - nonce_raw[10] = net_packet.is_gateway() as u8; - nonce_raw[11] = net_packet.source_ttl(); - - let mut secret_body = RsaSecretBody::new(net_packet.payload_mut())?; - let mut rng = rand::thread_rng(); - rng.fill(secret_body.random_mut()); - - let mut hasher = sha2::Sha256::new(); - hasher.update(secret_body.body()); - hasher.update(nonce_raw); - let key: [u8; 32] = hasher.finalize().into(); - secret_body.set_finger(&key[16..])?; - match self.inner.public_key.encrypt( - &mut rng, - rsa::pkcs1v15::Pkcs1v15Encrypt, - secret_body.buffer(), - ) { - Ok(enc_data) => { - let mut net_packet_e = NetPacket::new(vec![0; 12 + enc_data.len()])?; - net_packet_e.buffer_mut()[..12].copy_from_slice(&net_packet.buffer()[..12]); - net_packet_e.set_payload(&enc_data)?; - Ok(net_packet_e) - } - Err(e) => Err(io::Error::new( - io::ErrorKind::Other, - format!("encrypt failed {}", e), - )), - } - } -} diff --git a/vnt/src/cipher/sm4_cbc/mod.rs b/vnt/src/cipher/sm4_cbc/mod.rs deleted file mode 100644 index 77a92eb..0000000 --- a/vnt/src/cipher/sm4_cbc/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod rs_sm4_cbc; -pub use rs_sm4_cbc::*; diff --git a/vnt/src/cipher/sm4_cbc/rs_sm4_cbc.rs b/vnt/src/cipher/sm4_cbc/rs_sm4_cbc.rs deleted file mode 100644 index 885749c..0000000 --- a/vnt/src/cipher/sm4_cbc/rs_sm4_cbc.rs +++ /dev/null @@ -1,153 +0,0 @@ -use crate::cipher::Finger; -use crate::protocol::{NetPacket, HEAD_LEN}; -use anyhow::anyhow; -use libsm::sm4::cipher_mode::CipherMode; -use libsm::sm4::Sm4CipherMode; -use rand::RngCore; - -pub struct Sm4CbcCipher { - key: [u8; 16], - pub(crate) cipher: Sm4CipherMode, - pub(crate) finger: Option, -} - -impl Clone for Sm4CbcCipher { - fn clone(&self) -> Self { - let cipher = Sm4CipherMode::new(&self.key, CipherMode::Cbc).unwrap(); - Self { - key: self.key, - cipher, - finger: self.finger.clone(), - } - } -} - -impl Sm4CbcCipher { - pub fn key(&self) -> &[u8] { - &self.key - } -} - -impl Sm4CbcCipher { - pub fn new_128(key: [u8; 16], finger: Option) -> Self { - let cipher = Sm4CipherMode::new(&key, CipherMode::Cbc).unwrap(); - Self { - key, - cipher, - finger, - } - } - - pub fn decrypt_ipv4 + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> anyhow::Result<()> { - if !net_packet.is_encrypt() { - //未加密的数据直接丢弃 - return Err(anyhow!("not encrypt")); - } - - if let Some(finger) = &self.finger { - let nonce_raw = net_packet.head_tag(); - let len = net_packet.payload().len(); - if len < 12 { - return Err(anyhow!("payload len <12")); - } - let secret_body = &net_packet.payload()[..len - 12]; - let finger = finger.calculate_finger(&nonce_raw, secret_body); - if &finger != &net_packet.payload()[len - 12..] { - return Err(anyhow!("finger err")); - } - net_packet.set_data_len(net_packet.data_len() - finger.len())?; - } - let payload = net_packet.payload(); - let len = payload.len(); - if len < 16 || len > 1024 * 4 { - log::error!("数据异常,长度{}小于16或大于4096", len); - return Err(anyhow!("data err")); - } - let mut out = [0u8; 1024 * 4]; - let data = &payload[..len - 16]; - let iv = &payload[len - 16..]; - match self.cipher.decrypt(data, iv, &mut out) { - Ok(len) => { - let src_net_packet = NetPacket::new(&out[..len])?; - if src_net_packet.source() != net_packet.source() { - return Err(anyhow!("data err")); - } - if src_net_packet.destination() != net_packet.destination() { - return Err(anyhow!("data err")); - } - if src_net_packet.protocol() != net_packet.protocol() { - return Err(anyhow!("data err")); - } - if src_net_packet.transport_protocol() != net_packet.transport_protocol() { - return Err(anyhow!("data err")); - } - if src_net_packet.is_gateway() != net_packet.is_gateway() { - return Err(anyhow!("data err")); - } - if src_net_packet.source_ttl() != net_packet.source_ttl() { - return Err(anyhow!("data err")); - } - net_packet.set_data_len(len)?; - net_packet.set_payload(src_net_packet.payload())?; - net_packet.set_encrypt_flag(false); - Ok(()) - } - Err(e) => Err(anyhow!("sm4_cbc解密失败:{}", e)), - } - } - /// net_packet 必须预留足够长度 - /// data_len是有效载荷的长度 - pub fn encrypt_ipv4 + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> anyhow::Result<()> { - let mut out = [0u8; 1024 * 4]; - let mut iv = [0u8; 16]; - rand::thread_rng().fill_bytes(&mut iv); - if net_packet.data_len() > 1024 * 4 - 32 { - log::error!( - "数据异常,长度{}大于1024 * 4 - 32", - net_packet.buffer().len() - ); - return Err(anyhow!("data err")); - } - match self.cipher.encrypt(net_packet.buffer(), &iv, &mut out) { - Ok(len) => { - net_packet.set_data_len(HEAD_LEN + len + 16)?; - net_packet.payload_mut()[..len].copy_from_slice(&out[..len]); - net_packet.payload_mut()[len..].copy_from_slice(&iv); - if let Some(finger) = &self.finger { - let nonce_raw = net_packet.head_tag(); - let finger = finger.calculate_finger(&nonce_raw, net_packet.payload()); - let src_data_len = net_packet.data_len(); - //设置实际长度 - net_packet.set_data_len(src_data_len + finger.len())?; - - net_packet.buffer_mut()[src_data_len..].copy_from_slice(&finger); - } - net_packet.set_encrypt_flag(true); - Ok(()) - } - Err(e) => Err(anyhow!("sm4_cbc加密失败:{}", e)), - } - } -} - -#[test] -fn test_sm4_ecb() { - let d = Sm4CbcCipher::new_128([0; 16], Some(Finger::new("123"))); - let mut p = NetPacket::new_encrypt([1; 1024]).unwrap(); - let src = p.buffer().to_vec(); - d.encrypt_ipv4(&mut p).unwrap(); - d.decrypt_ipv4(&mut p).unwrap(); - assert_eq!(p.buffer(), &src); - let d = Sm4CbcCipher::new_128([0; 16], None); - let mut p = NetPacket::new_encrypt([1; 102]).unwrap(); - let src = p.buffer().to_vec(); - d.encrypt_ipv4(&mut p).unwrap(); - d.decrypt_ipv4(&mut p).unwrap(); - assert_eq!(p.buffer(), &src) -} diff --git a/vnt/src/cipher/xor/mod.rs b/vnt/src/cipher/xor/mod.rs deleted file mode 100644 index fe38376..0000000 --- a/vnt/src/cipher/xor/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod xor; -pub use xor::*; diff --git a/vnt/src/cipher/xor/xor.rs b/vnt/src/cipher/xor/xor.rs deleted file mode 100644 index 7a76241..0000000 --- a/vnt/src/cipher/xor/xor.rs +++ /dev/null @@ -1,84 +0,0 @@ -use anyhow::anyhow; - -use crate::protocol::NetPacket; - -pub fn simple_hash(input: &str) -> [u8; 32] { - let mut result = [0u8; 32]; - let bytes = input.as_bytes(); - for (index, v) in result.iter_mut().enumerate() { - *v = bytes[index % bytes.len()]; - } - - let mut state = 0u8; - - for (i, &byte) in bytes.iter().enumerate() { - let combined = byte.wrapping_add(state).rotate_left((i % 8) as u32); - result[i % 32] ^= combined; - state = state.wrapping_add(byte).rotate_left(3); - } - - for i in 0..32 { - result[i] = result[i] - .rotate_left((result[(i + 1) % 32] % 8) as u32) - .wrapping_add(state); - state = state.wrapping_add(result[i]).rotate_left(3); - } - - result -} - -#[derive(Clone)] -pub struct XORCipher { - key: [u8; 32], -} - -impl XORCipher { - pub fn new_256(key: [u8; 32]) -> Self { - Self { key } - } -} - -impl XORCipher { - pub fn key(&self) -> &[u8] { - &self.key - } -} - -impl XORCipher { - pub fn decrypt_ipv4 + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> anyhow::Result<()> { - if !net_packet.is_encrypt() { - //未加密的数据直接丢弃 - return Err(anyhow!("not encrypt")); - } - let key = &self.key; - for (i, byte) in net_packet.payload_mut().iter_mut().enumerate() { - *byte ^= key[i & 31]; - } - net_packet.set_encrypt_flag(false); - Ok(()) - } - pub fn encrypt_ipv4 + AsMut<[u8]>>( - &self, - net_packet: &mut NetPacket, - ) -> anyhow::Result<()> { - net_packet.set_encrypt_flag(true); - let key = &self.key; - for (i, byte) in net_packet.payload_mut().iter_mut().enumerate() { - *byte ^= key[i & 31]; - } - Ok(()) - } -} - -#[test] -fn test_xor() { - let d = XORCipher::new_256(simple_hash("password")); - let mut p = NetPacket::new_encrypt([0; 1000]).unwrap(); - let src = p.buffer().to_vec(); - d.encrypt_ipv4(&mut p).unwrap(); - d.decrypt_ipv4(&mut p).unwrap(); - assert_eq!(p.buffer(), &src) -} diff --git a/vnt/src/compression/lz4_compress.rs b/vnt/src/compression/lz4_compress.rs deleted file mode 100644 index 6c28abc..0000000 --- a/vnt/src/compression/lz4_compress.rs +++ /dev/null @@ -1,33 +0,0 @@ -use anyhow::anyhow; - -use crate::protocol::NetPacket; - -#[derive(Clone)] -pub struct Lz4Compressor; - -impl Lz4Compressor { - pub fn compress, O: AsRef<[u8]> + AsMut<[u8]>>( - in_net_packet: &NetPacket, - out: &mut NetPacket, - ) -> anyhow::Result<()> { - out.set_data_len_max(); - let len = match lz4_flex::compress_into(in_net_packet.payload(), out.payload_mut()) { - Ok(len) => len, - Err(e) => Err(anyhow!("Lz4 compress {}", e))?, - }; - out.set_payload_len(len)?; - Ok(()) - } - pub fn decompress, O: AsRef<[u8]> + AsMut<[u8]>>( - in_net_packet: &NetPacket, - out: &mut NetPacket, - ) -> anyhow::Result<()> { - out.set_data_len_max(); - let len = match lz4_flex::decompress_into(in_net_packet.payload(), out.payload_mut()) { - Ok(len) => len, - Err(e) => Err(anyhow!("Lz4 decompress {}", e))?, - }; - out.set_payload_len(len)?; - Ok(()) - } -} diff --git a/vnt/src/compression/mod.rs b/vnt/src/compression/mod.rs deleted file mode 100644 index d9f06a8..0000000 --- a/vnt/src/compression/mod.rs +++ /dev/null @@ -1,220 +0,0 @@ -use std::str::FromStr; - -use anyhow::anyhow; - -#[cfg(feature = "lz4_compress")] -use crate::compression::lz4_compress::Lz4Compressor; -#[cfg(feature = "zstd_compress")] -use crate::compression::zstd_compress::ZstdCompressor; -use crate::protocol::extension::CompressionAlgorithm; -#[cfg(feature = "zstd_compress")] -use zstd::zstd_safe::CompressionLevel; - -use crate::protocol::NetPacket; - -#[cfg(feature = "lz4_compress")] -mod lz4_compress; -#[cfg(feature = "zstd_compress")] -mod zstd_compress; - -#[derive(Clone, Copy, Debug)] -pub enum Compressor { - #[cfg(feature = "lz4_compress")] - Lz4, - #[cfg(feature = "zstd_compress")] - Zstd(CompressionLevel), - None, -} - -impl FromStr for Compressor { - type Err = String; - #[cfg(not(any(feature = "lz4_compress", feature = "zstd_compress")))] - fn from_str(s: &str) -> Result { - Err(format!("not match '{}', Compression not supported", s)) - } - #[cfg(any(feature = "lz4_compress", feature = "zstd_compress"))] - fn from_str(s: &str) -> Result { - let str = s.trim().to_lowercase(); - match str.as_str() { - #[cfg(feature = "lz4_compress")] - "lz4" => Ok(Compressor::Lz4), - #[cfg(feature = "zstd_compress")] - "zstd" => Ok(Compressor::Zstd(9)), - "none" => Ok(Compressor::None), - _ => { - #[cfg(feature = "zstd_compress")] - { - let string_array: Vec = str.split(',').map(|s| s.to_string()).collect(); - if string_array.len() != 2 || string_array[0] != "zstd" { - return Err(format!("not match '{}', exp: zstd,10", s)); - } - return match CompressionLevel::from_str(&string_array[1]) { - Ok(level) => Ok(Compressor::Zstd(level)), - Err(_) => Err(format!("not match '{}', exp: zstd,10", s)), - }; - } - #[cfg(not(feature = "zstd_compress"))] - #[cfg(feature = "lz4_compress")] - return Err(format!("not match '{}', exp: lz4", s)); - } - } - } -} - -#[cfg(not(any(feature = "lz4_compress", feature = "zstd_compress")))] -impl Compressor { - pub fn compress, O: AsRef<[u8]> + AsMut<[u8]>>( - &self, - _in_net_packet: &NetPacket, - _out: &mut NetPacket, - ) -> anyhow::Result { - Ok(false) - } - pub fn decompress, O: AsRef<[u8]> + AsMut<[u8]>>( - _algorithm: CompressionAlgorithm, - _in_net_packet: &NetPacket, - _out: &mut NetPacket, - ) -> anyhow::Result<()> { - Err(anyhow!("Unsupported decompress")) - } -} - -#[cfg(any(feature = "lz4_compress", feature = "zstd_compress"))] -impl Compressor { - pub fn compress, O: AsRef<[u8]> + AsMut<[u8]>>( - &self, - in_net_packet: &NetPacket, - out: &mut NetPacket, - ) -> anyhow::Result { - match self { - #[cfg(feature = "lz4_compress")] - Compressor::Lz4 => { - if in_net_packet.data_len() < 128 { - return Ok(false); - } - Lz4Compressor::compress(in_net_packet, out)?; - let mut compression_extension_tail = out.append_compression_extension_tail()?; - compression_extension_tail.set_algorithm(CompressionAlgorithm::Lz4); - //压缩没效果,则放弃压缩 - if out.data_len() >= in_net_packet.data_len() - 16 { - return Ok(false); - } - return Ok(true); - } - #[cfg(feature = "zstd_compress")] - Compressor::Zstd(level) => { - if in_net_packet.data_len() < 128 { - return Ok(false); - } - ZstdCompressor::compress(*level, in_net_packet, out)?; - let mut compression_extension_tail = out.append_compression_extension_tail()?; - compression_extension_tail.set_algorithm(CompressionAlgorithm::Zstd); - //压缩没效果,则放弃压缩 - if out.data_len() >= in_net_packet.data_len() - 16 { - return Ok(false); - } - return Ok(true); - } - Compressor::None => {} - } - Ok(false) - } - pub fn decompress, O: AsRef<[u8]> + AsMut<[u8]>>( - algorithm: CompressionAlgorithm, - in_net_packet: &NetPacket, - out: &mut NetPacket, - ) -> anyhow::Result<()> { - match algorithm { - #[cfg(feature = "lz4_compress")] - CompressionAlgorithm::Lz4 => Lz4Compressor::decompress(in_net_packet, out), - #[cfg(feature = "zstd_compress")] - CompressionAlgorithm::Zstd => ZstdCompressor::decompress(in_net_packet, out), - _ => Err(anyhow!("Unknown decompress {:?}", algorithm)), - } - } -} - -#[test] -#[cfg(feature = "zstd_compress")] -fn test_lz4() { - use crate::protocol::extension::{CompressionAlgorithm, ExtensionTailPacket}; - let lz4 = Compressor::Lz4; - let in_packet = NetPacket::new([ - 65, 108, 105, 99, 101, 32, 119, 97, 116, 32, 98, 101, 103, 105, 110, 110, 105, 110, 103, - 32, 116, 111, 32, 103, 101, 116, 32, 118, 101, 114, 121, 32, 116, 105, 114, 101, 100, 32, - 111, 102, 32, 115, 105, 116, 116, 105, 110, 103, 32, 98, 121, 32, 104, 101, 114, 32, 115, - 105, 115, 116, 101, 114, 32, 111, 110, 32, 116, 104, 101, 32, 98, 97, 110, 107, 44, 32, 97, - 110, 100, 32, 111, 102, 32, 104, 97, 118, 105, 110, 103, 32, 110, 111, 116, 104, 105, 110, - 103, 32, 116, 111, 32, 100, 111, 58, 32, 111, 110, 99, 101, 32, 111, 114, 32, 116, 119, - 105, 99, 101, 32, 115, 104, 101, 32, 104, 97, 100, 32, 112, 101, 101, 112, 101, 100, 32, - 105, 110, 116, 111, 32, 116, 104, 101, 32, 98, 111, 111, 107, 32, 104, 101, 114, 32, 115, - 105, 115, 116, 101, 114, 32, 119, 97, 115, 32, 114, 101, 97, 100, 105, 110, 103, 44, 32, - 98, 117, 116, 32, 105, 116, 32, 104, 97, 100, 32, 110, 111, 32, 112, 105, 99, 116, 117, - 114, 101, 115, 32, 111, 114, 32, 99, 111, 110, 118, 101, 114, 115, 97, 116, 105, - ]) - .unwrap(); - let mut out_packet = NetPacket::new([0; 1000]).unwrap(); - let mut src_out_packet = NetPacket::new([0; 1000]).unwrap(); - lz4.compress(&in_packet, &mut out_packet).unwrap(); - let tail = out_packet.split_tail_packet().unwrap(); - match tail { - ExtensionTailPacket::Compression(c) => match c.algorithm() { - CompressionAlgorithm::Lz4 => { - Compressor::decompress(CompressionAlgorithm::Lz4, &out_packet, &mut src_out_packet) - .unwrap(); - } - _ => { - unimplemented!() - } - }, - _ => { - unimplemented!() - } - } - assert!(!out_packet.is_extension()); - assert_eq!(in_packet.payload(), src_out_packet.payload()) -} -#[test] -#[cfg(feature = "zstd_compress")] -fn test_zstd() { - use crate::protocol::extension::{CompressionAlgorithm, ExtensionTailPacket}; - let zstd = Compressor::Zstd(22); - let in_packet = NetPacket::new([ - 65, 108, 105, 99, 101, 32, 119, 97, 115, 32, 98, 101, 103, 105, 110, 110, 105, 110, 103, - 32, 116, 111, 32, 103, 101, 116, 32, 118, 101, 114, 121, 32, 116, 105, 114, 101, 100, 32, - 111, 102, 32, 115, 105, 116, 116, 105, 110, 103, 32, 98, 121, 32, 104, 101, 114, 32, 115, - 105, 115, 116, 101, 114, 32, 111, 110, 32, 116, 104, 101, 32, 98, 97, 110, 107, 44, 32, 97, - 110, 100, 32, 111, 102, 32, 104, 97, 118, 105, 110, 103, 32, 110, 111, 116, 104, 105, 110, - 103, 32, 116, 111, 32, 100, 111, 58, 32, 111, 110, 99, 101, 32, 111, 114, 32, 116, 119, - 105, 99, 101, 32, 115, 104, 101, 32, 104, 97, 100, 32, 112, 101, 101, 112, 101, 100, 32, - 105, 110, 116, 111, 32, 116, 104, 101, 32, 98, 111, 111, 107, 32, 104, 101, 114, 32, 115, - 105, 115, 116, 101, 114, 32, 119, 97, 115, 32, 114, 101, 97, 100, 105, 110, 103, 44, 32, - 98, 117, 116, 32, 105, 116, 32, 104, 97, 100, 32, 110, 111, 32, 112, 105, 99, 116, 117, - 114, 101, 115, 32, 111, 114, 32, 99, 111, 110, 118, 101, 114, 115, 97, 116, 105, - ]) - .unwrap(); - let mut out_packet = NetPacket::new([0; 1000]).unwrap(); - let mut src_out_packet = NetPacket::new([0; 1000]).unwrap(); - zstd.compress(&in_packet, &mut out_packet).unwrap(); - let tail = out_packet.split_tail_packet().unwrap(); - match tail { - ExtensionTailPacket::Compression(c) => match c.algorithm() { - CompressionAlgorithm::Zstd => { - Compressor::decompress( - CompressionAlgorithm::Zstd, - &out_packet, - &mut src_out_packet, - ) - .unwrap(); - } - _ => { - unimplemented!() - } - }, - _ => { - unimplemented!() - } - } - assert!(!out_packet.is_extension()); - assert_eq!(in_packet.payload(), src_out_packet.payload()) -} diff --git a/vnt/src/compression/zstd_compress.rs b/vnt/src/compression/zstd_compress.rs deleted file mode 100644 index d647ecb..0000000 --- a/vnt/src/compression/zstd_compress.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::protocol::NetPacket; -use anyhow::anyhow; -use zstd::zstd_safe::CompressionLevel; - -#[derive(Clone)] -pub struct ZstdCompressor; - -impl ZstdCompressor { - pub fn compress, O: AsRef<[u8]> + AsMut<[u8]>>( - compression_level: CompressionLevel, - in_net_packet: &NetPacket, - out: &mut NetPacket, - ) -> anyhow::Result<()> { - out.set_data_len_max(); - let len = match zstd::zstd_safe::compress( - out.payload_mut(), - in_net_packet.payload(), - compression_level, - ) { - Ok(len) => len, - Err(e) => Err(anyhow!("zstd compress {}", e))?, - }; - out.set_payload_len(len)?; - Ok(()) - } - pub fn decompress, O: AsRef<[u8]> + AsMut<[u8]>>( - in_net_packet: &NetPacket, - out: &mut NetPacket, - ) -> anyhow::Result<()> { - out.set_data_len_max(); - let len = match zstd::zstd_safe::decompress(out.payload_mut(), in_net_packet.payload()) { - Ok(len) => len, - Err(e) => Err(anyhow!("zstd decompress {}", e))?, - }; - out.set_payload_len(len)?; - Ok(()) - } -} diff --git a/vnt/src/core/conn.rs b/vnt/src/core/conn.rs deleted file mode 100644 index 22cc3f6..0000000 --- a/vnt/src/core/conn.rs +++ /dev/null @@ -1,538 +0,0 @@ -use std::collections::HashMap; -use std::net::Ipv4Addr; -use std::ops::Deref; -use std::sync::Arc; -use std::time::Duration; - -use crossbeam_utils::atomic::AtomicCell; -use parking_lot::{Mutex, RwLock}; -use rand::Rng; - -use crate::channel::context::ChannelContext; -use crate::channel::idle::Idle; -use crate::channel::punch::{NatInfo, Punch}; -use crate::channel::sender::IpPacketSender; -use crate::channel::{init_channel, init_context, Route, RouteKey}; -use crate::cipher::Cipher; -#[cfg(feature = "server_encrypt")] -use crate::cipher::RsaCipher; -use crate::compression::Compressor; -use crate::core::Config; -use crate::external_route::{AllowExternalRoute, ExternalRoute}; -use crate::handle::handshaker::Handshake; -use crate::handle::maintain::PunchReceiver; -use crate::handle::recv_data::RecvDataHandler; -use crate::handle::{maintain, BaseConfigInfo, ConnectStatus, CurrentDeviceInfo, PeerDeviceInfo}; -use crate::nat::NatTest; -#[cfg(feature = "integrated_tun")] -use crate::tun_tap_device::tun_create_helper::{DeviceAdapter, TunDeviceHelper}; -use crate::tun_tap_device::vnt_device::DeviceWrite; -use crate::util::limit::TrafficMeterMultiAddress; -use crate::util::{Scheduler, StopManager}; -use crate::{nat, VntCallback}; - -#[derive(Clone)] -pub struct Vnt { - inner: Arc, -} - -impl Vnt { - #[cfg(feature = "integrated_tun")] - pub fn new(config: Config, callback: Call) -> anyhow::Result { - let inner = Arc::new(VntInner::new(config, callback)?); - Ok(Self { inner }) - } - #[cfg(not(feature = "integrated_tun"))] - pub fn new_device( - config: Config, - callback: Call, - device: Device, - ) -> anyhow::Result { - let inner = Arc::new(VntInner::new_device(config, callback, device)?); - Ok(Self { inner }) - } -} - -impl Deref for Vnt { - type Target = VntInner; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -pub struct VntInner { - stop_manager: StopManager, - config: Config, - current_device: Arc>, - nat_test: NatTest, - device_map: Arc)>>, - context: Arc>>, - peer_nat_info_map: Arc>>, - client_secret_hash: Option<[u8; 16]>, - compressor: Compressor, - client_cipher: Cipher, - server_cipher: Cipher, - external_route: ExternalRoute, - up_traffic_meter: Option, - down_traffic_meter: Option, -} - -impl VntInner { - #[cfg(feature = "integrated_tun")] - pub fn new(config: Config, callback: Call) -> anyhow::Result { - VntInner::new_device0(config, callback, DeviceAdapter::default()) - } - #[cfg(not(feature = "integrated_tun"))] - pub fn new_device( - config: Config, - callback: Call, - device: Device, - ) -> anyhow::Result { - VntInner::new_device0(config, callback, device) - } - fn new_device0( - config: Config, - callback: Call, - device: Device, - ) -> anyhow::Result { - log::info!("config: {:?}", config); - let (up_traffic_meter, down_traffic_meter) = if config.enable_traffic { - ( - Some(TrafficMeterMultiAddress::default()), - Some(TrafficMeterMultiAddress::default()), - ) - } else { - (None, None) - }; - - //服务端非对称加密 - #[cfg(feature = "server_encrypt")] - let rsa_cipher: Arc>> = Arc::new(Mutex::new(None)); - //服务端对称加密 - let server_cipher: Cipher = if config.server_encrypt { - let mut key = [0u8; 32]; - rand::thread_rng().fill(&mut key); - Cipher::new_key(key, config.token.clone())? - } else { - Cipher::None - }; - let finger = if config.finger { - Some(config.token.clone()) - } else { - None - }; - //客户端对称加密 - let client_cipher = - Cipher::new_password(config.cipher_model, config.password.clone(), finger)?; - //当前设备信息 - let current_device = Arc::new(AtomicCell::new(CurrentDeviceInfo::new0( - config.server_address, - ))); - //设备列表 - let device_map: Arc)>> = - Arc::new(Mutex::new((0, HashMap::with_capacity(16)))); - let local_ipv4 = if let Some(local_ipv4) = config.local_ipv4 { - Some(local_ipv4) - } else { - nat::local_ipv4() - }; - let default_interface = config.local_interface.clone(); - - //基础信息 - let config_info = BaseConfigInfo::new( - config.name.clone(), - config.token.clone(), - config.ip, - config.password_hash(), - config.server_encrypt, - config.device_id.clone(), - config.server_address_str.clone(), - config.name_servers.clone(), - config.mtu.unwrap_or(1420), - #[cfg(feature = "integrated_tun")] - #[cfg(target_os = "windows")] - config.tap, - #[cfg(feature = "integrated_tun")] - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - config.device_name.clone(), - config.allow_wire_guard, - default_interface.clone(), - ); - // 服务停止管理器 - let stop_manager = { - let callback = callback.clone(); - StopManager::new(move || callback.stop()) - }; - #[cfg(feature = "port_mapping")] - crate::port_mapping::start_port_mapping( - stop_manager.clone(), - config.port_mapping_list.clone(), - )?; - let mut ports = config.ports.as_ref().map_or(vec![0, 0], |v| { - if v.is_empty() { - vec![0, 0] - } else { - v.clone() - } - }); - if config.use_channel_type.is_only_relay() { - //中继模式下只监听一个端口就够了 - ports.truncate(1); - } - //通道上下文 - let (context, tcp_listener) = init_context( - ports, - config.use_channel_type, - config.first_latency, - config.protocol, - config.packet_loss_rate, - config.packet_delay, - default_interface, - up_traffic_meter.clone(), - down_traffic_meter.clone(), - )?; - let local_ipv6 = nat::local_ipv6(); - let udp_ports = context.main_local_udp_port()?; - let tcp_port = tcp_listener.local_addr()?.port(); - //nat检测工具 - let nat_test = NatTest::new( - context.channel_num(), - config.stun_server.clone(), - local_ipv4, - local_ipv6, - udp_ports, - tcp_port, - config.local_ipv4.is_none(), - config.punch_model, - ); - // 定时器 - let scheduler = Scheduler::new(stop_manager.clone())?; - let external_route = ExternalRoute::new(config.in_ips.clone()); - let out_external_route = AllowExternalRoute::new(config.out_ips.clone()); - - #[cfg(feature = "ip_proxy")] - #[cfg(feature = "integrated_tun")] - let proxy_map = if !config.out_ips.is_empty() && !config.no_proxy { - Some(crate::ip_proxy::init_proxy( - context.clone(), - stop_manager.clone(), - current_device.clone(), - client_cipher.clone(), - )?) - } else { - None - }; - let (punch_sender, punch_receiver) = maintain::punch_channel(); - let peer_nat_info_map: Arc>> = - Arc::new(RwLock::new(HashMap::with_capacity(16))); - let handshake = Handshake::new( - #[cfg(feature = "server_encrypt")] - rsa_cipher.clone(), - ); - #[cfg(feature = "integrated_tun")] - let tun_device_helper = { - TunDeviceHelper::new( - stop_manager.clone(), - context.clone(), - current_device.clone(), - external_route.clone(), - #[cfg(feature = "ip_proxy")] - proxy_map.clone(), - client_cipher.clone(), - server_cipher.clone(), - device_map.clone(), - config.compressor, - device.clone().into_device_adapter(), - ) - }; - - let handler = RecvDataHandler::new( - #[cfg(feature = "server_encrypt")] - rsa_cipher, - server_cipher.clone(), - client_cipher.clone(), - current_device.clone(), - device, - device_map.clone(), - config_info.clone(), - nat_test.clone(), - callback.clone(), - punch_sender, - peer_nat_info_map.clone(), - external_route.clone(), - out_external_route, - #[cfg(feature = "ip_proxy")] - #[cfg(feature = "integrated_tun")] - proxy_map.clone(), - handshake.clone(), - #[cfg(feature = "integrated_tun")] - tun_device_helper, - ); - - //初始化网络数据通道 - let (udp_socket_sender, connect_util) = - init_channel(tcp_listener, context.clone(), stop_manager.clone(), handler)?; - // 打洞逻辑 - let punch = Punch::new( - context.clone(), - config.punch_model, - connect_util.clone(), - nat_test.clone(), - current_device.clone(), - ); - - // #[cfg(not(target_os = "android"))] - // tun_helper.start(device)?; - - maintain::idle_gateway( - &scheduler, - context.clone(), - current_device.clone(), - config_info.clone(), - connect_util.clone(), - callback.clone(), - 0, - handshake, - ); - { - let context = context.clone(); - let nat_test = nat_test.clone(); - let device_map = device_map.clone(); - let config_info = config_info.clone(); - let current_device = current_device.clone(); - if !config.use_channel_type.is_only_relay() { - // 定时nat探测 - maintain::retrieve_nat_type( - &scheduler, - context.clone(), - nat_test.clone(), - udp_socket_sender, - ); - } - let client_cipher = client_cipher.clone(); - let server_cipher = server_cipher.clone(); - //延迟启动 - scheduler.timeout(Duration::from_secs(1), move |scheduler| { - start( - scheduler, - context, - nat_test, - device_map, - current_device, - client_cipher, - server_cipher, - punch_receiver, - config_info, - punch, - callback, - ); - }); - } - let compressor = config.compressor; - Ok(Self { - stop_manager, - config, - current_device, - nat_test, - device_map, - context: Arc::new(Mutex::new(Some(context))), - peer_nat_info_map, - client_secret_hash: config_info.client_secret_hash, - compressor, - client_cipher, - server_cipher, - external_route, - up_traffic_meter, - down_traffic_meter, - }) - } -} - -pub fn start( - scheduler: &Scheduler, - context: ChannelContext, - nat_test: NatTest, - device_map: Arc)>>, - current_device: Arc>, - client_cipher: Cipher, - server_cipher: Cipher, - punch_receiver: PunchReceiver, - config_info: BaseConfigInfo, - punch: Punch, - callback: Call, -) { - // 定时心跳 - maintain::heartbeat( - &scheduler, - context.clone(), - current_device.clone(), - device_map.clone(), - client_cipher.clone(), - server_cipher.clone(), - ); - // 路由空闲检测逻辑 - let idle = Idle::new(Duration::from_secs(10), context.clone()); - // 定时空闲检查 - maintain::idle_route( - &scheduler, - idle, - context.clone(), - current_device.clone(), - callback, - ); - // 定时客户端中继检测 - if !context.use_channel_type().is_only_p2p() { - maintain::client_relay( - &scheduler, - context.clone(), - current_device.clone(), - device_map.clone(), - client_cipher.clone(), - ); - } - - if !context.use_channel_type().is_only_relay() { - // 定时地址探测 - maintain::addr_request( - &scheduler, - context.clone(), - current_device.clone(), - nat_test.clone(), - config_info.clone(), - ); - // 定时打洞 - maintain::punch( - &scheduler, - context.clone(), - nat_test.clone(), - device_map.clone(), - current_device.clone(), - client_cipher.clone(), - punch_receiver, - punch, - ); - } - maintain::up_status(scheduler, context.clone(), current_device.clone()) -} - -impl VntInner { - pub fn name(&self) -> &str { - &self.config.name - } - pub fn server_encrypt(&self) -> bool { - self.config.server_encrypt - } - pub fn client_encrypt(&self) -> bool { - self.config.password.is_some() - } - pub fn client_encrypt_hash(&self) -> Option<&[u8]> { - self.client_secret_hash.as_ref().map(|v| v.as_ref()) - } - pub fn current_device(&self) -> CurrentDeviceInfo { - self.current_device.load() - } - pub fn current_device_info(&self) -> Arc> { - self.current_device.clone() - } - pub fn peer_nat_info(&self, ip: &Ipv4Addr) -> Option { - self.peer_nat_info_map.read().get(ip).cloned() - } - pub fn connection_status(&self) -> ConnectStatus { - self.current_device.load().status - } - pub fn nat_info(&self) -> NatInfo { - self.nat_test.nat_info() - } - pub fn device_list(&self) -> Vec { - let device_list_lock = self.device_map.lock(); - let (_epoch, device_list) = device_list_lock.clone(); - drop(device_list_lock); - device_list.into_values().collect() - } - pub fn route(&self, ip: &Ipv4Addr) -> Option { - self.context.lock().as_ref()?.route_table.route_one(ip) - } - pub fn is_gateway(&self, ip: &Ipv4Addr) -> bool { - self.current_device.load().is_gateway(ip) - } - pub fn route_key(&self, route_key: &RouteKey) -> Option { - self.context - .lock() - .as_ref()? - .route_table - .route_to_id(route_key) - } - pub fn route_table(&self) -> Vec<(Ipv4Addr, Vec)> { - if let Some(context) = self.context.lock().as_ref() { - context.route_table.route_table() - } else { - vec![] - } - } - pub fn up_stream(&self) -> u64 { - self.up_traffic_meter.as_ref().map_or(0, |v| v.total()) - } - pub fn up_stream_all(&self) -> Option<(u64, HashMap)> { - self.up_traffic_meter.as_ref().map(|v| v.get_all()) - } - pub fn up_stream_history(&self) -> Option<(u64, HashMap)>)> { - self.up_traffic_meter.as_ref().map(|v| v.get_all_history()) - } - pub fn down_stream(&self) -> u64 { - self.down_traffic_meter.as_ref().map_or(0, |v| v.total()) - } - pub fn down_stream_all(&self) -> Option<(u64, HashMap)> { - self.down_traffic_meter.as_ref().map(|v| v.get_all()) - } - pub fn down_stream_history(&self) -> Option<(u64, HashMap)>)> { - self.down_traffic_meter - .as_ref() - .map(|v| v.get_all_history()) - } - pub fn stop(&self) { - //退出协助回收资源 - let _ = self.context.lock().take(); - self.stop_manager.stop() - } - pub fn is_stopped(&self) -> bool { - self.stop_manager.is_stopped() - } - pub fn add_stop_listener(&self, name: String, f: F) -> anyhow::Result - where - F: FnOnce() + Send + 'static, - { - self.stop_manager.add_listener(name, f) - } - pub fn wait(&self) { - self.stop_manager.wait() - } - pub fn wait_timeout(&self, dur: Duration) -> bool { - self.stop_manager.wait_timeout(dur) - } - pub fn config(&self) -> &Config { - &self.config - } - pub fn ipv4_packet_sender(&self) -> Option { - if let Some(c) = self.context.lock().as_ref() { - Some(IpPacketSender::new( - c.clone(), - self.current_device.clone(), - self.compressor.clone(), - self.client_cipher.clone(), - self.server_cipher.clone(), - self.external_route.clone(), - self.device_map.clone(), - self.config.allow_wire_guard, - )) - } else { - None - } - } -} - -impl Drop for VntInner { - fn drop(&mut self) { - self.stop(); - } -} diff --git a/vnt/src/core/mod.rs b/vnt/src/core/mod.rs deleted file mode 100644 index c55262e..0000000 --- a/vnt/src/core/mod.rs +++ /dev/null @@ -1,247 +0,0 @@ -use anyhow::anyhow; -use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; -use std::str::FromStr; - -pub use conn::Vnt; - -use crate::channel::punch::PunchModel; -use crate::channel::socket::LocalInterface; -use crate::channel::{ConnectProtocol, UseChannelType}; -use crate::cipher::CipherModel; -use crate::compression::Compressor; -use crate::util::{address_choose, dns_query_all}; - -mod conn; - -#[derive(Clone, Debug)] -pub struct Config { - #[cfg(feature = "integrated_tun")] - #[cfg(target_os = "windows")] - pub tap: bool, - pub token: String, - pub device_id: String, - pub name: String, - pub server_address: SocketAddr, - pub server_address_str: String, - pub name_servers: Vec, - pub stun_server: Vec, - pub in_ips: Vec<(u32, u32, Ipv4Addr)>, - pub out_ips: Vec<(u32, u32)>, - pub password: Option, - pub mtu: Option, - pub protocol: ConnectProtocol, - pub ip: Option, - #[cfg(feature = "ip_proxy")] - #[cfg(feature = "integrated_tun")] - pub no_proxy: bool, - pub server_encrypt: bool, - pub cipher_model: CipherModel, - pub finger: bool, - pub punch_model: PunchModel, - pub ports: Option>, - pub first_latency: bool, - #[cfg(feature = "integrated_tun")] - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - pub device_name: Option, - pub use_channel_type: UseChannelType, - //控制丢包率 - pub packet_loss_rate: Option, - pub packet_delay: u32, - // 端口映射 - #[cfg(feature = "port_mapping")] - pub port_mapping_list: Vec<(bool, SocketAddr, String)>, - pub compressor: Compressor, - pub enable_traffic: bool, - pub allow_wire_guard: bool, - pub local_ipv4: Option, - pub local_interface: LocalInterface, -} - -impl Config { - pub fn new( - #[cfg(feature = "integrated_tun")] - #[cfg(target_os = "windows")] - tap: bool, - token: String, - device_id: String, - name: String, - server_address_str: String, - mut name_servers: Vec, - mut stun_server: Vec, - mut in_ips: Vec<(u32, u32, Ipv4Addr)>, - out_ips: Vec<(u32, u32)>, - password: Option, - mtu: Option, - ip: Option, - #[cfg(feature = "integrated_tun")] - #[cfg(feature = "ip_proxy")] - no_proxy: bool, - server_encrypt: bool, - cipher_model: CipherModel, - finger: bool, - punch_model: PunchModel, - ports: Option>, - first_latency: bool, - #[cfg(feature = "integrated_tun")] - #[cfg(not(target_os = "android"))] - device_name: Option, - use_channel_type: UseChannelType, - packet_loss_rate: Option, - packet_delay: u32, - // 例如 [udp:127.0.0.1:80->10.26.0.10:8080,tcp:127.0.0.1:80->10.26.0.10:8080] - #[cfg(feature = "port_mapping")] port_mapping_list: Vec, - compressor: Compressor, - enable_traffic: bool, - // 允许传递wg流量 - allow_wire_guard: bool, - local_dev: Option, - ) -> anyhow::Result { - for x in stun_server.iter_mut() { - if !x.contains(":") { - x.push_str(":3478"); - } - } - for x in name_servers.iter_mut() { - if Ipv6Addr::from_str(x).is_ok() { - x.push_str(":53"); - } else if !x.contains(":") { - x.push_str(":53"); - } - } - if token.is_empty() || token.len() > 128 { - return Err(anyhow!("token too long")); - } - if device_id.is_empty() || device_id.len() > 128 { - return Err(anyhow!("device_id too long")); - } - if name.is_empty() || name.len() > 128 { - return Err(anyhow!("name too long")); - } - let mut server_address_str = server_address_str.to_lowercase(); - let mut _query_dns = true; - let mut protocol = ConnectProtocol::UDP; - if server_address_str.starts_with("ws://") { - #[cfg(not(feature = "ws"))] - Err(anyhow!("Ws not supported"))?; - protocol = ConnectProtocol::WS; - _query_dns = false; - } - if server_address_str.starts_with("wss://") { - #[cfg(not(feature = "wss"))] - Err(anyhow!("Wss not supported"))?; - protocol = ConnectProtocol::WSS; - _query_dns = false; - } - - let mut server_address = "0.0.0.0:0".parse().unwrap(); - if _query_dns { - if let Some(s) = server_address_str.strip_prefix("udp://") { - server_address_str = s.to_string(); - } else if let Some(s) = server_address_str.strip_prefix("tcp://") { - server_address_str = s.to_string(); - protocol = ConnectProtocol::TCP; - } - let address_result = dns_query_all( - &server_address_str, - name_servers.clone(), - &LocalInterface::default(), - ); - match address_result { - Ok(address) => match address_choose(address) { - Ok(resolved_address) => { - server_address = resolved_address; - } - Err(e) => { - log::error!("Failed to choose address: {}", e); - println!("Failed to choose address: {}", e); - } - }, - Err(e) => { - log::error!("DNS query failed: {}", e); - println!("DNS query failed: {}", e); - } - } - } - - #[cfg(feature = "port_mapping")] - let port_mapping_list = crate::port_mapping::convert(port_mapping_list)?; - - for (dest, mask, _) in &mut in_ips { - *dest = *mask & *dest; - } - in_ips.sort_by(|(dest1, _, _), (dest2, _, _)| dest2.cmp(dest1)); - let (local_interface, local_ipv4) = if let Some(local_dev) = local_dev { - let (default_interface, ip) = crate::channel::socket::get_interface(local_dev)?; - log::info!("default_interface = {:?} local_ip= {ip}", default_interface); - (default_interface, Some(ip)) - } else { - (LocalInterface::default(), None) - }; - Ok(Self { - #[cfg(feature = "integrated_tun")] - #[cfg(target_os = "windows")] - tap, - token, - device_id, - name, - server_address, - server_address_str, - name_servers, - stun_server, - in_ips, - out_ips, - password, - mtu, - protocol, - ip, - #[cfg(feature = "ip_proxy")] - #[cfg(feature = "integrated_tun")] - no_proxy, - server_encrypt, - cipher_model, - finger, - punch_model, - ports, - first_latency, - #[cfg(feature = "integrated_tun")] - #[cfg(not(target_os = "android"))] - device_name, - use_channel_type, - packet_loss_rate, - packet_delay, - #[cfg(feature = "port_mapping")] - port_mapping_list, - compressor, - enable_traffic, - allow_wire_guard, - local_ipv4, - local_interface, - }) - } -} - -impl Config { - pub fn password_hash(&self) -> Option<[u8; 16]> { - if let Some(p) = self.password.as_ref() { - match self.cipher_model { - CipherModel::Xor => { - let key = crate::cipher::simple_hash(&format!("Xor{}{}", p, self.token)); - Some(key[16..].try_into().unwrap()) - } - CipherModel::None => None, - #[cfg(cipher)] - _ => { - use sha2::Digest; - let mut hasher = sha2::Sha256::new(); - hasher.update(self.cipher_model.to_string().as_bytes()); - hasher.update(p.as_bytes()); - hasher.update(self.token.as_bytes()); - let key: [u8; 32] = hasher.finalize().into(); - Some(key[16..].try_into().unwrap()) - } - } - } else { - None - } - } -} diff --git a/vnt/src/external_route/mod.rs b/vnt/src/external_route/mod.rs deleted file mode 100644 index ad95bbe..0000000 --- a/vnt/src/external_route/mod.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::net::Ipv4Addr; -use std::sync::Arc; - -// 目标网段,子网掩码,网关 -#[derive(Clone)] -pub struct ExternalRoute { - route_table: Vec<(u32, u32, Ipv4Addr)>, -} - -impl ExternalRoute { - pub fn new(mut route_table: Vec<(u32, u32, Ipv4Addr)>) -> Self { - for (dest, mask, _) in &mut route_table { - *dest = *mask & *dest; - } - route_table.sort_by(|(dest1, _, _), (dest2, _, _)| dest2.cmp(dest1)); - Self { route_table } - } - pub fn route(&self, ip: &Ipv4Addr) -> Option { - if self.route_table.is_empty() { - return None; - } - let ip = u32::from_be_bytes(ip.octets()); - for (dest, mask, gateway) in self.route_table.iter() { - if *mask & ip == *dest { - return Some(*gateway); - } - } - None - } - pub fn to_route(&self) -> Vec<(Ipv4Addr, Ipv4Addr)> { - self.route_table - .iter() - .map(|(dest, mask, _)| (Ipv4Addr::from(*dest), Ipv4Addr::from(*mask))) - .collect::>() - } -} - -// 目标网段,子网掩码 -#[derive(Clone)] -pub struct AllowExternalRoute { - route_table: Arc>, -} - -impl AllowExternalRoute { - pub fn new(mut route_table: Vec<(u32, u32)>) -> Self { - for (dest, mask) in &mut route_table { - *dest = *mask & *dest; - } - route_table.sort_by(|(dest1, _), (dest2, _)| dest2.cmp(dest1)); - Self { - route_table: Arc::new(route_table), - } - } - pub fn allow(&self, ip: &Ipv4Addr) -> bool { - if self.route_table.is_empty() { - return false; - } - let ip = u32::from_be_bytes(ip.octets()); - for (dest, mask) in self.route_table.iter() { - if *mask & ip == *mask & *dest { - return true; - } - } - false - } -} diff --git a/vnt/src/handle/callback.rs b/vnt/src/handle/callback.rs deleted file mode 100644 index 7f32eb4..0000000 --- a/vnt/src/handle/callback.rs +++ /dev/null @@ -1,322 +0,0 @@ -use crate::handle::PeerDeviceStatus; -#[cfg(feature = "server_encrypt")] -use rsa::RsaPublicKey; -use std::fmt::{Display, Formatter}; -use std::io; -use std::net::{Ipv4Addr, SocketAddr}; - -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] -#[derive(Debug)] -pub struct DeviceInfo { - pub name: String, - pub version: String, -} - -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] -impl Display for DeviceInfo { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(&format!("name={} ,version={}", self.name, self.version)) - } -} - -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] -impl DeviceInfo { - pub fn new(name: String, version: String) -> Self { - return Self { name, version }; - } -} - -#[derive(Debug)] -pub struct ConnectInfo { - // 第几次连接,从1开始 - pub count: usize, - // 服务端地址 - pub address: SocketAddr, -} - -impl Display for ConnectInfo { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(&format!("count={} ,address={}", self.count, self.address)) - } -} - -impl ConnectInfo { - pub fn new(count: usize, address: SocketAddr) -> Self { - Self { count, address } - } -} - -#[derive(Debug)] -pub struct HandshakeInfo { - //服务端公钥 - #[cfg(feature = "server_encrypt")] - pub public_key: Option, - //服务端指纹 - #[cfg(feature = "server_encrypt")] - pub finger: Option, - //服务端版本 - pub version: String, -} - -impl Display for HandshakeInfo { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - #[cfg(feature = "server_encrypt")] - return match &self.finger { - None => f.write_str(&format!("no_secret server version={}", self.version)), - Some(finger) => f.write_str(&format!( - "finger={} ,server version={}", - finger, self.version - )), - }; - #[cfg(not(feature = "server_encrypt"))] - f.write_str(&format!("server version={}", self.version)) - } -} - -#[cfg(feature = "server_encrypt")] -impl HandshakeInfo { - pub fn new(public_key: RsaPublicKey, finger: String, version: String) -> Self { - Self { - public_key: Some(public_key), - finger: Some(finger), - version, - } - } - pub fn new_no_secret(version: String) -> Self { - Self { - public_key: None, - finger: None, - version, - } - } -} - -#[cfg(not(feature = "server_encrypt"))] -impl HandshakeInfo { - pub fn new_no_secret(version: String) -> Self { - Self { version } - } -} - -#[derive(Debug)] -pub struct RegisterInfo { - //本机虚拟IP - pub virtual_ip: Ipv4Addr, - //子网掩码 - pub virtual_netmask: Ipv4Addr, - //虚拟网关 - pub virtual_gateway: Ipv4Addr, -} - -impl Display for RegisterInfo { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(&format!( - "ip={} ,netmask={} ,gateway={}", - self.virtual_ip, self.virtual_netmask, self.virtual_gateway, - )) - } -} - -impl RegisterInfo { - pub fn new(virtual_ip: Ipv4Addr, virtual_netmask: Ipv4Addr, virtual_gateway: Ipv4Addr) -> Self { - Self { - virtual_ip, - virtual_netmask, - virtual_gateway, - } - } -} - -#[derive(Debug)] -pub struct ErrorInfo { - pub code: ErrorType, - pub msg: Option, - pub source: Option, -} - -impl Display for ErrorInfo { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(&format!("ErrorType={:?} ", self.code))?; - if let Some(msg) = &self.msg { - f.write_str(&format!(",msg={:?} ", msg))?; - } - if let Some(source) = &self.source { - f.write_str(&format!(",source={:?} ", source))?; - } - Ok(()) - } -} - -impl ErrorInfo { - pub fn new(code: ErrorType) -> Self { - Self { - code, - msg: None, - source: None, - } - } - pub fn new_msg(code: ErrorType, msg: String) -> Self { - Self { - code, - msg: Some(msg), - source: None, - } - } -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum ErrorType { - TokenError, - Disconnect, - AddressExhausted, - IpAlreadyExists, - InvalidIp, - LocalIpExists, - FailedToCrateDevice, - Warn, - Unknown, -} - -impl Into for ErrorType { - fn into(self) -> u8 { - match self { - ErrorType::TokenError => 1, - ErrorType::Disconnect => 2, - ErrorType::AddressExhausted => 3, - ErrorType::IpAlreadyExists => 4, - ErrorType::InvalidIp => 5, - ErrorType::LocalIpExists => 6, - ErrorType::FailedToCrateDevice => 101, - ErrorType::Warn => 102, - ErrorType::Unknown => 255, - } - } -} - -#[derive(Clone, Debug)] -pub struct DeviceConfig { - #[cfg(feature = "integrated_tun")] - #[cfg(target_os = "windows")] - pub tap: bool, - #[cfg(feature = "integrated_tun")] - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - pub device_name: Option, - //虚拟网卡mtu值 - pub mtu: u32, - //本机虚拟IP - pub virtual_ip: Ipv4Addr, - //子网掩码 - pub virtual_netmask: Ipv4Addr, - //虚拟网关 - pub virtual_gateway: Ipv4Addr, - //虚拟网段 - pub virtual_network: Ipv4Addr, - // 额外的路由 - pub external_route: Vec<(Ipv4Addr, Ipv4Addr)>, -} - -impl DeviceConfig { - pub fn new( - #[cfg(feature = "integrated_tun")] - #[cfg(target_os = "windows")] - tap: bool, - #[cfg(feature = "integrated_tun")] - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - device_name: Option, - mtu: u32, - virtual_ip: Ipv4Addr, - virtual_netmask: Ipv4Addr, - virtual_gateway: Ipv4Addr, - virtual_network: Ipv4Addr, - external_route: Vec<(Ipv4Addr, Ipv4Addr)>, - ) -> Self { - Self { - #[cfg(feature = "integrated_tun")] - #[cfg(target_os = "windows")] - tap, - #[cfg(feature = "integrated_tun")] - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - device_name, - mtu, - virtual_ip, - virtual_netmask, - virtual_gateway, - virtual_network, - external_route, - } - } -} - -impl Display for DeviceConfig { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(&format!( - "ip={} ,netmask={} ,gateway={}, external_route={:?}", - self.virtual_ip, self.virtual_netmask, self.virtual_gateway, self.external_route - )) - } -} - -#[derive(Debug)] -pub struct PeerClientInfo { - pub virtual_ip: Ipv4Addr, - pub name: String, - pub status: PeerDeviceStatus, - pub client_secret: bool, -} - -impl PeerClientInfo { - pub fn new( - virtual_ip: Ipv4Addr, - name: String, - status: PeerDeviceStatus, - client_secret: bool, - ) -> Self { - Self { - virtual_ip, - name, - status, - client_secret, - } - } -} - -impl Display for PeerClientInfo { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(&format!( - "ip={} ,name={} ,status={:?}, client_secret={}", - self.virtual_ip, self.name, self.status, self.client_secret - )) - } -} - -pub trait VntCallback: Clone + Send + Sync + 'static { - /// 启动成功 - fn success(&self) {} - - /// 创建网卡的信息 - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - #[cfg(feature = "integrated_tun")] - fn create_tun(&self, _info: DeviceInfo) {} - /// 连接 - fn connect(&self, _info: ConnectInfo) {} - /// 握手,返回false则拒绝握手,可在此处检查服务端信息 - fn handshake(&self, _info: HandshakeInfo) -> bool { - true - } - /// 注册,返回false则拒绝注册 - fn register(&self, _info: RegisterInfo) -> bool { - true - } - #[cfg(not(feature = "integrated_tun"))] - fn create_device(&self, _info: DeviceConfig) {} - #[cfg(target_os = "android")] - #[cfg(feature = "integrated_tun")] - fn generate_tun(&self, _info: DeviceConfig) -> usize { - 0 - } - fn peer_client_list(&self, _info: Vec) {} - /// 异常信息 - fn error(&self, _info: ErrorInfo) {} - /// 服务停止 - fn stop(&self) {} -} diff --git a/vnt/src/handle/extension/mod.rs b/vnt/src/handle/extension/mod.rs deleted file mode 100644 index 45e13cb..0000000 --- a/vnt/src/handle/extension/mod.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::compression::Compressor; -use crate::protocol::extension::ExtensionTailPacket; -use crate::protocol::NetPacket; -use anyhow::anyhow; - -pub fn handle_extension_tail + AsMut<[u8]>, O: AsRef<[u8]> + AsMut<[u8]>>( - in_net_packet: &mut NetPacket, - out: &mut NetPacket, -) -> anyhow::Result { - if in_net_packet.is_extension() { - let tail_packet = in_net_packet.split_tail_packet()?; - match tail_packet { - ExtensionTailPacket::Compression(extension) => { - let compression_algorithm = extension.algorithm(); - Compressor::decompress(compression_algorithm, &in_net_packet, out)?; - out.head_mut().copy_from_slice(in_net_packet.head()); - Ok(true) - } - ExtensionTailPacket::Unknown => Err(anyhow!("Unknown decompress")), - } - } else { - Ok(false) - } -} diff --git a/vnt/src/handle/handshaker.rs b/vnt/src/handle/handshaker.rs deleted file mode 100644 index bd8a401..0000000 --- a/vnt/src/handle/handshaker.rs +++ /dev/null @@ -1,112 +0,0 @@ -use std::io; -use std::net::SocketAddr; -use std::sync::Arc; -use std::time::{Duration, Instant}; - -use crossbeam_utils::atomic::AtomicCell; -#[cfg(feature = "server_encrypt")] -use parking_lot::Mutex; -use protobuf::Message; - -use crate::channel::context::ChannelContext; -#[cfg(feature = "server_encrypt")] -use crate::cipher::RsaCipher; -use crate::handle::{GATEWAY_IP, SELF_IP}; -use crate::proto::message::HandshakeRequest; -#[cfg(feature = "server_encrypt")] -use crate::proto::message::SecretHandshakeRequest; -#[cfg(feature = "server_encrypt")] -use crate::protocol::body::RSA_ENCRYPTION_RESERVED; -use crate::protocol::{service_packet, NetPacket, Protocol, MAX_TTL}; - -#[derive(Clone)] -pub struct Handshake { - time: Arc>, - #[cfg(feature = "server_encrypt")] - rsa_cipher: Arc>>, -} -impl Handshake { - pub fn new( - #[cfg(feature = "server_encrypt")] rsa_cipher: Arc>>, - ) -> Self { - Handshake { - time: Arc::new(AtomicCell::new( - Instant::now() - .checked_sub(Duration::from_secs(60)) - .unwrap_or(Instant::now()), - )), - #[cfg(feature = "server_encrypt")] - rsa_cipher, - } - } - pub fn send(&self, context: &ChannelContext, secret: bool, addr: SocketAddr) -> io::Result<()> { - let last = self.time.load(); - //短时间不重复发送 - if last.elapsed() < Duration::from_secs(3) { - return Ok(()); - } - let request_packet = self.handshake_request_packet(secret)?; - log::info!("发送握手请求,secret={},{:?}", secret, addr); - context.send_default(&request_packet, addr)?; - self.time.store(Instant::now()); - Ok(()) - } - /// 第一次握手数据 - pub fn handshake_request_packet(&self, secret: bool) -> io::Result>> { - let mut request = HandshakeRequest::new(); - request.secret = secret; - request.version = crate::VNT_VERSION.to_string(); - #[cfg(feature = "server_encrypt")] - if let Some(finger) = self.rsa_cipher.lock().as_ref().map(|v| v.finger().clone()) { - request.key_finger = finger; - } - let bytes = request.write_to_bytes().map_err(|e| { - io::Error::new( - io::ErrorKind::Other, - format!("handshake_request_packet {:?}", e), - ) - })?; - let buf = vec![0u8; 12 + bytes.len()]; - let mut net_packet = NetPacket::new(buf)?; - net_packet.set_default_version(); - net_packet.set_gateway_flag(true); - net_packet.set_destination(GATEWAY_IP); - net_packet.set_source(SELF_IP); - net_packet.set_protocol(Protocol::Service); - net_packet.set_transport_protocol(service_packet::Protocol::HandshakeRequest.into()); - net_packet.first_set_ttl(MAX_TTL); - net_packet.set_payload(&bytes)?; - Ok(net_packet) - } -} - -/// 第二次加密握手 -#[cfg(feature = "server_encrypt")] -pub fn secret_handshake_request_packet( - rsa_cipher: &RsaCipher, - token: String, - key: &[u8], -) -> io::Result>> { - let mut request = SecretHandshakeRequest::new(); - request.token = token; - request.key = key.to_vec(); - let bytes = request.write_to_bytes().map_err(|e| { - io::Error::new( - io::ErrorKind::Other, - format!("secret_handshake_request_packet {:?}", e), - ) - })?; - let mut net_packet = NetPacket::new0( - 12 + bytes.len(), - vec![0u8; 12 + bytes.len() + RSA_ENCRYPTION_RESERVED], - )?; - net_packet.set_default_version(); - net_packet.set_gateway_flag(true); - net_packet.set_destination(GATEWAY_IP); - net_packet.set_source(SELF_IP); - net_packet.set_protocol(Protocol::Service); - net_packet.set_transport_protocol(service_packet::Protocol::SecretHandshakeRequest.into()); - net_packet.first_set_ttl(MAX_TTL); - net_packet.set_payload(&bytes)?; - Ok(rsa_cipher.encrypt(&mut net_packet)?) -} diff --git a/vnt/src/handle/maintain/addr_request.rs b/vnt/src/handle/maintain/addr_request.rs deleted file mode 100644 index fff00e8..0000000 --- a/vnt/src/handle/maintain/addr_request.rs +++ /dev/null @@ -1,71 +0,0 @@ -use std::sync::Arc; -use std::time::Duration; - -use crossbeam_utils::atomic::AtomicCell; - -use crate::channel::context::ChannelContext; -use crate::channel::punch::NatType; -use crate::handle::{BaseConfigInfo, CurrentDeviceInfo}; -use crate::nat::NatTest; -use crate::util::Scheduler; - -pub fn addr_request( - scheduler: &Scheduler, - context: ChannelContext, - current_device_info: Arc>, - nat_test: NatTest, - _config: BaseConfigInfo, -) { - pub_address_request(scheduler, context, current_device_info.clone(), nat_test, 0); -} - -fn pub_address_request( - scheduler: &Scheduler, - context: ChannelContext, - current_device_info: Arc>, - nat_test: NatTest, - count: usize, -) { - let channel_num = context.channel_num(); - let index = count % channel_num; - if let Err(e) = addr_request0(&context, ¤t_device_info, &nat_test, index) { - log::warn!("{:?}", e); - } - let nat_info = nat_test.nat_info(); - let time = if !nat_info.public_ports.contains(&0) && !nat_info.public_ips.is_empty() { - //对称网络探测端口没啥作用,把频率放低,(锥形网络也只在打洞前需要探测端口,后续可以改改) - if nat_info.nat_type == NatType::Symmetric { - 600 - } else { - if index == channel_num - 1 { - 19 - } else { - 9 - } - } - } else { - 3 - }; - - let rs = scheduler.timeout(Duration::from_secs(time), move |s| { - pub_address_request(s, context, current_device_info, nat_test, index + 1) - }); - if !rs { - log::info!("定时任务停止"); - } -} - -fn addr_request0( - context: &ChannelContext, - current_device: &AtomicCell, - nat_test: &NatTest, - index: usize, -) -> anyhow::Result<()> { - let current_dev = current_device.load(); - if current_dev.status.offline() { - return Ok(()); - } - let (data, addr) = nat_test.send_data()?; - context.send_main_udp(index, &data, addr)?; - Ok(()) -} diff --git a/vnt/src/handle/maintain/heartbeat.rs b/vnt/src/handle/maintain/heartbeat.rs deleted file mode 100644 index 5327a11..0000000 --- a/vnt/src/handle/maintain/heartbeat.rs +++ /dev/null @@ -1,260 +0,0 @@ -use std::collections::HashMap; -use std::net::Ipv4Addr; -use std::sync::Arc; -use std::time::Duration; - -use crossbeam_utils::atomic::AtomicCell; -use parking_lot::Mutex; -use rand::prelude::SliceRandom; - -use crate::channel::context::ChannelContext; -use crate::cipher::Cipher; -use crate::handle::{CurrentDeviceInfo, PeerDeviceInfo}; -use crate::protocol::body::ENCRYPTION_RESERVED; -use crate::protocol::control_packet::PingPacket; -use crate::protocol::{control_packet, NetPacket, Protocol}; -use crate::util::Scheduler; - -/// 定时发送心跳包 -pub fn heartbeat( - scheduler: &Scheduler, - context: ChannelContext, - current_device_info: Arc>, - device_map: Arc)>>, - client_cipher: Cipher, - server_cipher: Cipher, -) { - heartbeat0( - &context, - ¤t_device_info.load(), - &device_map, - &client_cipher, - &server_cipher, - ); - // 心跳包 3秒发送一次 - let rs = scheduler.timeout(Duration::from_secs(3), |s| { - heartbeat( - s, - context, - current_device_info, - device_map, - client_cipher, - server_cipher, - ) - }); - if !rs { - log::info!("定时任务停止"); - } -} - -fn heartbeat0( - context: &ChannelContext, - current_device: &CurrentDeviceInfo, - device_map: &Mutex<(u16, HashMap)>, - client_cipher: &Cipher, - server_cipher: &Cipher, -) { - let gateway_ip = current_device.virtual_gateway; - let src_ip = current_device.virtual_ip; - let channel_num = context.channel_num(); - // 可能服务器ip发生变化,导致发送失败 - let mut is_send_gateway = false; - match heartbeat_packet_server(device_map, server_cipher, src_ip, gateway_ip) { - Ok(net_packet) => { - if let Err(e) = context.send_default(&net_packet, current_device.connect_server) { - log::warn!("heartbeat err={:?}", e) - } else { - is_send_gateway = true - } - } - Err(e) => { - log::error!("heartbeat_packet err={:?}", e); - } - } - - for (dest_ip, routes) in context.route_table.route_table() { - let net_packet = if current_device.is_gateway(&dest_ip) { - if is_send_gateway { - continue; - } - heartbeat_packet_server(device_map, server_cipher, src_ip, gateway_ip) - } else { - heartbeat_packet_client(client_cipher, src_ip, dest_ip) - }; - let net_packet = match net_packet { - Ok(net_packet) => net_packet, - Err(e) => { - log::error!("heartbeat_packet err={:?}", e); - continue; - } - }; - for (index, route) in routes.iter().enumerate() { - let limit = if context.first_latency() { - channel_num + 1 - } else { - channel_num - }; - if index >= limit { - // 多余的通道不再发送心跳包,让它自动过期 - break; - } - if let Err(e) = context.send_by_key(&net_packet, route.route_key()) { - log::warn!("heartbeat err={:?}", e) - } - } - } - let peer_list = { device_map.lock().1.clone() }; - for peer in peer_list.values() { - if !peer.status.is_online() || peer.wireguard { - continue; - } - if current_device.is_gateway(&peer.virtual_ip) { - continue; - } - if current_device.status.offline() { - continue; - } - if context.route_table.route_one(&peer.virtual_ip).is_none() { - //路由为空,则向服务端地址发送 - let net_packet = match heartbeat_packet_client(client_cipher, src_ip, peer.virtual_ip) { - Ok(net_packet) => net_packet, - Err(e) => { - log::error!("heartbeat_packet err={:?}", e); - continue; - } - }; - if let Err(e) = context.send_default(&net_packet, current_device.connect_server) { - log::error!("heartbeat_packet send_default err={:?}", e); - } - } - } -} - -/// 客户端中继路径探测,延迟启动 -pub fn client_relay( - scheduler: &Scheduler, - context: ChannelContext, - current_device: Arc>, - device_map: Arc)>>, - client_cipher: Cipher, -) { - let rs = scheduler.timeout(Duration::from_secs(30), move |s| { - client_relay_(s, context, current_device, device_map, client_cipher) - }); - if !rs { - log::info!("定时任务停止"); - } -} - -/// 客户端中继路径探测,每30秒探测一次 -fn client_relay_( - scheduler: &Scheduler, - context: ChannelContext, - current_device: Arc>, - device_map: Arc)>>, - client_cipher: Cipher, -) { - if let Err(e) = client_relay0( - &context, - ¤t_device.load(), - &device_map, - &client_cipher, - ) { - log::error!("{:?}", e); - } - let rs = scheduler.timeout(Duration::from_secs(30), move |s| { - client_relay_(s, context, current_device, device_map, client_cipher) - }); - if !rs { - log::info!("定时任务停止"); - } -} - -fn client_relay0( - context: &ChannelContext, - current_device: &CurrentDeviceInfo, - device_map: &Mutex<(u16, HashMap)>, - client_cipher: &Cipher, -) -> anyhow::Result<()> { - // 离线了不再探测 - if current_device.status.offline() { - return Ok(()); - } - let peer_list = { device_map.lock().1.clone() }; - let mut routes = context.route_table.route_table_p2p(); - for peer in peer_list.values() { - if peer.wireguard - || !peer.status.is_online() - || peer.virtual_ip == current_device.virtual_ip - { - continue; - } - if context - .route_table - .route_one_p2p(&peer.virtual_ip) - .is_some() - && !context.first_latency() - { - continue; - } - let client_packet = - heartbeat_packet_client(client_cipher, current_device.virtual_ip, peer.virtual_ip)?; - - //随机发送到其他地址,看有没有客户端符合转发条件 - routes.shuffle(&mut rand::thread_rng()); - - for (index, (ip, route)) in routes.iter().enumerate() { - if current_device.is_gateway(ip) { - continue; - } - if let Err(e) = context.send_by_key(&client_packet, route.route_key()) { - log::error!("{:?}", e); - } - if index >= 2 { - break; - } - } - } - Ok(()) -} - -/// 构建心跳包 -fn heartbeat_packet( - src: Ipv4Addr, - dest: Ipv4Addr, -) -> anyhow::Result> { - let mut net_packet = NetPacket::new_encrypt([0u8; 12 + 4 + ENCRYPTION_RESERVED])?; - net_packet.set_default_version(); - net_packet.set_protocol(Protocol::Control); - net_packet.set_transport_protocol(control_packet::Protocol::Ping.into()); - net_packet.first_set_ttl(5); - net_packet.set_source(src); - net_packet.set_destination(dest); - let mut ping = PingPacket::new(net_packet.payload_mut())?; - ping.set_time(crate::handle::now_time() as u16); - Ok(net_packet) -} - -fn heartbeat_packet_client( - client_cipher: &Cipher, - src: Ipv4Addr, - dest: Ipv4Addr, -) -> anyhow::Result> { - let mut net_packet = heartbeat_packet(src, dest)?; - client_cipher.encrypt_ipv4(&mut net_packet)?; - Ok(net_packet) -} - -fn heartbeat_packet_server( - device_map: &Mutex<(u16, HashMap)>, - server_cipher: &Cipher, - src: Ipv4Addr, - dest: Ipv4Addr, -) -> anyhow::Result> { - let mut net_packet = heartbeat_packet(src, dest)?; - let mut ping = PingPacket::new(net_packet.payload_mut())?; - ping.set_epoch(device_map.lock().0); - net_packet.set_gateway_flag(true); - server_cipher.encrypt_ipv4(&mut net_packet)?; - Ok(net_packet) -} diff --git a/vnt/src/handle/maintain/idle.rs b/vnt/src/handle/maintain/idle.rs deleted file mode 100644 index 0e4edcd..0000000 --- a/vnt/src/handle/maintain/idle.rs +++ /dev/null @@ -1,210 +0,0 @@ -use std::io; -use std::sync::Arc; -use std::time::Duration; - -use crossbeam_utils::atomic::AtomicCell; - -use crate::channel::context::ChannelContext; -use crate::channel::idle::{Idle, IdleType}; -use crate::channel::sender::ConnectUtil; -use crate::channel::socket::LocalInterface; -use crate::channel::ConnectProtocol; -use crate::handle::callback::{ConnectInfo, ErrorType}; -use crate::handle::handshaker::Handshake; -use crate::handle::{BaseConfigInfo, ConnectStatus, CurrentDeviceInfo}; -use crate::util::{address_choose, dns_query_all, Scheduler}; -use crate::{ErrorInfo, VntCallback}; - -pub fn idle_route( - scheduler: &Scheduler, - idle: Idle, - context: ChannelContext, - current_device_info: Arc>, - call: Call, -) { - let delay = idle_route0(&idle, &context, ¤t_device_info, &call); - let rs = scheduler.timeout(delay, move |s| { - idle_route(s, idle, context, current_device_info, call) - }); - if !rs { - log::info!("定时任务停止"); - } -} - -pub fn idle_gateway( - scheduler: &Scheduler, - context: ChannelContext, - current_device_info: Arc>, - config: BaseConfigInfo, - connect_util: ConnectUtil, - call: Call, - mut connect_count: usize, - handshake: Handshake, -) { - idle_gateway0( - &context, - ¤t_device_info, - &config, - &connect_util, - &call, - &mut connect_count, - &handshake, - ); - let rs = scheduler.timeout(Duration::from_secs(8), move |s| { - idle_gateway( - s, - context, - current_device_info, - config, - connect_util, - call, - connect_count, - handshake, - ) - }); - if !rs { - log::info!("定时任务停止"); - } -} - -fn idle_gateway0( - context: &ChannelContext, - current_device: &AtomicCell, - config: &BaseConfigInfo, - connect_util: &ConnectUtil, - call: &Call, - connect_count: &mut usize, - handshake: &Handshake, -) { - if let Err(e) = check_gateway_channel( - context, - current_device, - config, - connect_util, - call, - connect_count, - handshake, - ) { - let cur = current_device.load(); - call.error(ErrorInfo::new_msg( - ErrorType::Disconnect, - format!("connect:{},error:{:?}", cur.connect_server, e), - )); - } -} - -fn idle_route0( - idle: &Idle, - context: &ChannelContext, - current_device: &AtomicCell, - call: &Call, -) -> Duration { - let cur = current_device.load(); - match idle.next_idle() { - IdleType::Timeout(ip, route) => { - log::info!("route Timeout {:?},{:?}", ip, route); - context.remove_route(&ip, route.route_key()); - if cur.is_gateway(&ip) { - //网关路由过期,则需要改变状态 - crate::handle::change_status(current_device, ConnectStatus::Connecting); - call.error(ErrorInfo::new(ErrorType::Disconnect)); - } - Duration::from_millis(100) - } - IdleType::Sleep(duration) => duration, - IdleType::None => Duration::from_millis(3000), - } -} - -fn check_gateway_channel( - context: &ChannelContext, - current_device_info: &AtomicCell, - config: &BaseConfigInfo, - connect_util: &ConnectUtil, - call: &Call, - count: &mut usize, - handshake: &Handshake, -) -> io::Result<()> { - let mut current_device = current_device_info.load(); - if current_device.status.offline() { - *count += 1; - let connect_protocol = context.main_protocol(); - if connect_protocol.is_transport() { - // 传输层的协议需要探测服务器地址 - current_device = - domain_request0(current_device_info, config, context.default_interface()); - } - //需要重连 - call.connect(ConnectInfo::new(*count, current_device.connect_server)); - log::info!("发送握手请求,{:?}", config); - if let Err(e) = handshake.send(context, config.server_secret, current_device.connect_server) - { - log::warn!("{:?}", e); - let request_packet = handshake.handshake_request_packet(config.server_secret)?; - match connect_protocol { - ConnectProtocol::UDP => {} - ConnectProtocol::TCP => { - connect_util.try_connect_tcp( - request_packet.into_buffer(), - current_device.connect_server, - ); - } - ConnectProtocol::WS | ConnectProtocol::WSS => { - connect_util - .try_connect_ws(request_packet.into_buffer(), config.server_addr.clone()); - } - } - } - } - Ok(()) -} - -pub fn domain_request0( - current_device: &AtomicCell, - config: &BaseConfigInfo, - default_interface: &LocalInterface, -) -> CurrentDeviceInfo { - let mut current_dev = current_device.load(); - - // 探测服务端地址变化 - match dns_query_all( - &config.server_addr, - config.name_servers.clone(), - default_interface, - ) { - Ok(addrs) => { - log::info!( - "domain {} dns {:?} addr {:?}", - config.server_addr, - config.name_servers, - addrs - ); - - match address_choose(addrs) { - Ok(addr) => { - if addr != current_dev.connect_server { - let mut tmp = current_dev.clone(); - tmp.connect_server = addr; - let rs = current_device.compare_exchange(current_dev, tmp); - log::info!( - "服务端地址变化,旧地址:{},新地址:{},替换结果:{}", - current_dev.connect_server, - addr, - rs.is_ok() - ); - if rs.is_ok() { - current_dev.connect_server = addr; - } - } - } - Err(e) => { - log::error!("域名地址选择失败:{:?},domain={}", e, config.server_addr); - } - } - } - Err(e) => { - log::error!("域名解析失败:{:?},domain={}", e, config.server_addr); - } - } - current_dev -} diff --git a/vnt/src/handle/maintain/mod.rs b/vnt/src/handle/maintain/mod.rs deleted file mode 100644 index 5e9dfe0..0000000 --- a/vnt/src/handle/maintain/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -mod heartbeat; -pub use heartbeat::client_relay; -pub use heartbeat::heartbeat; - -mod re_nat_type; -pub use re_nat_type::retrieve_nat_type; - -mod addr_request; -pub use addr_request::*; - -mod punch; -pub use punch::*; - -mod idle; -pub use idle::idle_gateway; -pub use idle::idle_route; - -mod up_status; -pub use up_status::*; diff --git a/vnt/src/handle/maintain/punch.rs b/vnt/src/handle/maintain/punch.rs deleted file mode 100644 index cef4039..0000000 --- a/vnt/src/handle/maintain/punch.rs +++ /dev/null @@ -1,354 +0,0 @@ -use std::collections::HashMap; -use std::net::Ipv4Addr; -use std::ops::{Div, Mul}; -use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; -use std::sync::Arc; -use std::thread; -use std::time::Duration; - -use anyhow::anyhow; -use crossbeam_utils::atomic::AtomicCell; -use parking_lot::Mutex; -use protobuf::Message; -use rand::prelude::SliceRandom; - -use crate::channel::context::ChannelContext; -use crate::channel::punch::{NatInfo, NatType, Punch}; -use crate::cipher::Cipher; -use crate::handle::{CurrentDeviceInfo, PeerDeviceInfo}; -use crate::nat::NatTest; -use crate::proto::message::{PunchInfo, PunchNatType}; -use crate::protocol::body::ENCRYPTION_RESERVED; -use crate::protocol::{control_packet, other_turn_packet, NetPacket, Protocol, MAX_TTL}; -use crate::util::Scheduler; - -#[derive(Clone)] -pub struct PunchSender { - sender_self: SyncSender<(Ipv4Addr, NatInfo)>, - sender_peer: SyncSender<(Ipv4Addr, NatInfo)>, - sender_cone_self: SyncSender<(Ipv4Addr, NatInfo)>, - sender_cone_peer: SyncSender<(Ipv4Addr, NatInfo)>, -} - -impl PunchSender { - pub fn send(&self, src_peer: bool, ip: Ipv4Addr, info: NatInfo) -> bool { - log::info!( - "发送打洞协商消息,是否对端发起:{},ip:{},info:{:?}", - src_peer, - ip, - info - ); - let sender = match info.nat_type { - NatType::Symmetric => { - if src_peer { - &self.sender_peer - } else { - &self.sender_self - } - } - NatType::Cone => { - if src_peer { - &self.sender_cone_peer - } else { - &self.sender_cone_self - } - } - }; - sender.try_send((ip, info)).is_ok() - } -} - -pub struct PunchReceiver { - receiver_peer: Receiver<(Ipv4Addr, NatInfo)>, - receiver_self: Receiver<(Ipv4Addr, NatInfo)>, - receiver_cone_peer: Receiver<(Ipv4Addr, NatInfo)>, - receiver_cone_self: Receiver<(Ipv4Addr, NatInfo)>, -} - -pub fn punch_channel() -> (PunchSender, PunchReceiver) { - let (sender_self, receiver_self) = sync_channel(0); - let (sender_peer, receiver_peer) = sync_channel(0); - let (sender_cone_peer, receiver_cone_peer) = sync_channel(0); - let (sender_cone_self, receiver_cone_self) = sync_channel(0); - ( - PunchSender { - sender_self, - sender_peer, - sender_cone_peer, - sender_cone_self, - }, - PunchReceiver { - receiver_peer, - receiver_self, - receiver_cone_peer, - receiver_cone_self, - }, - ) -} - -pub fn punch( - scheduler: &Scheduler, - context: ChannelContext, - nat_test: NatTest, - device_map: Arc)>>, - current_device: Arc>, - client_cipher: Cipher, - receiver: PunchReceiver, - punch: Punch, -) { - let punch_record = Arc::new(Mutex::new(HashMap::new())); - let last_punch_record = HashMap::new(); - punch_request( - scheduler, - context, - nat_test, - device_map, - current_device.clone(), - client_cipher.clone(), - 0, - punch_record.clone(), - last_punch_record, - ); - let f = |receiver: Receiver<(Ipv4Addr, NatInfo)>| { - let punch = punch.clone(); - let current_device = current_device.clone(); - let client_cipher = client_cipher.clone(); - let punch_record = punch_record.clone(); - thread::Builder::new() - .name("punch".into()) - .spawn(move || { - punch_start(receiver, punch, current_device, client_cipher, punch_record); - }) - .expect("punch"); - }; - f(receiver.receiver_peer); - f(receiver.receiver_self); - f(receiver.receiver_cone_peer); - f(receiver.receiver_cone_self); -} - -/// 接收打洞消息,配合对端打洞 -fn punch_start( - receiver: Receiver<(Ipv4Addr, NatInfo)>, - mut punch: Punch, - current_device: Arc>, - client_cipher: Cipher, - punch_record: Arc>>, -) { - while let Ok((peer_ip, nat_info)) = receiver.recv() { - let mut packet = NetPacket::new_encrypt([0u8; 12 + ENCRYPTION_RESERVED]).unwrap(); - packet.set_default_version(); - packet.first_set_ttl(1); - packet.set_protocol(Protocol::Control); - packet.set_transport_protocol(control_packet::Protocol::PunchRequest.into()); - packet.set_source(current_device.load().virtual_ip()); - packet.set_destination(peer_ip); - let count = { - let mut guard = punch_record.lock(); - if let Some(v) = guard.get_mut(&peer_ip) { - *v += 1; - *v - } else { - guard.insert(peer_ip, 0); - 0 - } - }; - log::info!("第{}次发起打洞,目标:{:?},{:?} ", count, peer_ip, nat_info); - - if let Err(e) = client_cipher.encrypt_ipv4(&mut packet) { - log::error!("{:?}", e); - continue; - } - if let Err(e) = punch.punch(packet.buffer(), peer_ip, nat_info, count < 2, count) { - log::warn!("{:?}", e) - } - } -} - -/// 定时发起打洞请求 -fn punch_request( - scheduler: &Scheduler, - context: ChannelContext, - nat_test: NatTest, - device_map: Arc)>>, - current_device: Arc>, - client_cipher: Cipher, - count: usize, - punch_record: Arc>>, - mut last_punch_record: HashMap, -) { - let curr = current_device.load(); - let secs = if curr.status.online() { - if let Err(e) = punch0( - &context, - &nat_test, - &device_map, - curr, - &client_cipher, - &punch_record, - &mut last_punch_record, - count, - ) { - log::warn!("{:?}", e) - } - let sleep_time = [6, 7]; - Duration::from_secs(sleep_time[count % sleep_time.len()]) - } else { - Duration::from_secs(5) - }; - let rs = scheduler.timeout(secs, move |s| { - punch_request( - s, - context, - nat_test, - device_map, - current_device, - client_cipher, - count + 1, - punch_record, - last_punch_record, - ); - }); - if !rs { - log::info!("定时任务停止"); - } -} -#[derive(Copy, Clone, Default)] -struct PunchRecordItem { - pub punch_record: usize, - pub last_p2p_num: usize, -} - -/// 随机对需要打洞的客户端发起打洞请求 -fn punch0( - context: &ChannelContext, - nat_test: &NatTest, - device_map: &Arc)>>, - current_device: CurrentDeviceInfo, - client_cipher: &Cipher, - punch_record: &Mutex>, - last_punch_record: &mut HashMap, - total_count: usize, -) -> anyhow::Result<()> { - let nat_info = nat_test.nat_info(); - if total_count < 10 - && (nat_info.public_ips.is_empty() - || nat_info.public_ports.is_empty() - || nat_info.public_ports[0] == 0 - || nat_info.public_ports.iter().filter(|&&v| v == 0).count() - > nat_info.public_ports.len() / 2) - { - log::info!("未获取到公网地址,暂时放弃打洞,第{}轮", total_count); - return Ok(()); - } - let current_ip = current_device.virtual_ip; - let mut list: Vec = device_map - .lock() - .1 - .values() - .filter(|info| !info.wireguard && info.virtual_ip > current_ip) - .cloned() - .collect(); - list.shuffle(&mut rand::thread_rng()); - for info in list { - if info.status.is_offline() { - // 客户端掉线了要重置打洞记录 - punch_record.lock().remove(&info.virtual_ip); - continue; - } - let mut punch_count = punch_record - .lock() - .get(&info.virtual_ip) - .cloned() - .unwrap_or(0) - .mul(2) - .div(3); - let p2p_num = context.route_table.p2p_num(&info.virtual_ip); - let mut max_punch_interval = 50; - if p2p_num > 0 { - if p2p_num >= context.channel_num() { - //通道数满足要求,不再打洞 - if punch_count != 0 { - punch_record.lock().remove(&info.virtual_ip); - } - continue; - } - //有p2p通道,但是通道数量不够,则继续打洞 - // 提高等待上限 - max_punch_interval = 300; - } - // 能发起打洞的前提是自己空闲,这里会间隔5秒以上发起一次打洞,所以假定上一轮打洞已结束 - let last_punch = last_punch_record.entry(info.virtual_ip).or_default(); - if last_punch.last_p2p_num > p2p_num { - // 打的洞掉线了,需要重置重新打 - punch_record.lock().remove(&info.virtual_ip); - punch_count = 0; - } - - // 梯度增加打洞时间间隔 - if total_count > last_punch.punch_record + punch_count.min(max_punch_interval) { - // 记录打洞周期,抑制下一次打洞,从而递减打洞频率 - last_punch.punch_record = total_count; - last_punch.last_p2p_num = p2p_num; - let packet = punch_packet( - client_cipher, - current_device.virtual_ip(), - &nat_info, - info.virtual_ip, - )?; - log::info!( - "目标:{:?},当前nat:{:?} 第{}次发起打洞协商请求, 第:{}轮", - info.virtual_ip, - nat_info, - punch_count, - total_count, - ); - context.send_default(&packet, current_device.connect_server)?; - break; - } - } - Ok(()) -} - -fn punch_packet( - client_cipher: &Cipher, - virtual_ip: Ipv4Addr, - nat_info: &NatInfo, - dest: Ipv4Addr, -) -> anyhow::Result>> { - let mut punch_reply = PunchInfo::new(); - punch_reply.reply = false; - punch_reply.public_ip_list = nat_info - .public_ips - .iter() - .map(|ip| u32::from_be_bytes(ip.octets())) - .collect(); - punch_reply.public_port = nat_info.public_ports.get(0).map_or(0, |v| *v as u32); - punch_reply.public_ports = nat_info.public_ports.iter().map(|e| *e as u32).collect(); - punch_reply.public_port_range = nat_info.public_port_range as u32; - punch_reply.public_tcp_port = nat_info.public_tcp_port as u32; - punch_reply.local_ip = u32::from(nat_info.local_ipv4().unwrap_or(Ipv4Addr::UNSPECIFIED)); - punch_reply.local_port = nat_info.udp_ports[0] as u32; - punch_reply.tcp_port = nat_info.tcp_port as u32; - punch_reply.udp_ports = nat_info.udp_ports.iter().map(|e| *e as u32).collect(); - if let Some(ipv6) = nat_info.ipv6 { - punch_reply.ipv6_port = nat_info.udp_ports[0] as u32; - punch_reply.ipv6 = ipv6.octets().to_vec(); - } - punch_reply.nat_type = protobuf::EnumOrUnknown::new(PunchNatType::from(nat_info.nat_type)); - punch_reply.punch_model = protobuf::EnumOrUnknown::new(nat_info.punch_model.into()); - log::info!("请求打洞={:?}", punch_reply); - let bytes = punch_reply - .write_to_bytes() - .map_err(|e| anyhow!("punch_packet {:?}", e))?; - let mut net_packet = NetPacket::new_encrypt(vec![0u8; 12 + bytes.len() + ENCRYPTION_RESERVED])?; - net_packet.set_default_version(); - net_packet.set_protocol(Protocol::OtherTurn); - net_packet.set_transport_protocol(other_turn_packet::Protocol::Punch.into()); - net_packet.first_set_ttl(MAX_TTL); - net_packet.set_source(virtual_ip); - net_packet.set_destination(dest); - net_packet.set_payload(&bytes)?; - client_cipher.encrypt_ipv4(&mut net_packet)?; - Ok(net_packet) -} diff --git a/vnt/src/handle/maintain/re_nat_type.rs b/vnt/src/handle/maintain/re_nat_type.rs deleted file mode 100644 index ca6d29f..0000000 --- a/vnt/src/handle/maintain/re_nat_type.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::thread; -use std::time::Duration; - -use crate::channel::context::ChannelContext; -use crate::channel::sender::AcceptSocketSender; -use crate::nat; -use crate::nat::NatTest; -use crate::util::Scheduler; - -/// 10分钟探测一次nat -pub fn retrieve_nat_type( - scheduler: &Scheduler, - context: ChannelContext, - nat_test: NatTest, - udp_socket_sender: AcceptSocketSender>>, -) { - retrieve_nat_type0(context.clone(), nat_test.clone(), udp_socket_sender.clone()); - scheduler.timeout(Duration::from_secs(60 * 10), move |s| { - retrieve_nat_type(s, context, nat_test, udp_socket_sender) - }); -} - -fn retrieve_nat_type0( - context: ChannelContext, - nat_test: NatTest, - udp_socket_sender: AcceptSocketSender>>, -) { - thread::Builder::new() - .name("natTest".into()) - .spawn(move || { - if nat_test.can_update() { - let local_ipv4 = if nat_test.update_local_ipv4 { - nat::local_ipv4() - } else { - None - }; - let local_ipv6 = nat::local_ipv6(); - match nat_test.re_test(local_ipv4, local_ipv6, context.default_interface()) { - Ok(nat_info) => { - log::info!("当前nat信息:{:?}", nat_info); - if let Err(e) = context.switch(nat_info.nat_type, &udp_socket_sender) { - log::warn!("{:?}", e); - } - } - Err(e) => { - log::warn!("nat re_test {:?}", e); - } - }; - #[cfg(feature = "upnp")] - nat_test.reset_upnp(); - log::info!("刷新nat结束") - } - }) - .expect("natTest"); -} diff --git a/vnt/src/handle/maintain/up_status.rs b/vnt/src/handle/maintain/up_status.rs deleted file mode 100644 index 25b51e7..0000000 --- a/vnt/src/handle/maintain/up_status.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::channel::context::ChannelContext; -use crate::handle::CurrentDeviceInfo; -use crate::proto::message::{ClientStatusInfo, PunchNatType, RouteItem}; -use crate::protocol::body::ENCRYPTION_RESERVED; -use crate::protocol::{service_packet, NetPacket, Protocol, HEAD_LEN, MAX_TTL}; -use crate::util::Scheduler; -use crossbeam_utils::atomic::AtomicCell; -use protobuf::Message; -use std::io; -use std::sync::Arc; -use std::time::Duration; - -/// 上报状态给服务器 -pub fn up_status( - scheduler: &Scheduler, - context: ChannelContext, - current_device_info: Arc>, -) { - let _ = scheduler.timeout(Duration::from_secs(60), move |x| { - up_status0(x, context, current_device_info) - }); -} - -fn up_status0( - scheduler: &Scheduler, - context: ChannelContext, - current_device_info: Arc>, -) { - if let Err(e) = send_up_status_packet(&context, ¤t_device_info) { - log::warn!("{:?}", e) - } - let rs = scheduler.timeout(Duration::from_secs(10 * 60), move |x| { - up_status0(x, context, current_device_info) - }); - if !rs { - log::info!("定时任务停止"); - } -} - -fn send_up_status_packet( - context: &ChannelContext, - current_device_info: &AtomicCell, -) -> io::Result<()> { - let device_info = current_device_info.load(); - if device_info.status.offline() { - return Ok(()); - } - let routes = context.route_table.route_table_p2p(); - if routes.is_empty() { - return Ok(()); - } - let mut message = ClientStatusInfo::new(); - message.source = device_info.virtual_ip.into(); - for (ip, _) in routes { - let mut item = RouteItem::new(); - item.next_ip = ip.into(); - message.p2p_list.push(item); - } - message.up_stream = context.up_traffic_meter.as_ref().map_or(0, |v| v.total()); - message.down_stream = context.down_traffic_meter.as_ref().map_or(0, |v| v.total()); - message.nat_type = protobuf::EnumOrUnknown::new(if context.is_cone() { - PunchNatType::Cone - } else { - PunchNatType::Symmetric - }); - let buf = message - .write_to_bytes() - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("up_status_packet {:?}", e)))?; - let mut net_packet = - NetPacket::new_encrypt(vec![0; HEAD_LEN + buf.len() + ENCRYPTION_RESERVED])?; - net_packet.set_default_version(); - net_packet.set_gateway_flag(true); - net_packet.set_protocol(Protocol::Service); - net_packet.set_transport_protocol_into(service_packet::Protocol::ClientStatusInfo); - net_packet.first_set_ttl(MAX_TTL); - net_packet.set_source(device_info.virtual_ip); - net_packet.set_destination(device_info.virtual_gateway); - net_packet.set_payload(&buf)?; - context.send_default(&net_packet, device_info.connect_server)?; - Ok(()) -} diff --git a/vnt/src/handle/mod.rs b/vnt/src/handle/mod.rs deleted file mode 100644 index 2fec003..0000000 --- a/vnt/src/handle/mod.rs +++ /dev/null @@ -1,276 +0,0 @@ -use crate::channel::socket::LocalInterface; -use crossbeam_utils::atomic::AtomicCell; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; - -pub mod callback; -mod extension; -pub mod handshaker; -pub mod maintain; -pub mod recv_data; -pub mod registrar; -#[cfg(feature = "integrated_tun")] -pub mod tun_tap; - -const SELF_IP: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 2); -const GATEWAY_IP: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 1); - -pub fn now_time() -> u64 { - let now = std::time::SystemTime::now(); - if let Ok(timestamp) = now.duration_since(std::time::UNIX_EPOCH) { - timestamp.as_secs() * 1000 + u64::from(timestamp.subsec_millis()) - } else { - 0 - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct PeerDeviceInfo { - pub virtual_ip: Ipv4Addr, - pub name: String, - pub status: PeerDeviceStatus, - pub client_secret: bool, - pub client_secret_hash: Vec, - pub wireguard: bool, -} - -impl PeerDeviceInfo { - pub fn new( - virtual_ip: Ipv4Addr, - name: String, - status: u8, - client_secret: bool, - client_secret_hash: Vec, - wireguard: bool, - ) -> Self { - Self { - virtual_ip, - name, - status: PeerDeviceStatus::from(status), - client_secret, - client_secret_hash, - wireguard, - } - } -} - -#[derive(Clone, Debug)] -pub struct BaseConfigInfo { - pub name: String, - pub token: String, - pub ip: Option, - pub client_secret_hash: Option<[u8; 16]>, - pub server_secret: bool, - pub device_id: String, - pub server_addr: String, - pub name_servers: Vec, - pub mtu: u32, - #[cfg(feature = "integrated_tun")] - #[cfg(target_os = "windows")] - pub tap: bool, - #[cfg(feature = "integrated_tun")] - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - pub device_name: Option, - pub allow_wire_guard: bool, - pub default_interface: LocalInterface, -} - -impl BaseConfigInfo { - pub fn new( - name: String, - token: String, - ip: Option, - client_secret_hash: Option<[u8; 16]>, - server_secret: bool, - device_id: String, - server_addr: String, - name_servers: Vec, - mtu: u32, - #[cfg(feature = "integrated_tun")] - #[cfg(target_os = "windows")] - tap: bool, - #[cfg(feature = "integrated_tun")] - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - device_name: Option, - allow_wire_guard: bool, - default_interface: LocalInterface, - ) -> Self { - Self { - name, - token, - ip, - client_secret_hash, - server_secret, - device_id, - server_addr, - name_servers, - mtu, - #[cfg(feature = "integrated_tun")] - #[cfg(target_os = "windows")] - tap, - #[cfg(feature = "integrated_tun")] - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - device_name, - allow_wire_guard, - default_interface, - } - } -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] -pub enum PeerDeviceStatus { - Online, - Offline, -} - -impl PeerDeviceStatus { - pub fn is_online(&self) -> bool { - self == &PeerDeviceStatus::Online - } - pub fn is_offline(&self) -> bool { - self == &PeerDeviceStatus::Offline - } -} - -impl Into for PeerDeviceStatus { - fn into(self) -> u8 { - match self { - PeerDeviceStatus::Online => 0, - PeerDeviceStatus::Offline => 1, - } - } -} - -impl From for PeerDeviceStatus { - fn from(value: u8) -> Self { - match value { - 0 => PeerDeviceStatus::Online, - _ => PeerDeviceStatus::Offline, - } - } -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum ConnectStatus { - Connecting, - Connected, -} - -impl ConnectStatus { - pub fn online(&self) -> bool { - self == &ConnectStatus::Connected - } - pub fn offline(&self) -> bool { - self == &ConnectStatus::Connecting - } -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub struct CurrentDeviceInfo { - //本机虚拟IP - pub virtual_ip: Ipv4Addr, - //子网掩码 - pub virtual_netmask: Ipv4Addr, - //虚拟网关 - pub virtual_gateway: Ipv4Addr, - //网络地址 - pub virtual_network: Ipv4Addr, - //直接广播地址 - pub broadcast_ip: Ipv4Addr, - //链接的服务器地址 - pub connect_server: SocketAddr, - //连接状态 - pub status: ConnectStatus, -} - -impl CurrentDeviceInfo { - pub fn new( - virtual_ip: Ipv4Addr, - virtual_netmask: Ipv4Addr, - virtual_gateway: Ipv4Addr, - connect_server: SocketAddr, - ) -> Self { - let broadcast_ip = (!u32::from_be_bytes(virtual_netmask.octets())) - | u32::from_be_bytes(virtual_gateway.octets()); - let broadcast_ip = Ipv4Addr::from(broadcast_ip); - let virtual_network = u32::from_be_bytes(virtual_netmask.octets()) - & u32::from_be_bytes(virtual_gateway.octets()); - let virtual_network = Ipv4Addr::from(virtual_network); - Self { - virtual_ip, - virtual_netmask, - virtual_gateway, - virtual_network, - broadcast_ip, - connect_server, - status: ConnectStatus::Connecting, - } - } - pub fn new0(connect_server: SocketAddr) -> Self { - Self { - virtual_ip: Ipv4Addr::UNSPECIFIED, - virtual_gateway: Ipv4Addr::UNSPECIFIED, - virtual_netmask: Ipv4Addr::UNSPECIFIED, - virtual_network: Ipv4Addr::UNSPECIFIED, - broadcast_ip: Ipv4Addr::UNSPECIFIED, - connect_server, - status: ConnectStatus::Connecting, - } - } - pub fn update( - &mut self, - virtual_ip: Ipv4Addr, - virtual_netmask: Ipv4Addr, - virtual_gateway: Ipv4Addr, - ) { - let broadcast_ip = (!u32::from_be_bytes(virtual_netmask.octets())) - | u32::from_be_bytes(virtual_ip.octets()); - let broadcast_ip = Ipv4Addr::from(broadcast_ip); - let virtual_network = - u32::from_be_bytes(virtual_netmask.octets()) & u32::from_be_bytes(virtual_ip.octets()); - let virtual_network = Ipv4Addr::from(virtual_network); - self.virtual_ip = virtual_ip; - self.virtual_netmask = virtual_netmask; - self.virtual_gateway = virtual_gateway; - self.broadcast_ip = broadcast_ip; - self.virtual_network = virtual_network; - } - #[inline] - pub fn virtual_ip(&self) -> Ipv4Addr { - self.virtual_ip - } - #[inline] - pub fn virtual_gateway(&self) -> Ipv4Addr { - self.virtual_gateway - } - #[inline] - pub fn is_gateway(&self, ip: &Ipv4Addr) -> bool { - &self.virtual_gateway == ip || ip == &GATEWAY_IP - } - #[inline] - pub fn not_in_network(&self, ip: Ipv4Addr) -> bool { - u32::from(ip) & u32::from(self.virtual_netmask) != u32::from(self.virtual_network) - } - pub fn is_server_addr(&self, addr: SocketAddr) -> bool { - if self.connect_server == addr { - return true; - } - let f = |ip: IpAddr| match ip { - IpAddr::V4(v4) => Some(v4), - IpAddr::V6(v6) => v6.to_ipv4(), - }; - addr.port() == self.connect_server.port() && f(addr.ip()) == f(self.connect_server.ip()) - } -} -pub fn change_status( - current_device: &AtomicCell, - connect_status: ConnectStatus, -) -> CurrentDeviceInfo { - loop { - let cur = current_device.load(); - let mut new_info = cur; - new_info.status = connect_status; - if current_device.compare_exchange(cur, new_info).is_ok() { - return new_info; - } - } -} diff --git a/vnt/src/handle/recv_data/client.rs b/vnt/src/handle/recv_data/client.rs deleted file mode 100644 index e658ad7..0000000 --- a/vnt/src/handle/recv_data/client.rs +++ /dev/null @@ -1,401 +0,0 @@ -use anyhow::anyhow; -use std::collections::HashMap; -use std::net::{Ipv4Addr, Ipv6Addr}; -use std::sync::Arc; - -use parking_lot::RwLock; -use protobuf::Message; - -use packet::icmp::{icmp, Kind}; -use packet::ip::ipv4; -use packet::ip::ipv4::packet::IpV4Packet; - -use crate::channel::context::ChannelContext; -use crate::channel::punch::NatInfo; -use crate::channel::{Route, RouteKey}; -use crate::cipher::Cipher; -use crate::external_route::AllowExternalRoute; -use crate::handle::extension::handle_extension_tail; -use crate::handle::maintain::PunchSender; -use crate::handle::recv_data::PacketHandler; -use crate::handle::CurrentDeviceInfo; -#[cfg(feature = "ip_proxy")] -use crate::ip_proxy::{IpProxyMap, ProxyHandler}; -use crate::nat::NatTest; -use crate::proto::message::{PunchInfo, PunchNatType}; -use crate::protocol::body::ENCRYPTION_RESERVED; -use crate::protocol::control_packet::ControlPacket; -use crate::protocol::{ - control_packet, ip_turn_packet, other_turn_packet, NetPacket, Protocol, MAX_TTL, -}; -use crate::tun_tap_device::vnt_device::DeviceWrite; - -/// 处理来源于客户端的包 -#[derive(Clone)] -pub struct ClientPacketHandler { - device: Device, - client_cipher: Cipher, - punch_sender: PunchSender, - peer_nat_info_map: Arc>>, - nat_test: NatTest, - route: AllowExternalRoute, - #[cfg(feature = "ip_proxy")] - #[cfg(feature = "integrated_tun")] - ip_proxy_map: Option, -} - -impl ClientPacketHandler { - pub fn new( - device: Device, - client_cipher: Cipher, - punch_sender: PunchSender, - peer_nat_info_map: Arc>>, - nat_test: NatTest, - route: AllowExternalRoute, - #[cfg(feature = "integrated_tun")] - #[cfg(feature = "ip_proxy")] - ip_proxy_map: Option, - ) -> Self { - Self { - device, - client_cipher, - punch_sender, - peer_nat_info_map, - nat_test, - route, - #[cfg(feature = "integrated_tun")] - #[cfg(feature = "ip_proxy")] - ip_proxy_map, - } - } -} - -impl PacketHandler for ClientPacketHandler { - fn handle( - &self, - mut net_packet: NetPacket<&mut [u8]>, - mut extend: NetPacket<&mut [u8]>, - route_key: RouteKey, - context: &ChannelContext, - current_device: &CurrentDeviceInfo, - ) -> anyhow::Result<()> { - self.client_cipher.decrypt_ipv4(&mut net_packet)?; - context - .route_table - .update_read_time(&net_packet.source(), &route_key); - //处理扩展 - let net_packet = if net_packet.is_extension() { - //这样重用数组,减少一次数据拷贝 - if handle_extension_tail(&mut net_packet, &mut extend)? { - extend - } else { - net_packet - } - } else { - net_packet - }; - match net_packet.protocol() { - Protocol::Service => {} - Protocol::Error => {} - Protocol::Control => { - self.control(context, current_device, net_packet, route_key)?; - } - Protocol::IpTurn => { - self.ip_turn(net_packet, context, current_device, route_key)?; - } - Protocol::OtherTurn => { - self.other_turn(context, current_device, net_packet, route_key)?; - } - Protocol::Unknown(_) => {} - } - Ok(()) - } -} - -impl ClientPacketHandler { - fn ip_turn( - &self, - mut net_packet: NetPacket<&mut [u8]>, - context: &ChannelContext, - current_device: &CurrentDeviceInfo, - route_key: RouteKey, - ) -> anyhow::Result<()> { - let destination = net_packet.destination(); - let source = net_packet.source(); - match ip_turn_packet::Protocol::from(net_packet.transport_protocol()) { - ip_turn_packet::Protocol::Ipv4 => { - let mut ipv4 = IpV4Packet::new(net_packet.payload_mut())?; - match ipv4.protocol() { - ipv4::protocol::Protocol::Icmp => { - if ipv4.destination_ip() == destination { - let mut icmp_packet = icmp::IcmpPacket::new(ipv4.payload_mut())?; - if icmp_packet.kind() == Kind::EchoRequest { - //开启ping - icmp_packet.set_kind(Kind::EchoReply); - icmp_packet.update_checksum(); - ipv4.set_source_ip(destination); - ipv4.set_destination_ip(source); - ipv4.update_checksum(); - net_packet.set_source(destination); - net_packet.set_destination(source); - //不管加不加密,和接收到的数据长度都一致 - self.client_cipher.encrypt_ipv4(&mut net_packet)?; - context.send_by_key(&net_packet, route_key)?; - return Ok(()); - } - } - } - _ => {} - } - // ip代理只关心实际目标 - let real_dest = ipv4.destination_ip(); - if real_dest != destination - && !(real_dest.is_broadcast() - || real_dest.is_multicast() - || real_dest == current_device.broadcast_ip - || real_dest.is_unspecified()) - { - if !self.route.allow(&real_dest) { - //拦截不符合的目标 - return Ok(()); - } - match ipv4.protocol() { - ipv4::protocol::Protocol::Tcp => { - let payload = ipv4.payload(); - if payload.len() < 20 { - return Ok(()); - } - let destination_port = - u16::from_be_bytes(payload[2..4].try_into().unwrap()); - if self.nat_test.is_local_tcp(real_dest, destination_port) { - return Ok(()); - } - } - ipv4::protocol::Protocol::Udp => { - let payload = ipv4.payload(); - if payload.len() < 8 { - return Ok(()); - } - let destination_port = - u16::from_be_bytes(payload[2..4].try_into().unwrap()); - if self.nat_test.is_local_udp(real_dest, destination_port) { - return Ok(()); - } - } - _ => {} - } - #[cfg(feature = "ip_proxy")] - #[cfg(feature = "integrated_tun")] - if let Some(ip_proxy_map) = &self.ip_proxy_map { - if ip_proxy_map.recv_handle(&mut ipv4, source, destination)? { - return Ok(()); - } - } - } - self.device.write(net_packet.payload())?; - } - ip_turn_packet::Protocol::WGIpv4 => { - // WG客户端的数据不会直接发过来,不用处理 - } - ip_turn_packet::Protocol::Ipv4Broadcast => { - //客户端不帮忙转发广播包,所以不会出现这种类型的数据 - } - ip_turn_packet::Protocol::Unknown(_) => {} - } - Ok(()) - } - fn control( - &self, - context: &ChannelContext, - current_device: &CurrentDeviceInfo, - mut net_packet: NetPacket<&mut [u8]>, - route_key: RouteKey, - ) -> anyhow::Result<()> { - let metric = net_packet.source_ttl() - net_packet.ttl() + 1; - let source = net_packet.source(); - match ControlPacket::new(net_packet.transport_protocol(), net_packet.payload())? { - ControlPacket::PingPacket(_) => { - let route = Route::from_default_rt(route_key, metric); - context.route_table.add_route_if_absent(source, route); - net_packet.set_transport_protocol(control_packet::Protocol::Pong.into()); - net_packet.set_source(current_device.virtual_ip); - net_packet.set_destination(source); - net_packet.first_set_ttl(MAX_TTL); - self.client_cipher.encrypt_ipv4(&mut net_packet)?; - context.send_by_key(&net_packet, route_key)?; - } - ControlPacket::PongPacket(pong_packet) => { - let current_time = crate::handle::now_time() as u16; - if current_time < pong_packet.time() { - return Ok(()); - } - let rt = (current_time - pong_packet.time()) as i64; - let route = Route::from(route_key, metric, rt); - context.route_table.add_route(source, route); - } - ControlPacket::PunchRequest => { - log::info!("PunchRequest={:?},source={}", route_key, source); - if context.use_channel_type().is_only_relay() { - return Ok(()); - } - //忽略掉来源于自己的包 - if self - .nat_test - .is_local_address(route_key.protocol().is_base_tcp(), route_key.addr) - { - return Ok(()); - } - - //回应 - net_packet.set_transport_protocol(control_packet::Protocol::PunchResponse.into()); - net_packet.set_source(current_device.virtual_ip); - net_packet.set_destination(source); - net_packet.first_set_ttl(1); - self.client_cipher.encrypt_ipv4(&mut net_packet)?; - context.send_by_key(&net_packet, route_key)?; - // 收到PunchRequest就添加路由,会导致单向通信的问题,删掉试试 - // let route = Route::from_default_rt(route_key, 1); - // context.route_table.add_route_if_absent(source, route); - } - ControlPacket::PunchResponse => { - log::info!("PunchResponse={:?},source={}", route_key, source); - if context.use_channel_type().is_only_relay() { - return Ok(()); - } - if self - .nat_test - .is_local_address(route_key.protocol().is_base_tcp(), route_key.addr) - { - return Ok(()); - } - let route = Route::from_default_rt(route_key, metric); - context.route_table.add_route_if_absent(source, route); - } - ControlPacket::AddrRequest => match route_key.addr.ip() { - std::net::IpAddr::V4(ipv4) => { - let mut packet = NetPacket::new_encrypt([0; 12 + 6 + ENCRYPTION_RESERVED])?; - packet.set_default_version(); - packet.set_protocol(Protocol::Control); - packet.set_transport_protocol(control_packet::Protocol::AddrResponse.into()); - packet.first_set_ttl(MAX_TTL); - packet.set_source(current_device.virtual_ip); - packet.set_destination(source); - let mut addr_packet = control_packet::AddrPacket::new(packet.payload_mut())?; - addr_packet.set_ipv4(ipv4); - addr_packet.set_port(route_key.addr.port()); - self.client_cipher.encrypt_ipv4(&mut packet)?; - context.send_by_key(&packet, route_key)?; - } - std::net::IpAddr::V6(_) => {} - }, - ControlPacket::AddrResponse(_) => {} - } - Ok(()) - } - fn other_turn( - &self, - context: &ChannelContext, - current_device: &CurrentDeviceInfo, - net_packet: NetPacket<&mut [u8]>, - route_key: RouteKey, - ) -> anyhow::Result<()> { - if context.use_channel_type().is_only_relay() { - return Ok(()); - } - let source = net_packet.source(); - match other_turn_packet::Protocol::from(net_packet.transport_protocol()) { - other_turn_packet::Protocol::Punch => { - let mut punch_info = PunchInfo::parse_from_bytes(net_packet.payload()) - .map_err(|e| anyhow!("PunchInfo {:?}", e))?; - let public_ips = punch_info - .public_ip_list - .iter() - .map(|v| Ipv4Addr::from(v.to_be_bytes())) - .collect(); - let local_ipv4 = Some(Ipv4Addr::from(punch_info.local_ip.to_be_bytes())); - let tcp_port = punch_info.tcp_port as u16; - let public_tcp_port = punch_info.public_tcp_port as u16; - let ipv6 = if punch_info.ipv6.len() == 16 { - let ipv6: [u8; 16] = punch_info.ipv6.try_into().unwrap(); - Some(Ipv6Addr::from(ipv6)) - } else { - None - }; - //兼容旧版本 - if punch_info.public_ports.is_empty() { - punch_info.public_ports.push(punch_info.public_port); - } - //兼容旧版本 - if punch_info.udp_ports.is_empty() { - punch_info.udp_ports.push(punch_info.local_port); - } - let peer_nat_info = NatInfo::new( - public_ips, - punch_info.public_ports.iter().map(|e| *e as u16).collect(), - punch_info.public_port_range as u16, - local_ipv4, - ipv6, - punch_info.udp_ports.iter().map(|e| *e as u16).collect(), - tcp_port, - public_tcp_port, - punch_info.nat_type.enum_value_or_default().into(), - punch_info.punch_model.enum_value_or_default().into(), - ); - { - let peer_nat_info = peer_nat_info.clone(); - self.peer_nat_info_map.write().insert(source, peer_nat_info); - } - if !punch_info.reply { - let mut punch_reply = PunchInfo::new(); - punch_reply.reply = true; - let nat_info = self.nat_test.nat_info(); - punch_reply.public_ip_list = nat_info - .public_ips - .iter() - .map(|ip| u32::from_be_bytes(ip.octets())) - .collect(); - punch_reply.public_port = nat_info.public_ports.get(0).map_or(0, |v| *v as u32); - punch_reply.public_ports = - nat_info.public_ports.iter().map(|e| *e as u32).collect(); - punch_reply.public_port_range = nat_info.public_port_range as u32; - punch_reply.tcp_port = nat_info.tcp_port as u32; - punch_reply.public_tcp_port = nat_info.public_tcp_port as u32; - punch_reply.nat_type = - protobuf::EnumOrUnknown::new(PunchNatType::from(nat_info.nat_type)); - punch_reply.punch_model = - protobuf::EnumOrUnknown::new(nat_info.punch_model.into()); - punch_reply.local_ip = - u32::from(nat_info.local_ipv4().unwrap_or(Ipv4Addr::UNSPECIFIED)); - punch_reply.local_port = nat_info.udp_ports[0] as u32; - punch_reply.udp_ports = nat_info.udp_ports.iter().map(|e| *e as u32).collect(); - if let Some(ipv6) = nat_info.ipv6() { - punch_reply.ipv6 = ipv6.octets().to_vec(); - punch_reply.ipv6_port = nat_info.udp_ports[0] as u32; - } - let bytes = punch_reply - .write_to_bytes() - .map_err(|e| anyhow!("punch_reply {:?}", e))?; - let mut punch_packet = - NetPacket::new_encrypt(vec![0u8; 12 + bytes.len() + ENCRYPTION_RESERVED])?; - punch_packet.set_default_version(); - punch_packet.set_protocol(Protocol::OtherTurn); - punch_packet.set_transport_protocol(other_turn_packet::Protocol::Punch.into()); - punch_packet.first_set_ttl(MAX_TTL); - punch_packet.set_source(current_device.virtual_ip()); - punch_packet.set_destination(source); - punch_packet.set_payload(&bytes)?; - self.client_cipher.encrypt_ipv4(&mut punch_packet)?; - if self.punch_sender.send(true, source, peer_nat_info) { - context.send_by_key(&punch_packet, route_key)?; - } - } else { - self.punch_sender.send(false, source, peer_nat_info); - } - } - other_turn_packet::Protocol::Unknown(e) => { - log::warn!("不支持的转发协议 {:?},source:{:?}", e, source); - } - } - Ok(()) - } -} diff --git a/vnt/src/handle/recv_data/mod.rs b/vnt/src/handle/recv_data/mod.rs deleted file mode 100644 index 4b745c1..0000000 --- a/vnt/src/handle/recv_data/mod.rs +++ /dev/null @@ -1,187 +0,0 @@ -use std::collections::HashMap; -use std::net::Ipv4Addr; -use std::sync::Arc; -use std::thread; - -use crossbeam_utils::atomic::AtomicCell; -use parking_lot::{Mutex, RwLock}; - -use crate::channel::context::ChannelContext; -use crate::channel::handler::RecvChannelHandler; -use crate::channel::punch::NatInfo; -use crate::channel::RouteKey; -use crate::cipher::Cipher; -#[cfg(feature = "server_encrypt")] -use crate::cipher::RsaCipher; -use crate::external_route::{AllowExternalRoute, ExternalRoute}; -use crate::handle::callback::VntCallback; -use crate::handle::handshaker::Handshake; -use crate::handle::maintain::PunchSender; -use crate::handle::recv_data::client::ClientPacketHandler; -use crate::handle::recv_data::server::ServerPacketHandler; -use crate::handle::recv_data::turn::TurnPacketHandler; -use crate::handle::{BaseConfigInfo, CurrentDeviceInfo, PeerDeviceInfo, SELF_IP}; -#[cfg(feature = "ip_proxy")] -use crate::ip_proxy::IpProxyMap; -use crate::nat::NatTest; -use crate::protocol::{NetPacket, HEAD_LEN}; -use crate::tun_tap_device::vnt_device::DeviceWrite; - -mod client; -mod server; -mod turn; - -#[derive(Clone)] -pub struct RecvDataHandler { - current_device: Arc>, - turn: TurnPacketHandler, - client: ClientPacketHandler, - server: ServerPacketHandler, - nat_test: NatTest, -} - -impl RecvChannelHandler for RecvDataHandler { - fn handle( - &self, - buf: &mut [u8], - extend: &mut [u8], - route_key: RouteKey, - context: &ChannelContext, - ) { - if buf.len() < HEAD_LEN { - return; - } - //判断stun响应包 - if route_key.protocol().is_udp() { - if let Ok(rs) = self - .nat_test - .recv_data(route_key.index(), route_key.addr, buf) - { - if rs { - return; - } - } - } - if let Err(e) = self.handle0(buf, extend, route_key, context) { - log::error!( - "[{}]-{:?}-{:?}", - thread::current().name().unwrap_or(""), - route_key.addr, - e - ); - } - } -} - -impl RecvDataHandler { - pub fn new( - #[cfg(feature = "server_encrypt")] rsa_cipher: Arc>>, - server_cipher: Cipher, - client_cipher: Cipher, - current_device: Arc>, - device: Device, - device_map: Arc)>>, - config_info: BaseConfigInfo, - nat_test: NatTest, - callback: Call, - punch_sender: PunchSender, - peer_nat_info_map: Arc>>, - external_route: ExternalRoute, - route: AllowExternalRoute, - #[cfg(feature = "integrated_tun")] - #[cfg(feature = "ip_proxy")] - ip_proxy_map: Option, - handshake: Handshake, - #[cfg(feature = "integrated_tun")] - tun_device_helper: crate::tun_tap_device::tun_create_helper::TunDeviceHelper, - ) -> Self { - let server = ServerPacketHandler::new( - #[cfg(feature = "server_encrypt")] - rsa_cipher, - server_cipher, - current_device.clone(), - device.clone(), - device_map, - config_info, - nat_test.clone(), - callback, - external_route.clone(), - handshake, - #[cfg(feature = "integrated_tun")] - tun_device_helper, - ); - let client = ClientPacketHandler::new( - device.clone(), - client_cipher, - punch_sender, - peer_nat_info_map, - nat_test.clone(), - route, - #[cfg(feature = "integrated_tun")] - #[cfg(feature = "ip_proxy")] - ip_proxy_map, - ); - let turn = TurnPacketHandler::new(); - Self { - current_device, - turn, - client, - server, - nat_test, - } - } - fn handle0( - &self, - buf: &mut [u8], - extend: &mut [u8], - route_key: RouteKey, - context: &ChannelContext, - ) -> anyhow::Result<()> { - let net_packet = NetPacket::new(buf)?; - - let extend = NetPacket::unchecked(extend); - if net_packet.ttl() == 0 || net_packet.source_ttl() < net_packet.ttl() { - log::warn!("丢弃过时包:{:?} {}", net_packet.head(), route_key.addr); - return Ok(()); - } - let current_device = self.current_device.load(); - let dest = net_packet.destination(); - if dest == current_device.virtual_ip - || dest.is_broadcast() - || dest.is_multicast() - || dest == SELF_IP - || dest.is_unspecified() - || dest == current_device.broadcast_ip - { - // 统计流量 - if let Some(down_traffic_meter) = &context.down_traffic_meter { - down_traffic_meter.add_traffic(net_packet.source(), net_packet.data_len()) - } - //发给自己的包 - if net_packet.is_gateway() { - //服务端-客户端包 - self.server - .handle(net_packet, extend, route_key, context, ¤t_device) - } else { - //客户端-客户端包 - self.client - .handle(net_packet, extend, route_key, context, ¤t_device) - } - } else { - //转发包 - self.turn - .handle(net_packet, extend, route_key, context, ¤t_device) - } - } -} - -pub trait PacketHandler { - fn handle( - &self, - net_packet: NetPacket<&mut [u8]>, - extend: NetPacket<&mut [u8]>, - route_key: RouteKey, - context: &ChannelContext, - current_device: &CurrentDeviceInfo, - ) -> anyhow::Result<()>; -} diff --git a/vnt/src/handle/recv_data/server.rs b/vnt/src/handle/recv_data/server.rs deleted file mode 100644 index bd66f24..0000000 --- a/vnt/src/handle/recv_data/server.rs +++ /dev/null @@ -1,599 +0,0 @@ -use anyhow::anyhow; -use std::collections::HashMap; -use std::io; -use std::net::Ipv4Addr; -use std::sync::Arc; -#[cfg(feature = "server_encrypt")] -use std::time::{Duration, Instant}; - -use crossbeam_utils::atomic::AtomicCell; -use packet::icmp::{icmp, Kind}; -use packet::ip::ipv4; -use packet::ip::ipv4::packet::IpV4Packet; -use parking_lot::Mutex; -use protobuf::Message; - -use crate::channel::context::ChannelContext; -use crate::channel::{Route, RouteKey}; -use crate::cipher::Cipher; -#[cfg(feature = "server_encrypt")] -use crate::cipher::RsaCipher; -use crate::external_route::ExternalRoute; -use crate::handle::callback::{ErrorInfo, ErrorType, HandshakeInfo, RegisterInfo, VntCallback}; -#[cfg(feature = "server_encrypt")] -use crate::handle::handshaker; -use crate::handle::handshaker::Handshake; -use crate::handle::recv_data::PacketHandler; -use crate::handle::{registrar, BaseConfigInfo, ConnectStatus, CurrentDeviceInfo, PeerDeviceInfo}; -use crate::nat::NatTest; -use crate::proto::message::{DeviceList, HandshakeResponse, RegistrationResponse}; -use crate::protocol::body::ENCRYPTION_RESERVED; -use crate::protocol::control_packet::ControlPacket; -use crate::protocol::error_packet::InErrorPacket; -use crate::protocol::{ip_turn_packet, service_packet, NetPacket, Protocol, MAX_TTL}; -use crate::tun_tap_device::vnt_device::DeviceWrite; -use crate::{proto, PeerClientInfo}; - -/// 处理来源于服务端的包 -#[derive(Clone)] -pub struct ServerPacketHandler { - #[cfg(feature = "server_encrypt")] - rsa_cipher: Arc>>, - server_cipher: Cipher, - current_device: Arc>, - device: Device, - device_map: Arc)>>, - config_info: BaseConfigInfo, - nat_test: NatTest, - callback: Call, - #[cfg(feature = "server_encrypt")] - up_key_time: Arc>, - external_route: ExternalRoute, - handshake: Handshake, - #[cfg(feature = "integrated_tun")] - tun_device_helper: crate::tun_tap_device::tun_create_helper::TunDeviceHelper, -} - -impl ServerPacketHandler { - pub fn new( - #[cfg(feature = "server_encrypt")] rsa_cipher: Arc>>, - server_cipher: Cipher, - current_device: Arc>, - device: Device, - device_map: Arc)>>, - config_info: BaseConfigInfo, - nat_test: NatTest, - callback: Call, - external_route: ExternalRoute, - handshake: Handshake, - #[cfg(feature = "integrated_tun")] - tun_device_helper: crate::tun_tap_device::tun_create_helper::TunDeviceHelper, - ) -> Self { - Self { - #[cfg(feature = "server_encrypt")] - rsa_cipher, - server_cipher, - current_device, - device, - device_map, - config_info, - nat_test, - callback, - #[cfg(feature = "server_encrypt")] - up_key_time: Arc::new(AtomicCell::new( - Instant::now() - .checked_sub(Duration::from_secs(60)) - .unwrap_or(Instant::now()), - )), - external_route, - handshake, - #[cfg(feature = "integrated_tun")] - tun_device_helper, - } - } -} - -impl PacketHandler for ServerPacketHandler { - fn handle( - &self, - mut net_packet: NetPacket<&mut [u8]>, - _extend: NetPacket<&mut [u8]>, - route_key: RouteKey, - context: &ChannelContext, - current_device: &CurrentDeviceInfo, - ) -> anyhow::Result<()> { - if !current_device.is_server_addr(route_key.addr) { - //拦截不是服务端的流量 - log::warn!( - "route_key={:?},不是来源于服务端地址{}", - route_key, - current_device.connect_server - ); - } - context - .route_table - .update_read_time(&net_packet.source(), &route_key); - if net_packet.protocol() == Protocol::Error - && net_packet.transport_protocol() - == crate::protocol::error_packet::Protocol::NoKey.into() - { - //服务端通知客户端上传密钥 - #[cfg(feature = "server_encrypt")] - { - let mutex_guard = self.rsa_cipher.lock(); - if let Some(rsa_cipher) = mutex_guard.as_ref() { - let last = self.up_key_time.load(); - if last.elapsed() < Duration::from_secs(1) - || self - .up_key_time - .compare_exchange(last, Instant::now()) - .is_err() - { - //短时间不重复上传服务端密钥 - return Ok(()); - } - if let Some(key) = self.server_cipher.key() { - log::info!("上传密钥到服务端:{:?}", route_key); - let packet = handshaker::secret_handshake_request_packet( - rsa_cipher, - self.config_info.token.clone(), - key, - )?; - context.send_by_key(&packet, route_key)?; - } - } - } - return Ok(()); - } else if net_packet.protocol() == Protocol::Service - && net_packet.transport_protocol() == service_packet::Protocol::HandshakeResponse.into() - { - let response = HandshakeResponse::parse_from_bytes(net_packet.payload()) - .map_err(|e| anyhow!("HandshakeResponse {:?}", e))?; - log::info!("握手响应:{:?},{}", route_key, response); - //设置为默认通道 - context.set_default_route_key(route_key); - //如果开启了加密,则发送加密握手请求 - #[cfg(feature = "server_encrypt")] - if let Some(key) = self.server_cipher.key() { - { - let guard = self.rsa_cipher.lock(); - if let Some(rsa_cipher) = guard.as_ref() { - if rsa_cipher.finger() == &response.key_finger { - let packet = handshaker::secret_handshake_request_packet( - rsa_cipher, - self.config_info.token.clone(), - key, - )?; - drop(guard); - context.send_by_key(&packet, route_key)?; - return Ok(()); - } - log::warn!( - "拒绝服务端密钥对变化,原指纹:{:?},新指纹:{:?},addr:{:?}", - rsa_cipher.finger(), - response.key_finger, - route_key - ); - return Ok(()); - } - drop(guard); - } - let rsa_cipher = RsaCipher::new(&response.public_key)?; - if rsa_cipher.finger() != &response.key_finger { - log::info!( - "服务端密钥和指纹不匹 配拒绝握手,指纹1:{:?},指纹2:{:?}", - rsa_cipher.finger(), - response.key_finger - ); - return Ok(()); - } - let handshake_info = HandshakeInfo::new( - rsa_cipher.public_key()?.clone(), - response.key_finger, - response.version, - ); - log::info!("加密握手请求:{:?}", handshake_info); - - if self.callback.handshake(handshake_info) { - let packet = handshaker::secret_handshake_request_packet( - &rsa_cipher, - self.config_info.token.clone(), - key, - )?; - context.send_by_key(&packet, route_key)?; - self.rsa_cipher.lock().replace(rsa_cipher); - } - return Ok(()); - } - #[cfg(feature = "server_encrypt")] - if let Ok(rsa_cipher) = RsaCipher::new(&response.public_key) { - self.rsa_cipher.lock().replace(rsa_cipher); - } - let handshake_info = HandshakeInfo::new_no_secret(response.version); - if self.callback.handshake(handshake_info) { - //没有加密,则发送注册请求 - self.register(current_device, context, route_key)?; - } - - return Ok(()); - } - //服务端数据解密 - self.server_cipher.decrypt_ipv4(&mut net_packet)?; - match net_packet.protocol() { - Protocol::Service => { - self.service(context, current_device, net_packet, route_key)?; - } - Protocol::Error => { - self.error(context, current_device, net_packet, route_key)?; - } - Protocol::Control => { - self.control(context, current_device, net_packet, route_key)?; - } - Protocol::IpTurn => { - match ip_turn_packet::Protocol::from(net_packet.transport_protocol()) { - ip_turn_packet::Protocol::Ipv4 => { - let ipv4 = IpV4Packet::new(net_packet.payload())?; - match ipv4.protocol() { - ipv4::protocol::Protocol::Icmp => { - if ipv4.destination_ip() == current_device.virtual_ip { - let icmp_packet = icmp::IcmpPacket::new(ipv4.payload())?; - if icmp_packet.kind() == Kind::EchoReply { - //网关ip ping的回应 - self.device.write(net_packet.payload())?; - return Ok(()); - } - } - } - _ => {} - } - } - ip_turn_packet::Protocol::WGIpv4 => { - if self.config_info.allow_wire_guard { - self.device.write(net_packet.payload())?; - } - } - ip_turn_packet::Protocol::Ipv4Broadcast => {} - ip_turn_packet::Protocol::Unknown(_) => {} - } - } - Protocol::OtherTurn => {} - Protocol::Unknown(_) => {} - } - Ok(()) - } -} - -impl ServerPacketHandler { - fn service( - &self, - context: &ChannelContext, - current_device: &CurrentDeviceInfo, - net_packet: NetPacket<&mut [u8]>, - route_key: RouteKey, - ) -> anyhow::Result<()> { - match service_packet::Protocol::from(net_packet.transport_protocol()) { - service_packet::Protocol::RegistrationResponse => { - let response = RegistrationResponse::parse_from_bytes(net_packet.payload()) - .map_err(|e| { - io::Error::new( - io::ErrorKind::Other, - format!("RegistrationResponse {:?}", e), - ) - })?; - let virtual_ip = Ipv4Addr::from(response.virtual_ip); - let virtual_netmask = Ipv4Addr::from(response.virtual_netmask); - let virtual_gateway = Ipv4Addr::from(response.virtual_gateway); - let virtual_network = - Ipv4Addr::from(response.virtual_ip & response.virtual_netmask); - let register_info = RegisterInfo::new(virtual_ip, virtual_netmask, virtual_gateway); - log::info!("注册成功:{:?}", register_info); - if self.callback.register(register_info) { - let route = Route::from_default_rt(route_key, 1); - context - .route_table - .add_route_if_absent(virtual_gateway, route); - let public_ip = response.public_ip.into(); - let public_port = response.public_port as u16; - self.nat_test - .update_addr(route_key.index(), public_ip, public_port); - if route_key.protocol().is_tcp() { - log::info!("更新公网tcp端口 {public_port}"); - self.nat_test.update_tcp_port(public_port); - } - let old = current_device; - let mut cur = *current_device; - loop { - let mut new_current_device = cur; - new_current_device.update(virtual_ip, virtual_netmask, virtual_gateway); - new_current_device.virtual_ip = virtual_ip; - new_current_device.virtual_netmask = virtual_netmask; - new_current_device.virtual_gateway = virtual_gateway; - new_current_device.status = ConnectStatus::Connected; - if let Err(c) = self - .current_device - .compare_exchange(cur, new_current_device) - { - cur = c; - } else { - break; - } - } - - if old.virtual_ip != virtual_ip - || old.virtual_gateway != virtual_gateway - || old.virtual_netmask != virtual_netmask - { - if old.virtual_ip != Ipv4Addr::UNSPECIFIED { - log::info!("ip发生变化,old:{:?},response={:?}", old, response); - } - let device_config = crate::handle::callback::DeviceConfig::new( - #[cfg(feature = "integrated_tun")] - #[cfg(target_os = "windows")] - self.config_info.tap, - #[cfg(feature = "integrated_tun")] - #[cfg(any( - target_os = "windows", - target_os = "linux", - target_os = "macos" - ))] - self.config_info.device_name.clone(), - self.config_info.mtu, - virtual_ip, - virtual_netmask, - virtual_gateway, - virtual_network, - self.external_route.to_route(), - ); - #[cfg(not(feature = "integrated_tun"))] - self.callback.create_device(device_config); - #[cfg(feature = "integrated_tun")] - { - self.tun_device_helper.stop(); - #[cfg(any( - target_os = "windows", - target_os = "linux", - target_os = "macos" - ))] - match crate::tun_tap_device::create_device( - device_config, - &self.callback, - ) { - Ok(device) => { - let tun_info = crate::handle::callback::DeviceInfo::new( - device.name().unwrap_or("unknown".into()), - "".into(), - ); - log::info!("tun信息{:?}", tun_info); - self.callback.create_tun(tun_info); - self.tun_device_helper - .start(device, self.config_info.allow_wire_guard)?; - } - Err(e) => { - log::error!("{:?}", e); - self.callback.error(e); - } - } - #[cfg(target_os = "android")] - { - let device_config = crate::handle::callback::DeviceConfig::new( - self.config_info.mtu, - virtual_ip, - virtual_netmask, - virtual_gateway, - virtual_network, - self.external_route.to_route(), - ); - let device_fd = self.callback.generate_tun(device_config); - if device_fd == 0 { - self.callback.error(ErrorInfo::new_msg( - ErrorType::FailedToCrateDevice, - "device_fd == 0".into(), - )); - } else { - let device = - unsafe { tun_rs::SyncDevice::from_fd(device_fd as _) }; - if let Err(e) = self - .tun_device_helper - .start(Arc::new(device), self.config_info.allow_wire_guard) - { - self.callback.error(ErrorInfo::new_msg( - ErrorType::FailedToCrateDevice, - format!("{:?}", e), - )); - } - } - } - } - } - self.set_device_info_list(response.device_info_list, response.epoch as _); - if old.status.offline() { - self.callback.success(); - } - } - } - service_packet::Protocol::PushDeviceList => { - let response = DeviceList::parse_from_bytes(net_packet.payload()).map_err(|e| { - io::Error::new(io::ErrorKind::Other, format!("PushDeviceList {:?}", e)) - })?; - self.set_device_info_list(response.device_info_list, response.epoch as _); - } - service_packet::Protocol::SecretHandshakeResponse => { - log::info!("SecretHandshakeResponse"); - //加密握手结束,发送注册数据 - self.register(current_device, context, route_key)?; - } - _ => { - log::warn!( - "service_packet::Protocol::Unknown = {:?}", - net_packet.head() - ); - } - } - Ok(()) - } - fn set_device_info_list(&self, device_info_list: Vec, epoch: u16) { - let ip_list: Vec = device_info_list - .into_iter() - .map(|info| { - PeerDeviceInfo::new( - Ipv4Addr::from(info.virtual_ip), - info.name, - info.device_status as u8, - info.client_secret, - info.client_secret_hash, - info.wireguard, - ) - }) - .collect(); - { - let mut dev = self.device_map.lock(); - //这里可能会收到旧的消息,但是随着时间推移总会收到新的 - dev.0 = epoch; - dev.1.clear(); - for info in ip_list.clone() { - dev.1.insert(info.virtual_ip, info); - } - } - self.callback.peer_client_list( - ip_list - .into_iter() - .map(|v| PeerClientInfo::new(v.virtual_ip, v.name, v.status, v.client_secret)) - .collect(), - ); - } - fn register( - &self, - current_device: &CurrentDeviceInfo, - context: &ChannelContext, - route_key: RouteKey, - ) -> anyhow::Result<()> { - if current_device.status.online() { - log::info!("已连接的不需要注册,{:?}", self.config_info); - return Ok(()); - } - //设置为默认通道 - context.set_default_route_key(route_key); - let token = self.config_info.token.clone(); - let device_id = self.config_info.device_id.clone(); - let name = self.config_info.name.clone(); - let client_secret = self - .config_info - .client_secret_hash - .as_ref() - .map(|v| v.as_ref()); - let mut ip = self.config_info.ip; - if ip.is_none() { - ip = Some(current_device.virtual_ip) - } - let response = registrar::registration_request_packet( - &self.server_cipher, - token, - device_id, - name, - ip, - false, - false, - client_secret, - )?; - log::info!("发送注册请求,{:?}", self.config_info); - //注册请求只发送到默认通道 - context.send_default(&response, current_device.connect_server)?; - Ok(()) - } - fn error( - &self, - context: &ChannelContext, - _current_device: &CurrentDeviceInfo, - net_packet: NetPacket<&mut [u8]>, - route_key: RouteKey, - ) -> io::Result<()> { - match InErrorPacket::new(net_packet.transport_protocol(), net_packet.payload())? { - InErrorPacket::TokenError => { - // token错误,可能是服务端设置了白名单 - let err = ErrorInfo::new(ErrorType::TokenError); - self.callback.error(err); - } - InErrorPacket::Disconnect => { - crate::handle::change_status(&self.current_device, ConnectStatus::Connecting); - let err = ErrorInfo::new(ErrorType::Disconnect); - self.callback.error(err); - //掉线epoch要归零 - { - let mut dev = self.device_map.lock(); - dev.0 = 0; - drop(dev); - } - self.handshake - .send(context, self.config_info.server_secret, route_key.addr)?; - // self.register(current_device, context, route_key)?; - } - InErrorPacket::AddressExhausted => { - // 地址用尽 - let err = ErrorInfo::new(ErrorType::AddressExhausted); - self.callback.error(err); - } - InErrorPacket::OtherError(e) => { - let err = ErrorInfo::new_msg(ErrorType::Unknown, e.message()?); - self.callback.error(err); - } - InErrorPacket::IpAlreadyExists => { - let err = ErrorInfo::new(ErrorType::IpAlreadyExists); - self.callback.error(err); - } - InErrorPacket::InvalidIp => { - let err = ErrorInfo::new(ErrorType::InvalidIp); - self.callback.error(err); - } - InErrorPacket::NoKey => { - //这个类型最开头已经处理过,这里忽略 - } - } - Ok(()) - } - fn control( - &self, - context: &ChannelContext, - current_device: &CurrentDeviceInfo, - net_packet: NetPacket<&mut [u8]>, - route_key: RouteKey, - ) -> anyhow::Result<()> { - match ControlPacket::new(net_packet.transport_protocol(), net_packet.payload())? { - ControlPacket::PongPacket(pong_packet) => { - let current_time = crate::handle::now_time() as u16; - if current_time < pong_packet.time() { - return Ok(()); - } - let metric = net_packet.source_ttl() - net_packet.ttl() + 1; - let rt = (current_time - pong_packet.time()) as i64; - let route = Route::from(route_key, metric, rt); - context.route_table.add_route(net_packet.source(), route); - let epoch = self.device_map.lock().0; - if pong_packet.epoch() != epoch { - //纪元不一致,可能有新客户端连接,向服务端拉取客户端列表 - let mut poll_device = NetPacket::new_encrypt([0; 12 + ENCRYPTION_RESERVED])?; - poll_device.set_source(current_device.virtual_ip); - poll_device.set_destination(current_device.virtual_gateway); - poll_device.set_default_version(); - poll_device.set_gateway_flag(true); - poll_device.first_set_ttl(MAX_TTL); - poll_device.set_protocol(Protocol::Service); - poll_device - .set_transport_protocol(service_packet::Protocol::PullDeviceList.into()); - self.server_cipher.encrypt_ipv4(&mut poll_device)?; - //发送到默认服务端即可 - context.send_default(&poll_device, current_device.connect_server)?; - } - } - ControlPacket::AddrResponse(addr_packet) => { - //更新本地公网ipv4 - self.nat_test.update_addr( - route_key.index(), - addr_packet.ipv4(), - addr_packet.port(), - ); - } - _ => {} - } - Ok(()) - } -} diff --git a/vnt/src/handle/recv_data/turn.rs b/vnt/src/handle/recv_data/turn.rs deleted file mode 100644 index 59f82d9..0000000 --- a/vnt/src/handle/recv_data/turn.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::channel::context::ChannelContext; -use crate::channel::RouteKey; -use crate::handle::recv_data::PacketHandler; -use crate::handle::CurrentDeviceInfo; -use crate::protocol::NetPacket; -use anyhow::Context; - -/// 处理客户端中转包 -#[derive(Clone)] -pub struct TurnPacketHandler {} - -impl TurnPacketHandler { - pub fn new() -> Self { - Self {} - } -} - -impl PacketHandler for TurnPacketHandler { - fn handle( - &self, - mut net_packet: NetPacket<&mut [u8]>, - _extend: NetPacket<&mut [u8]>, - route_key: RouteKey, - context: &ChannelContext, - _current_device: &CurrentDeviceInfo, - ) -> anyhow::Result<()> { - // ttl减一 - let ttl = net_packet.incr_ttl(); - if ttl > 0 { - if net_packet.is_gateway() { - // 暂时不转发服务端包 - return Ok(()); - } - let destination = net_packet.destination(); - if let Some(route) = context.route_table.route_one(&destination) { - if route.addr == route_key.addr { - //防止环路 - log::warn!("来源和目标相同 {:?},{:?}", route_key, net_packet.head()); - return Ok(()); - } - if route.metric <= ttl { - return context - .send_by_key(&net_packet, route.route_key()) - .context("转发失败"); - } - } - //其他没有路由的不转发 - } - log::info!("没有路由 {:?},{:?}", route_key, net_packet.head()); - Ok(()) - } -} diff --git a/vnt/src/handle/registrar.rs b/vnt/src/handle/registrar.rs deleted file mode 100644 index 417fea5..0000000 --- a/vnt/src/handle/registrar.rs +++ /dev/null @@ -1,54 +0,0 @@ -use anyhow::anyhow; -use std::net::Ipv4Addr; - -use protobuf::Message; - -use crate::cipher::Cipher; -use crate::handle::{GATEWAY_IP, SELF_IP}; -use crate::proto::message::RegistrationRequest; -use crate::protocol::body::ENCRYPTION_RESERVED; -use crate::protocol::{service_packet, NetPacket, Protocol, MAX_TTL}; - -/// 注册数据 -pub fn registration_request_packet( - server_cipher: &Cipher, - token: String, - device_id: String, - name: String, - ip: Option, - is_fast: bool, - allow_ip_change: bool, - client_secret_hash: Option<&[u8]>, -) -> anyhow::Result>> { - let mut request = RegistrationRequest::new(); - request.token = token; - request.device_id = device_id; - request.name = name; - if let Some(ip) = ip { - request.virtual_ip = ip.into(); - } - request.allow_ip_change = allow_ip_change; - request.is_fast = is_fast; - request.version = crate::VNT_VERSION.to_string(); - if let Some(client_secret_hash) = client_secret_hash { - request.client_secret = true; - request - .client_secret_hash - .extend_from_slice(client_secret_hash); - } - let bytes = request - .write_to_bytes() - .map_err(|e| anyhow!("RegistrationRequest {:?}", e))?; - let buf = vec![0u8; 12 + bytes.len() + ENCRYPTION_RESERVED]; - let mut net_packet = NetPacket::new_encrypt(buf)?; - net_packet.set_destination(GATEWAY_IP); - net_packet.set_source(SELF_IP); - net_packet.set_default_version(); - net_packet.set_gateway_flag(true); - net_packet.set_protocol(Protocol::Service); - net_packet.set_transport_protocol(service_packet::Protocol::RegistrationRequest.into()); - net_packet.first_set_ttl(MAX_TTL); - net_packet.set_payload(&bytes)?; - server_cipher.encrypt_ipv4(&mut net_packet)?; - Ok(net_packet) -} diff --git a/vnt/src/handle/tun_tap/mod.rs b/vnt/src/handle/tun_tap/mod.rs deleted file mode 100644 index 6571e48..0000000 --- a/vnt/src/handle/tun_tap/mod.rs +++ /dev/null @@ -1,35 +0,0 @@ -pub mod tun_handler; - -use crossbeam_utils::atomic::AtomicCell; -use parking_lot::Mutex; -use std::sync::Arc; -mod platform; - -pub(crate) use platform::*; - -/// 仅仅是停止tun,不停止vnt -#[derive(Clone, Default)] -pub struct DeviceStop { - f: Arc>>>, - stopped: Arc>, -} - -impl DeviceStop { - pub fn set_stop_fn(&self, f: F) - where - F: FnOnce() + Send + 'static, - { - self.f.lock().replace(Box::new(f)); - } - pub fn stop(&self) { - if let Some(f) = self.f.lock().take() { - f() - } - } - pub fn stopped(&self) { - self.stopped.store(true); - } - pub fn is_stopped(&self) -> bool { - self.stopped.load() - } -} diff --git a/vnt/src/handle/tun_tap/platform.rs b/vnt/src/handle/tun_tap/platform.rs deleted file mode 100644 index 4cdc5d3..0000000 --- a/vnt/src/handle/tun_tap/platform.rs +++ /dev/null @@ -1,115 +0,0 @@ -use crate::channel::context::ChannelContext; -use crate::channel::BUFFER_SIZE; -use crate::cipher::Cipher; -use crate::compression::Compressor; -use crate::external_route::ExternalRoute; -use crate::handle::tun_tap::DeviceStop; -use crate::handle::{CurrentDeviceInfo, PeerDeviceInfo}; -#[cfg(feature = "ip_proxy")] -use crate::ip_proxy::IpProxyMap; -use crate::util::StopManager; -use crossbeam_utils::atomic::AtomicCell; -use parking_lot::Mutex; -use std::collections::HashMap; -use std::net::Ipv4Addr; -use std::sync::Arc; -use tun_rs::{InterruptEvent, SyncDevice}; - -pub(crate) fn start_simple( - stop_manager: StopManager, - context: &ChannelContext, - device: Arc, - current_device: Arc>, - ip_route: ExternalRoute, - #[cfg(feature = "ip_proxy")] ip_proxy_map: Option, - client_cipher: Cipher, - server_cipher: Cipher, - device_map: Arc)>>, - compressor: Compressor, - device_stop: DeviceStop, - allow_wire_guard: bool, -) -> anyhow::Result<()> { - let event = Arc::new(InterruptEvent::new()?); - let worker = { - let event = event.clone(); - stop_manager.add_listener("tun_device".into(), move || { - if let Err(e) = event.trigger() { - log::warn!("{:?}", e); - } - })? - }; - let worker_cell = Arc::new(AtomicCell::new(Some(worker))); - - { - let worker_cell = worker_cell.clone(); - device_stop.set_stop_fn(move || { - if let Some(worker) = worker_cell.take() { - worker.stop_self() - } - }); - } - if let Err(e) = start_simple0( - context, - device, - event, - current_device, - ip_route, - #[cfg(feature = "ip_proxy")] - ip_proxy_map, - client_cipher, - server_cipher, - device_map, - compressor, - allow_wire_guard, - ) { - log::error!("{:?}", e); - } - device_stop.stopped(); - if let Some(worker) = worker_cell.take() { - worker.stop_all(); - } - Ok(()) -} - -fn start_simple0( - context: &ChannelContext, - device: Arc, - event: Arc, - current_device: Arc>, - ip_route: ExternalRoute, - #[cfg(feature = "ip_proxy")] ip_proxy_map: Option, - client_cipher: Cipher, - server_cipher: Cipher, - device_map: Arc)>>, - compressor: Compressor, - allow_wire_guard: bool, -) -> anyhow::Result<()> { - let mut buf = [0; BUFFER_SIZE]; - let mut extend = [0; BUFFER_SIZE]; - loop { - let len = device.recv_intr(&mut buf[12..],&event)? + 12; - // buf是重复利用的,需要重置头部 - buf[..12].fill(0); - match crate::handle::tun_tap::tun_handler::handle( - context, - &mut buf, - len, - &mut extend, - &device, - current_device.load(), - &ip_route, - #[cfg(feature = "ip_proxy")] - &ip_proxy_map, - &client_cipher, - &server_cipher, - &device_map, - &compressor, - allow_wire_guard, - ) { - Ok(_) => {} - Err(e) => { - log::warn!("tun/tap {:?}", e) - } - } - } -} diff --git a/vnt/src/handle/tun_tap/tun_handler.rs b/vnt/src/handle/tun_tap/tun_handler.rs deleted file mode 100644 index d3be016..0000000 --- a/vnt/src/handle/tun_tap/tun_handler.rs +++ /dev/null @@ -1,288 +0,0 @@ -use crossbeam_utils::atomic::AtomicCell; -use packet::icmp::icmp::IcmpPacket; -use packet::icmp::Kind; -use packet::ip::ipv4::packet::IpV4Packet; -use packet::ip::ipv4::protocol::Protocol; -use parking_lot::Mutex; -use std::collections::HashMap; -use std::net::Ipv4Addr; -use std::sync::Arc; -use std::{io, thread}; -use tun_rs::SyncDevice; - -use crate::channel::context::ChannelContext; -use crate::channel::sender::{send_to_wg, send_to_wg_broadcast}; -use crate::cipher::Cipher; -use crate::compression::Compressor; -use crate::external_route::ExternalRoute; -use crate::handle::tun_tap::DeviceStop; -use crate::handle::{CurrentDeviceInfo, PeerDeviceInfo}; -#[cfg(feature = "ip_proxy")] -use crate::ip_proxy::IpProxyMap; -#[cfg(feature = "ip_proxy")] -use crate::ip_proxy::ProxyHandler; -use crate::protocol; -use crate::protocol::body::ENCRYPTION_RESERVED; -use crate::protocol::ip_turn_packet::BroadcastPacket; -use crate::protocol::{ip_turn_packet, NetPacket, MAX_TTL}; -use crate::util::StopManager; -fn icmp(device_writer: &SyncDevice, mut ipv4_packet: IpV4Packet<&mut [u8]>) -> anyhow::Result<()> { - if ipv4_packet.protocol() == Protocol::Icmp { - let mut icmp = IcmpPacket::new(ipv4_packet.payload_mut())?; - if icmp.kind() == Kind::EchoRequest { - icmp.set_kind(Kind::EchoReply); - icmp.update_checksum(); - let src = ipv4_packet.source_ip(); - ipv4_packet.set_source_ip(ipv4_packet.destination_ip()); - ipv4_packet.set_destination_ip(src); - ipv4_packet.update_checksum(); - device_writer.send(ipv4_packet.buffer)?; - } - } - Ok(()) -} - -pub fn start( - stop_manager: StopManager, - context: ChannelContext, - device: Arc, - current_device: Arc>, - ip_route: ExternalRoute, - #[cfg(feature = "ip_proxy")] ip_proxy_map: Option, - client_cipher: Cipher, - server_cipher: Cipher, - device_map: Arc)>>, - compressor: Compressor, - device_stop: DeviceStop, - allow_wire_guard: bool, -) -> io::Result<()> { - thread::Builder::new() - .name("tunHandlerS".into()) - .spawn(move || { - if let Err(e) = crate::handle::tun_tap::start_simple( - stop_manager, - &context, - device, - current_device, - ip_route, - #[cfg(feature = "ip_proxy")] - ip_proxy_map, - client_cipher, - server_cipher, - device_map, - compressor, - device_stop, - allow_wire_guard, - ) { - log::warn!("stop:{}", e); - } - })?; - - Ok(()) -} - -fn broadcast( - server_cipher: &Cipher, - sender: &ChannelContext, - net_packet: &mut NetPacket<&mut [u8]>, - current_device: &CurrentDeviceInfo, - device_map: &Mutex<(u16, HashMap)>, -) -> anyhow::Result<()> { - let list: Vec = device_map - .lock() - .1 - .values() - .filter(|info| !info.wireguard && info.status.is_online()) - .map(|info| info.virtual_ip) - .collect(); - if list.is_empty() { - return Ok(()); - } - const MAX_COUNT: usize = 8; - let mut p2p_ips = Vec::with_capacity(8); - let mut relay = false; - let mut overflow = false; - for (index, peer_ip) in list.into_iter().enumerate() { - if index > MAX_COUNT { - overflow = true; - break; - } - if let Some(route) = sender.route_table.route_one_p2p(&peer_ip) { - if sender.send_by_key(&net_packet, route.route_key()).is_ok() { - p2p_ips.push(peer_ip); - continue; - } - } - relay = true; - } - if !overflow && !relay { - //全部p2p,不需要服务器中转 - return Ok(()); - } - if current_device.status.offline() { - //离线的不再转发 - return Ok(()); - } - if p2p_ips.is_empty() { - //都没有p2p则直接由服务器转发 - sender.send_default(&net_packet, current_device.connect_server)?; - return Ok(()); - } - - let buf = vec![0u8; 12 + 1 + p2p_ips.len() * 4 + net_packet.data_len() + ENCRYPTION_RESERVED]; - //剩余的发送到服务端,需要告知哪些已发送过 - let mut server_packet = NetPacket::new_encrypt(buf)?; - server_packet.set_default_version(); - server_packet.set_gateway_flag(true); - server_packet.first_set_ttl(MAX_TTL); - server_packet.set_source(net_packet.source()); - //使用对应的目的地址 - server_packet.set_destination(net_packet.destination()); - server_packet.set_protocol(protocol::Protocol::IpTurn); - server_packet.set_transport_protocol(ip_turn_packet::Protocol::Ipv4Broadcast.into()); - - let mut broadcast = BroadcastPacket::unchecked(server_packet.payload_mut()); - broadcast.set_address(&p2p_ips)?; - broadcast.set_data(net_packet.buffer())?; - server_cipher.encrypt_ipv4(&mut server_packet)?; - sender.send_default(&server_packet, current_device.connect_server)?; - Ok(()) -} - -/// 接收tun数据,并且转发到udp上 -/// 实现一个原地发送,必须保证是如下结构 -/// |12字节开头|ip报文|至少1024字节结尾| -/// -pub(crate) fn handle( - context: &ChannelContext, - buf: &mut [u8], - data_len: usize, //数据总长度=12+ip包长度 - extend: &mut [u8], - device_writer: &SyncDevice, - current_device: CurrentDeviceInfo, - ip_route: &ExternalRoute, - #[cfg(feature = "ip_proxy")] proxy_map: &Option, - client_cipher: &Cipher, - server_cipher: &Cipher, - device_map: &Mutex<(u16, HashMap)>, - compressor: &Compressor, - allow_wire_guard: bool, -) -> anyhow::Result<()> { - //忽略掉结构不对的情况(ipv6数据、win tap会读到空数据),不然日志打印太多了 - let ipv4_packet = match IpV4Packet::new(&mut buf[12..data_len]) { - Ok(packet) => packet, - Err(_) => return Ok(()), - }; - let src_ip = ipv4_packet.source_ip(); - let dest_ip = ipv4_packet.destination_ip(); - if src_ip == dest_ip { - return icmp(&device_writer, ipv4_packet); - } - let protocol = ipv4_packet.protocol(); - let src_ip = ipv4_packet.source_ip(); - let mut dest_ip = ipv4_packet.destination_ip(); - let mut net_packet = NetPacket::new0(data_len, buf)?; - let mut out = NetPacket::unchecked(extend); - net_packet.set_default_version(); - net_packet.set_protocol(protocol::Protocol::IpTurn); - net_packet.set_transport_protocol(ip_turn_packet::Protocol::Ipv4.into()); - net_packet.first_set_ttl(6); - net_packet.set_source(src_ip); - net_packet.set_destination(dest_ip); - if dest_ip == current_device.virtual_gateway { - // 发到网关的加密方式不一样,要单独处理 - if protocol == Protocol::Icmp { - net_packet.set_gateway_flag(true); - server_cipher.encrypt_ipv4(&mut net_packet)?; - context.send_default(&net_packet, current_device.connect_server)?; - } - return Ok(()); - } - if !dest_ip.is_multicast() && !dest_ip.is_broadcast() && current_device.broadcast_ip != dest_ip - { - if current_device.not_in_network(dest_ip) { - if let Some(r_dest_ip) = ip_route.route(&dest_ip) { - //路由的目标不能是自己 - if r_dest_ip == src_ip { - return Ok(()); - } - //需要修改目的地址 - dest_ip = r_dest_ip; - net_packet.set_destination(r_dest_ip); - } else { - return Ok(()); - } - } - #[cfg(feature = "ip_proxy")] - if let Some(proxy_map) = proxy_map { - let mut ipv4_packet = IpV4Packet::new(net_packet.payload_mut())?; - proxy_map.send_handle(&mut ipv4_packet)?; - } - } - - if dest_ip.is_multicast() { - //当作广播处理 - dest_ip = Ipv4Addr::BROADCAST; - net_packet.set_destination(Ipv4Addr::BROADCAST); - } - let is_broadcast = dest_ip.is_broadcast() || current_device.broadcast_ip == dest_ip; - if allow_wire_guard { - if is_broadcast { - // wg客户端和vnt客户端分开广播 - let exists_wg = device_map - .lock() - .1 - .values() - .any(|v| v.status.is_online() && v.wireguard); - if exists_wg { - send_to_wg_broadcast(context, &net_packet, server_cipher, ¤t_device)?; - } - } else { - // 如果是wg客户端则发到vnts转发 - let guard = device_map.lock(); - if let Some(peer_info) = guard.1.get(&dest_ip) { - if peer_info.status.is_offline() { - return Ok(()); - } - if peer_info.wireguard { - drop(guard); - send_to_wg(context, &mut net_packet, server_cipher, ¤t_device)?; - return Ok(()); - } - } - } - } - - let mut net_packet = if compressor.compress(&net_packet, &mut out)? { - out.set_default_version(); - out.set_protocol(protocol::Protocol::IpTurn); - out.set_transport_protocol(ip_turn_packet::Protocol::Ipv4.into()); - out.first_set_ttl(6); - out.set_source(src_ip); - out.set_destination(dest_ip); - out - } else { - net_packet - }; - if is_broadcast { - // 广播 发送到直连目标 - client_cipher.encrypt_ipv4(&mut net_packet)?; - broadcast( - server_cipher, - context, - &mut net_packet, - ¤t_device, - device_map, - )?; - return Ok(()); - } - - client_cipher.encrypt_ipv4(&mut net_packet)?; - context.send_ipv4_by_id( - &net_packet, - &dest_ip, - current_device.connect_server, - current_device.status.online(), - )?; - Ok(()) -} diff --git a/vnt/src/ip_proxy/icmp_proxy.rs b/vnt/src/ip_proxy/icmp_proxy.rs deleted file mode 100644 index 9afca4b..0000000 --- a/vnt/src/ip_proxy/icmp_proxy.rs +++ /dev/null @@ -1,225 +0,0 @@ -use anyhow::Context; -use std::collections::HashMap; -use std::io; -use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}; -use std::sync::Arc; - -use crossbeam_utils::atomic::AtomicCell; -use parking_lot::Mutex; -use tokio::net::UdpSocket; - -use packet::icmp::icmp; -use packet::icmp::icmp::HeaderOther; -use packet::ip::ipv4::packet::IpV4Packet; - -use crate::channel::context::ChannelContext; -use crate::channel::socket::{LocalInterface, VntSocketTrait}; -use crate::cipher::Cipher; -use crate::handle::CurrentDeviceInfo; -use crate::ip_proxy::ProxyHandler; -use crate::protocol; -use crate::protocol::{NetPacket, MAX_TTL}; -#[derive(Clone)] -pub struct IcmpProxy { - icmp_socket: Arc, - // 对端-> 真实来源 - nat_map: Arc>>, -} - -impl IcmpProxy { - pub async fn new( - context: ChannelContext, - current_device: Arc>, - client_cipher: Cipher, - default_interface: &LocalInterface, - ) -> anyhow::Result { - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - let icmp_socket = socket2::Socket::new( - socket2::Domain::IPV4, - socket2::Type::RAW, - Some(socket2::Protocol::ICMPV4), - ) - .context("new Socket RAW ICMPV4 failed")?; - #[cfg(target_os = "android")] - let icmp_socket = socket2::Socket::new( - socket2::Domain::IPV4, - socket2::Type::DGRAM, - Some(socket2::Protocol::ICMPV4), - ) - .context("new Socket DGRAM ICMPV4 failed")?; - let addr: SocketAddrV4 = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0); - icmp_socket - .bind(&socket2::SockAddr::from(addr)) - .context("bind Socket ICMPV4 failed")?; - icmp_socket.set_nonblocking(true)?; - if let Err(e) = icmp_socket.set_ip_unicast_if(default_interface) { - log::warn!("set_ip_unicast_if {:?}", e) - } - let std_socket: std::net::UdpSocket = icmp_socket.into(); - - let tokio_icmp_socket = UdpSocket::from_std(std_socket.try_clone()?)?; - let nat_map: Arc>> = - Arc::new(Mutex::new(HashMap::with_capacity(16))); - { - let nat_map = nat_map.clone(); - tokio::spawn(async { - if let Err(e) = icmp_proxy( - tokio_icmp_socket, - nat_map, - context, - current_device, - client_cipher, - ) - .await - { - log::warn!("icmp_proxy:{:?}", e); - } - }); - } - Ok(Self { - icmp_socket: Arc::new(std_socket), - nat_map, - }) - } -} - -async fn icmp_proxy( - icmp_socket: UdpSocket, - // 对端-> 真实来源 - nat_map: Arc>>, - context: ChannelContext, - current_device: Arc>, - client_cipher: Cipher, -) -> io::Result<()> { - let mut buf = [0u8; 65535 - 20 - 8]; - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - let start = 12; - #[cfg(target_os = "android")] - let start = 12 + 20; - loop { - let (len, addr) = icmp_socket.recv_from(&mut buf[start..]).await?; - if let IpAddr::V4(peer_ip) = addr.ip() { - #[cfg(target_os = "android")] - { - let buf = &mut buf[12..]; - // ipv4 头部20字节 - buf[0] = 0b0100_0110; - //写入总长度 - buf[2..4].copy_from_slice(&((20 + len) as u16).to_be_bytes()); - - let mut ipv4 = IpV4Packet::unchecked(buf); - ipv4.set_flags(2); - ipv4.set_ttl(1); - ipv4.set_protocol(packet::ip::ipv4::protocol::Protocol::Icmp); - ipv4.set_source_ip(peer_ip); - } - recv_handle( - &mut buf, - start + len, - peer_ip, - &nat_map, - &context, - ¤t_device, - &client_cipher, - ); - } - } -} - -fn recv_handle( - buf: &mut [u8], - data_len: usize, - peer_ip: Ipv4Addr, - nat_map: &Mutex>, - context: &ChannelContext, - current_device: &AtomicCell, - client_cipher: &Cipher, -) { - match IpV4Packet::new(&mut buf[12..data_len]) { - Ok(mut ipv4_packet) => match icmp::IcmpPacket::new(ipv4_packet.payload()) { - Ok(icmp_packet) => match icmp_packet.header_other() { - HeaderOther::Identifier(id, seq) => { - if let Some(dest_ip) = nat_map.lock().get(&(peer_ip, id, seq)).cloned() { - ipv4_packet.set_destination_ip(dest_ip); - ipv4_packet.update_checksum(); - - let current_device = current_device.load(); - let virtual_ip = current_device.virtual_ip(); - - let mut net_packet = NetPacket::new0(data_len, buf).unwrap(); - net_packet.set_default_version(); - net_packet.set_protocol(protocol::Protocol::IpTurn); - net_packet.set_transport_protocol( - protocol::ip_turn_packet::Protocol::Ipv4.into(), - ); - net_packet.first_set_ttl(MAX_TTL); - net_packet.set_source(virtual_ip); - net_packet.set_destination(dest_ip); - if let Err(e) = client_cipher.encrypt_ipv4(&mut net_packet) { - log::warn!("加密失败:{}", e); - return; - } - if let Err(e) = context.send_ipv4_by_id( - &net_packet, - &dest_ip, - current_device.connect_server, - current_device.status.online(), - ) { - log::warn!("发送到目标失败:{}", e); - } - } - } - h => { - log::debug!("不支持的icmp代理 {:?},{:?}", peer_ip, h) - } - }, - Err(e) => { - log::warn!("icmp {:?}", e) - } - }, - Err(e) => { - log::warn!("icmp {:?}", e) - } - } -} - -/// icmp用Identifier来区分,没有Identifier的一律不转发 -impl ProxyHandler for IcmpProxy { - fn recv_handle( - &self, - ipv4: &mut IpV4Packet<&mut [u8]>, - source: Ipv4Addr, - destination: Ipv4Addr, - ) -> io::Result { - if ipv4.offset() != 0 || ipv4.flags() & 1 == 1 { - // ip分片的直接丢弃 - return Ok(true); - } - let dest_ip = ipv4.destination_ip(); - //转发到代理目标地址 - let icmp_packet = icmp::IcmpPacket::new(ipv4.payload())?; - match icmp_packet.header_other() { - HeaderOther::Identifier(id, seq) => { - self.nat_map.lock().insert((dest_ip, id, seq), source); - self.icmp_socket.send_to( - ipv4.payload(), - SocketAddr::from(SocketAddrV4::new(dest_ip, 0)), - )?; - } - header_other => { - log::warn!( - "不支持的ip代理Icmp协议:{}->{}->{},{:?}", - source, - destination, - dest_ip, - header_other - ); - } - } - Ok(true) - } - - fn send_handle(&self, _ipv4: &mut IpV4Packet<&mut [u8]>) -> io::Result<()> { - Ok(()) - } -} diff --git a/vnt/src/ip_proxy/mod.rs b/vnt/src/ip_proxy/mod.rs deleted file mode 100644 index 6f16916..0000000 --- a/vnt/src/ip_proxy/mod.rs +++ /dev/null @@ -1,126 +0,0 @@ -use std::net::Ipv4Addr; -use std::sync::Arc; -use std::{io, thread}; - -use crossbeam_utils::atomic::AtomicCell; - -use packet::ip::ipv4; -use packet::ip::ipv4::packet::IpV4Packet; - -use crate::channel::context::ChannelContext; -use crate::cipher::Cipher; -use crate::handle::CurrentDeviceInfo; -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] -use crate::ip_proxy::icmp_proxy::IcmpProxy; -use crate::ip_proxy::tcp_proxy::TcpProxy; -use crate::ip_proxy::udp_proxy::UdpProxy; -use crate::util::StopManager; - -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] -pub mod icmp_proxy; -pub mod tcp_proxy; -pub mod udp_proxy; - -pub trait ProxyHandler { - fn recv_handle( - &self, - ipv4: &mut IpV4Packet<&mut [u8]>, - source: Ipv4Addr, - destination: Ipv4Addr, - ) -> io::Result; - fn send_handle(&self, ipv4: &mut IpV4Packet<&mut [u8]>) -> io::Result<()>; -} - -#[derive(Clone)] -pub struct IpProxyMap { - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - icmp_proxy: IcmpProxy, - tcp_proxy: TcpProxy, - udp_proxy: UdpProxy, -} - -pub fn init_proxy( - context: ChannelContext, - stop_manager: StopManager, - current_device: Arc>, - client_cipher: Cipher, -) -> anyhow::Result { - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .thread_name("ipProxy") - .build()?; - let proxy_map = runtime.block_on(init_proxy0(context, current_device, client_cipher))?; - let (sender, receiver) = tokio::sync::oneshot::channel::<()>(); - let worker = stop_manager.add_listener("ipProxy".into(), move || { - let _ = sender.send(()); - })?; - thread::Builder::new() - .name("ipProxy".into()) - .spawn(move || { - runtime.block_on(async { - let _ = receiver.await; - }); - runtime.shutdown_background(); - drop(worker); - })?; - - return Ok(proxy_map); -} - -async fn init_proxy0( - context: ChannelContext, - _current_device: Arc>, - _client_cipher: Cipher, -) -> anyhow::Result { - let default_interface = context.default_interface().clone(); - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - let icmp_proxy = - IcmpProxy::new(context, _current_device, _client_cipher, &default_interface).await?; - let tcp_proxy = TcpProxy::new(default_interface.clone()).await?; - let udp_proxy = UdpProxy::new(default_interface.clone()).await?; - - Ok(IpProxyMap { - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - icmp_proxy, - tcp_proxy, - udp_proxy, - }) -} - -impl ProxyHandler for IpProxyMap { - fn recv_handle( - &self, - ipv4: &mut IpV4Packet<&mut [u8]>, - source: Ipv4Addr, - destination: Ipv4Addr, - ) -> io::Result { - match ipv4.protocol() { - ipv4::protocol::Protocol::Tcp => self.tcp_proxy.recv_handle(ipv4, source, destination), - ipv4::protocol::Protocol::Udp => self.udp_proxy.recv_handle(ipv4, source, destination), - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - ipv4::protocol::Protocol::Icmp => { - self.icmp_proxy.recv_handle(ipv4, source, destination) - } - _ => { - log::warn!( - "不支持的ip代理ipv4协议{:?}:{}->{}->{}", - ipv4.protocol(), - source, - destination, - ipv4.destination_ip() - ); - Ok(false) - } - } - } - - fn send_handle(&self, ipv4: &mut IpV4Packet<&mut [u8]>) -> io::Result<()> { - match ipv4.protocol() { - ipv4::protocol::Protocol::Tcp => self.tcp_proxy.send_handle(ipv4), - ipv4::protocol::Protocol::Udp => self.udp_proxy.send_handle(ipv4), - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - ipv4::protocol::Protocol::Icmp => self.icmp_proxy.send_handle(ipv4), - _ => Ok(()), - } - } -} diff --git a/vnt/src/ip_proxy/tcp_proxy.rs b/vnt/src/ip_proxy/tcp_proxy.rs deleted file mode 100644 index 333f8df..0000000 --- a/vnt/src/ip_proxy/tcp_proxy.rs +++ /dev/null @@ -1,160 +0,0 @@ -use anyhow::Context; -use std::net::{Ipv4Addr, SocketAddrV4}; -use std::sync::Arc; -use std::time::Duration; -use std::{collections::HashMap, io, net::SocketAddr}; - -use parking_lot::Mutex; -use tokio::net::{TcpListener, TcpStream}; - -use crate::channel::socket::{create_tcp, LocalInterface}; -use crate::ip_proxy::ProxyHandler; -use packet::ip::ipv4::packet::IpV4Packet; -use packet::tcp::tcp::TcpPacket; - -#[derive(Clone)] -pub struct TcpProxy { - port: u16, - nat_map: Arc>>, -} - -impl TcpProxy { - pub async fn new(default_interface: LocalInterface) -> anyhow::Result { - let nat_map: Arc>> = - Arc::new(Mutex::new(HashMap::with_capacity(16))); - let tcp_listener = TcpListener::bind(format!("0.0.0.0:{}", 0)) - .await - .context("TcpProxy bind failed")?; - let port = tcp_listener.local_addr()?.port(); - { - let nat_map = nat_map.clone(); - tokio::spawn(tcp_proxy(tcp_listener, nat_map, default_interface)); - } - Ok(Self { port, nat_map }) - } -} - -impl ProxyHandler for TcpProxy { - fn recv_handle( - &self, - ipv4: &mut IpV4Packet<&mut [u8]>, - source: Ipv4Addr, - destination: Ipv4Addr, - ) -> io::Result { - let dest_ip = ipv4.destination_ip(); - //转发到代理目标地址 - let mut tcp_packet = TcpPacket::new(source, destination, ipv4.payload_mut())?; - let source_port = tcp_packet.source_port(); - let dest_port = tcp_packet.destination_port(); - tcp_packet.set_destination_port(self.port); - tcp_packet.update_checksum(); - ipv4.set_destination_ip(destination); - ipv4.update_checksum(); - let key = SocketAddrV4::new(source, source_port); - self.nat_map - .lock() - .insert(key, SocketAddrV4::new(dest_ip, dest_port)); - Ok(false) - } - - fn send_handle(&self, ipv4: &mut IpV4Packet<&mut [u8]>) -> io::Result<()> { - let src_ip = ipv4.source_ip(); - let dest_ip = ipv4.destination_ip(); - let dest_addr = { - let tcp_packet = TcpPacket::new(src_ip, dest_ip, ipv4.payload_mut())?; - SocketAddrV4::new(dest_ip, tcp_packet.destination_port()) - }; - if let Some(source_addr) = self.nat_map.lock().get(&dest_addr) { - let source_ip = *source_addr.ip(); - let mut tcp_packet = TcpPacket::new(source_ip, dest_ip, ipv4.payload_mut())?; - tcp_packet.set_source_port(source_addr.port()); - tcp_packet.update_checksum(); - ipv4.set_source_ip(source_ip); - ipv4.update_checksum(); - } - Ok(()) - } -} - -async fn tcp_proxy( - tcp_listener: TcpListener, - nat_map: Arc>>, - default_interface: LocalInterface, -) { - loop { - match tcp_listener.accept().await { - Ok((tcp_stream, sender_addr)) => match sender_addr { - SocketAddr::V4(sender_addr) => { - if let Some(dest_addr) = nat_map.lock().get(&sender_addr).cloned() { - let default_interface = default_interface.clone(); - tokio::spawn(async move { - let peer_tcp_stream = match tcp_connect( - sender_addr.port(), - dest_addr.into(), - &default_interface, - ) - .await - { - Ok(peer_tcp_stream) => peer_tcp_stream, - Err(e) => { - log::warn!( - "tcp代理异常:{:?},来源:{},目标:{}", - e, - sender_addr, - dest_addr - ); - return; - } - }; - proxy(sender_addr, dest_addr, tcp_stream, peer_tcp_stream).await - }); - } else { - log::warn!("tcp代理异常: 来源:{},未找到目标", sender_addr); - } - } - SocketAddr::V6(_) => {} - }, - Err(e) => { - log::warn!("tcp代理监听:{:?}", e); - } - } - } -} -/// 优先使用来源端口建立tcp连接 -async fn tcp_connect( - src_port: u16, - addr: SocketAddr, - default_interface: &LocalInterface, -) -> anyhow::Result { - let socket = create_tcp(true, default_interface)?; - if socket - .bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, src_port).into()) - .is_err() - { - socket.bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0).into())?; - } - let _ = socket.set_nodelay(true); - let tcp_stream = tokio::time::timeout(Duration::from_secs(5), socket.connect(addr)) - .await - .with_context(|| format!("TCP connection timeout {}", addr))? - .with_context(|| format!("TCP connection target failed {}", addr))?; - Ok(tcp_stream) -} - -async fn proxy( - sender_addr: SocketAddrV4, - dest_addr: SocketAddrV4, - client: TcpStream, - server: TcpStream, -) { - let (mut client_read, mut client_write) = client.into_split(); - let (mut server_read, mut server_write) = server.into_split(); - tokio::spawn(async move { - if let Err(e) = tokio::io::copy(&mut client_read, &mut server_write).await { - log::warn!("client tcp proxy {}->{},{:?}", sender_addr, dest_addr, e); - } - }); - if let Err(e) = tokio::io::copy(&mut server_read, &mut client_write).await { - log::warn!("server tcp proxy {}->{},{:?}", sender_addr, dest_addr, e); - } -} diff --git a/vnt/src/ip_proxy/udp_proxy.rs b/vnt/src/ip_proxy/udp_proxy.rs deleted file mode 100644 index 591330a..0000000 --- a/vnt/src/ip_proxy/udp_proxy.rs +++ /dev/null @@ -1,192 +0,0 @@ -use anyhow::Context; -use crossbeam_utils::atomic::AtomicCell; -use std::net::{Ipv4Addr, SocketAddrV4}; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use std::{collections::HashMap, io, net::SocketAddr}; - -use parking_lot::Mutex; -use tokio::net::UdpSocket; - -use crate::channel::socket::{bind_udp, LocalInterface}; -use crate::ip_proxy::ProxyHandler; -use packet::ip::ipv4::packet::IpV4Packet; -use packet::udp::udp::UdpPacket; - -#[derive(Clone)] -pub struct UdpProxy { - port: u16, - nat_map: Arc>>, -} - -impl UdpProxy { - pub async fn new(default_interface: LocalInterface) -> anyhow::Result { - let nat_map: Arc>> = - Arc::new(Mutex::new(HashMap::with_capacity(16))); - let udp = UdpSocket::bind(format!("0.0.0.0:{}", 0)) - .await - .context("UdpProxy bind failed")?; - let port = udp.local_addr()?.port(); - { - let nat_map = nat_map.clone(); - tokio::spawn(async move { - if let Err(e) = udp_proxy(udp, nat_map, default_interface).await { - log::warn!("udp_proxy:{:?}", e); - } - }); - } - Ok(Self { port, nat_map }) - } -} - -impl ProxyHandler for UdpProxy { - fn recv_handle( - &self, - ipv4: &mut IpV4Packet<&mut [u8]>, - source: Ipv4Addr, - destination: Ipv4Addr, - ) -> io::Result { - let dest_ip = ipv4.destination_ip(); - //转发到代理目标地址 - let mut udp_packet = UdpPacket::new(source, destination, ipv4.payload_mut())?; - let source_port = udp_packet.source_port(); - let dest_port = udp_packet.destination_port(); - udp_packet.set_destination_port(self.port); - udp_packet.update_checksum(); - ipv4.set_destination_ip(destination); - ipv4.update_checksum(); - let key = SocketAddrV4::new(source, source_port); - self.nat_map - .lock() - .insert(key.into(), SocketAddrV4::new(dest_ip, dest_port).into()); - Ok(false) - } - - fn send_handle(&self, ipv4: &mut IpV4Packet<&mut [u8]>) -> io::Result<()> { - let src_ip = ipv4.source_ip(); - let dest_ip = ipv4.destination_ip(); - let dest_addr = { - let udp_packet = UdpPacket::new(src_ip, dest_ip, ipv4.payload_mut())?; - SocketAddrV4::new(dest_ip, udp_packet.destination_port()) - }; - if let Some(source_addr) = self.nat_map.lock().get(&dest_addr) { - let source_ip = *source_addr.ip(); - let mut udp_packet = UdpPacket::new(source_ip, dest_ip, ipv4.payload_mut())?; - udp_packet.set_source_port(source_addr.port()); - udp_packet.update_checksum(); - ipv4.set_source_ip(source_ip); - ipv4.update_checksum(); - } - Ok(()) - } -} - -async fn udp_proxy( - udp: UdpSocket, - nat_map: Arc>>, - default_interface: LocalInterface, -) -> anyhow::Result<()> { - let mut buf = [0u8; 65536]; - - let inner_map: Arc, Arc>)>>> = - Arc::new(Mutex::new(HashMap::with_capacity(64))); - let udp_socket = Arc::new(udp); - loop { - match udp_socket.recv_from(&mut buf).await { - Ok((len, sender_addr)) => match sender_addr { - SocketAddr::V4(sender_addr) => { - if let Err(e) = udp_proxy0( - &buf[..len], - sender_addr, - &inner_map, - &nat_map, - &udp_socket, - &default_interface, - ) - .await - { - log::warn!("udp proxy {} {:?}", sender_addr, e); - } - } - SocketAddr::V6(_) => {} - }, - Err(e) => { - log::warn!("udp代理异常:{:?}", e); - } - }; - } -} - -async fn udp_proxy0( - buf: &[u8], - sender_addr: SocketAddrV4, - inner_map: &Arc, Arc>)>>>, - map: &Arc>>, - udp_socket: &Arc, - default_interface: &LocalInterface, -) -> anyhow::Result<()> { - let option = inner_map.lock().get(&sender_addr).cloned(); - if let Some((udp, time)) = option { - time.store(Instant::now()); - udp.send(buf).await?; - } else { - let option = map.lock().get(&sender_addr).cloned(); - if let Some(dest_addr) = option { - //先使用相同的端口,冲突了再随机端口 - let peer_udp_socket = match bind_udp( - format!("0.0.0.0:{}", sender_addr.port()).parse().unwrap(), - default_interface, - ) { - Ok(udp) => udp, - Err(_) => bind_udp("0.0.0.0:0".parse().unwrap(), default_interface)?, - }; - let peer_udp_socket = UdpSocket::from_std(peer_udp_socket.into())?; - peer_udp_socket.connect(dest_addr).await?; - peer_udp_socket.send(buf).await?; - let peer_udp_socket = Arc::new(peer_udp_socket); - let inner_map = inner_map.clone(); - let time = Arc::new(AtomicCell::new(Instant::now())); - inner_map - .lock() - .insert(sender_addr, (peer_udp_socket.clone(), time.clone())); - let udp_socket = udp_socket.clone(); - let map = map.clone(); - tokio::spawn(async move { - let mut buf = [0u8; 65536]; - loop { - match tokio::time::timeout( - Duration::from_secs(600), - peer_udp_socket.recv(&mut buf), - ) - .await - { - Ok(rs) => match rs { - Ok(len) => match udp_socket.send_to(&buf[..len], sender_addr).await { - Ok(_) => {} - Err(e) => { - log::warn!("udp proxy {}->{} {:?}", sender_addr, dest_addr, e); - break; - } - }, - Err(e) => { - log::warn!("udp proxy {}->{} {:?}", sender_addr, dest_addr, e); - - break; - } - }, - Err(_) => { - if time.load().elapsed() > Duration::from_secs(580) { - //超时关闭 - log::warn!("udp proxy timeout {}->{}", sender_addr, dest_addr); - break; - } - } - } - } - inner_map.lock().remove(&sender_addr); - map.lock().remove(&sender_addr); - }); - } - } - Ok(()) -} diff --git a/vnt/src/lib.rs b/vnt/src/lib.rs deleted file mode 100644 index 4de7596..0000000 --- a/vnt/src/lib.rs +++ /dev/null @@ -1,31 +0,0 @@ -pub const VNT_VERSION: &'static str = env!("CARGO_PKG_VERSION"); - -pub mod channel; -pub mod cipher; -pub mod core; -mod external_route; -pub mod handle; -#[cfg(feature = "ip_proxy")] -mod ip_proxy; -pub mod nat; -#[cfg(feature = "port_mapping")] -mod port_mapping; -mod proto; -pub mod protocol; -mod tun_tap_device; -pub use tun_tap_device::*; -pub mod util; - -pub use handle::callback::*; - -pub mod compression; -pub use packet; - -pub(crate) fn ignore_io_interrupted(e: std::io::Error) -> std::io::Result<()> { - if e.kind() == std::io::ErrorKind::Interrupted { - log::warn!("ignore_io_interrupted"); - Ok(()) - } else { - Err(e) - } -} diff --git a/vnt/src/nat/mod.rs b/vnt/src/nat/mod.rs deleted file mode 100644 index 628eddf..0000000 --- a/vnt/src/nat/mod.rs +++ /dev/null @@ -1,354 +0,0 @@ -use anyhow::{anyhow, Context}; -use std::io; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, ToSocketAddrs}; -use std::net::{SocketAddr, UdpSocket}; -use std::sync::Arc; -use std::time::{Duration, Instant}; - -use crossbeam_utils::atomic::AtomicCell; -use parking_lot::Mutex; -use rand::prelude::SliceRandom; -use rand::Rng; - -use crate::channel::punch::{NatInfo, NatType, PunchModel}; -use crate::channel::socket::LocalInterface; -#[cfg(feature = "upnp")] -use crate::util::UPnP; - -mod stun; - -pub fn local_ipv4_() -> io::Result { - let socket = UdpSocket::bind("0.0.0.0:0")?; - socket.connect("8.8.8.8:80")?; - let addr = socket.local_addr()?; - match addr.ip() { - IpAddr::V4(ip) => Ok(ip), - IpAddr::V6(_) => Ok(Ipv4Addr::UNSPECIFIED), - } -} - -pub fn local_ipv4() -> Option { - match local_ipv4_() { - Ok(ipv4) => Some(ipv4), - Err(e) => { - log::warn!("获取ipv4失败:{:?}", e); - None - } - } -} - -pub fn local_ipv6_() -> io::Result { - let socket = UdpSocket::bind("[::]:0")?; - socket.connect("[2001:4860:4860:0000:0000:0000:0000:8888]:80")?; - let addr = socket.local_addr()?; - match addr.ip() { - IpAddr::V4(_) => Ok(Ipv6Addr::UNSPECIFIED), - IpAddr::V6(ip) => Ok(ip), - } -} - -pub fn local_ipv6() -> Option { - match local_ipv6_() { - Ok(ipv6) => { - if is_ipv6_global(&ipv6) { - return Some(ipv6); - } - } - Err(e) => { - log::warn!("获取ipv6失败:{:?}", e); - } - } - None -} - -pub const fn is_ipv4_global(ipv4: &Ipv4Addr) -> bool { - !(ipv4.octets()[0] == 0 // "This network" - || ipv4.is_private() - || ipv4.octets()[0] == 100 && (ipv4.octets()[1] & 0b1100_0000 == 0b0100_0000)//ipv4.is_shared() - || ipv4.is_loopback() - || ipv4.is_link_local() - // addresses reserved for future protocols (`192.0.0.0/24`) - // .9 and .10 are documented as globally reachable so they're excluded - || ( - ipv4.octets()[0] == 192 && ipv4.octets()[1] == 0 && ipv4.octets()[2] == 0 - && ipv4.octets()[3] != 9 && ipv4.octets()[3] != 10 - ) - || ipv4.is_documentation() - || ipv4.octets()[0] == 198 && (ipv4.octets()[1] & 0xfe) == 18//ipv4.is_benchmarking() - || ipv4.octets()[0] & 240 == 240 && !ipv4.is_broadcast()//ipv4.is_reserved() - || ipv4.is_broadcast()) -} - -pub const fn is_ipv6_global(ipv6addr: &Ipv6Addr) -> bool { - !(ipv6addr.is_unspecified() - || ipv6addr.is_loopback() - // IPv4-mapped Address (`::ffff:0:0/96`) - || matches!(ipv6addr.segments(), [0, 0, 0, 0, 0, 0xffff, _, _]) - // IPv4-IPv6 Translat. (`64:ff9b:1::/48`) - || matches!(ipv6addr.segments(), [0x64, 0xff9b, 1, _, _, _, _, _]) - // Discard-Only Address Block (`100::/64`) - || matches!(ipv6addr.segments(), [0x100, 0, 0, 0, _, _, _, _]) - // IETF Protocol Assignments (`2001::/23`) - || (matches!(ipv6addr.segments(), [0x2001, b, _, _, _, _, _, _] if b < 0x200) - && !( - // Port Control Protocol Anycast (`2001:1::1`) - u128::from_be_bytes(ipv6addr.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0001 - // Traversal Using Relays around NAT Anycast (`2001:1::2`) - || u128::from_be_bytes(ipv6addr.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0002 - // AMT (`2001:3::/32`) - || matches!(ipv6addr.segments(), [0x2001, 3, _, _, _, _, _, _]) - // AS112-v6 (`2001:4:112::/48`) - || matches!(ipv6addr.segments(), [0x2001, 4, 0x112, _, _, _, _, _]) - // ORCHIDv2 (`2001:20::/28`) - || matches!(ipv6addr.segments(), [0x2001, b, _, _, _, _, _, _] if b >= 0x20 && b <= 0x2F) - )) - || (ipv6addr.segments()[0] == 0x2001) && (ipv6addr.segments()[1] == 0xdb8)//ipv6addr.is_documentation() - || (ipv6addr.segments()[0] & 0xfe00) == 0xfc00//ipv6addr.is_unique_local() - || (ipv6addr.segments()[0] & 0xffc0) == 0xfe80) //ipv6addr.is_unicast_link_local()) -} - -#[derive(Clone)] -pub struct NatTest { - stun_server: Vec, - info: Arc>, - time: Arc>, - udp_ports: Vec, - tcp_port: u16, - #[cfg(feature = "upnp")] - upnp: UPnP, - pub(crate) update_local_ipv4: bool, -} - -impl NatTest { - pub fn new( - _channel_num: usize, - stun_server: Vec, - local_ipv4: Option, - ipv6: Option, - udp_ports: Vec, - tcp_port: u16, - update_local_ipv4: bool, - punch_model: PunchModel, - ) -> NatTest { - let ports = vec![0; udp_ports.len()]; - let nat_info = NatInfo::new( - Vec::new(), - ports, - 0, - local_ipv4, - ipv6, - udp_ports.clone(), - tcp_port, - 0, - NatType::Cone, - punch_model, - ); - let info = Arc::new(Mutex::new(nat_info)); - #[cfg(feature = "upnp")] - let upnp = UPnP::default(); - #[cfg(feature = "upnp")] - for port in &udp_ports { - upnp.add_udp_port(*port); - } - #[cfg(feature = "upnp")] - upnp.add_tcp_port(tcp_port); - let instant = Instant::now(); - NatTest { - stun_server, - info, - time: Arc::new(AtomicCell::new( - instant - .checked_sub(Duration::from_secs(100)) - .unwrap_or(instant), - )), - udp_ports, - tcp_port, - #[cfg(feature = "upnp")] - upnp, - update_local_ipv4, - } - } - pub fn can_update(&self) -> bool { - let last = self.time.load(); - last.elapsed() > Duration::from_secs(10) - && self.time.compare_exchange(last, Instant::now()).is_ok() - } - - pub fn nat_info(&self) -> NatInfo { - self.info.lock().clone() - } - pub fn is_local_udp(&self, ipv4: Ipv4Addr, port: u16) -> bool { - for x in &self.udp_ports { - if x == &port { - let guard = self.info.lock(); - if let Some(ip) = guard.local_ipv4 { - if ipv4 == ip { - return true; - } - } - break; - } - } - false - } - pub fn is_local_tcp(&self, ipv4: Ipv4Addr, port: u16) -> bool { - if self.tcp_port == port { - let guard = self.info.lock(); - if let Some(ip) = guard.local_ipv4 { - if ipv4 == ip { - return true; - } - } - } - false - } - pub fn is_local_address(&self, is_tcp: bool, addr: SocketAddr) -> bool { - let port = addr.port(); - let check_ip = || { - let guard = self.info.lock(); - match addr.ip() { - IpAddr::V4(ipv4) => { - if let Some(ip) = guard.local_ipv4 { - if ipv4 == ip { - return true; - } - } - } - IpAddr::V6(ipv6) => { - if let Some(ip) = guard.ipv6 { - if ipv6 == ip { - return true; - } - } - } - } - false - }; - if is_tcp { - if self.tcp_port == port { - return check_ip(); - } - } else { - for x in &self.udp_ports { - if x == &port { - return check_ip(); - } - } - } - false - } - pub fn update_addr(&self, index: usize, ip: Ipv4Addr, port: u16) -> bool { - let mut guard = self.info.lock(); - guard.update_addr(index, ip, port) - } - pub fn update_tcp_port(&self, port: u16) { - let mut guard = self.info.lock(); - guard.update_tcp_port(port) - } - pub fn re_test( - &self, - local_ipv4: Option, - ipv6: Option, - default_interface: &LocalInterface, - ) -> anyhow::Result { - let mut stun_server = self.stun_server.clone(); - if stun_server.len() > 5 { - stun_server.shuffle(&mut rand::thread_rng()); - stun_server.truncate(5); - log::info!("stun_server truncate {:?}", stun_server); - } - let (nat_type, public_ips, port_range) = - stun::stun_test_nat(stun_server, default_interface)?; - if public_ips.is_empty() { - Err(anyhow!("public_ips.is_empty"))? - } - let mut guard = self.info.lock(); - guard.nat_type = nat_type; - guard.public_ips = public_ips; - guard.public_port_range = port_range; - if local_ipv4.is_some() { - guard.local_ipv4 = local_ipv4; - } - guard.ipv6 = ipv6; - - Ok(guard.clone()) - } - #[cfg(feature = "upnp")] - pub fn reset_upnp(&self) { - let local_ipv4 = self.info.lock().local_ipv4.clone(); - if let Some(local_ipv4) = local_ipv4 { - self.upnp.reset(local_ipv4) - } - } - pub fn send_data(&self) -> anyhow::Result<(Vec, SocketAddr)> { - let len = self.stun_server.len(); - let stun_server = if len == 1 { - &self.stun_server[0] - } else { - let index = rand::thread_rng().gen_range(0..self.stun_server.len()); - &self.stun_server[index] - }; - let addr = stun_server - .to_socket_addrs()? - .next() - .with_context(|| format!("stun error {:?}", stun_server))?; - Ok((stun::send_stun_request(), addr)) - } - pub fn recv_data( - &self, - index: usize, - source_addr: SocketAddr, - buf: &[u8], - ) -> anyhow::Result { - if buf[0] == 0x01 && buf[1] == 0x01 { - if let Some(addr) = stun::recv_stun_response(buf) { - if let Err(e) = self.recv_data_(index, source_addr, addr) { - log::warn!("{:?}", e); - } - } - Ok(true) - } else { - Ok(false) - } - } - fn recv_data_( - &self, - index: usize, - source_addr: SocketAddr, - addr: SocketAddr, - ) -> anyhow::Result<()> { - if let SocketAddr::V4(addr) = addr { - let mut check_fail = true; - let source_ip = match source_addr.ip() { - IpAddr::V4(ip) => ip, - IpAddr::V6(ip) => { - if let Some(ip) = ip.to_ipv4() { - ip - } else { - return Ok(()); - } - } - }; - 'a: for stun_server in &self.stun_server { - for x in stun_server.to_socket_addrs()? { - if source_addr.port() == x.port() { - if let IpAddr::V4(ip) = x.ip() { - if ip == source_ip { - check_fail = false; - break 'a; - } - }; - } - } - } - if !check_fail { - if is_ipv4_global(addr.ip()) { - if self.update_addr(index, *addr.ip(), addr.port()) { - log::info!("回应地址{:?},来源stun {:?}", addr, source_addr) - } - } - } - } - Ok(()) - } -} diff --git a/vnt/src/nat/stun.rs b/vnt/src/nat/stun.rs deleted file mode 100644 index 6fcdb9b..0000000 --- a/vnt/src/nat/stun.rs +++ /dev/null @@ -1,224 +0,0 @@ -use std::collections::HashSet; -use std::io; -use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; -use std::time::Duration; - -use crate::channel::punch::NatType; -use crate::channel::socket::{bind_udp, LocalInterface}; -use rand::RngCore; -use std::net::UdpSocket; -use stun_format::Attr; - -pub fn stun_test_nat( - stun_servers: Vec, - default_interface: &LocalInterface, -) -> anyhow::Result<(NatType, Vec, u16)> { - let mut nat_type = NatType::Cone; - let mut port_range = 0; - let mut hash_set = HashSet::new(); - for _ in 0..2 { - let stun_servers = stun_servers.clone(); - match stun_test_nat0(stun_servers, default_interface) { - Ok((nat_type_t, ip_list_t, port_range_t)) => { - if nat_type_t == NatType::Symmetric { - nat_type = NatType::Symmetric; - } - for x in ip_list_t { - hash_set.insert(x); - } - if port_range < port_range_t { - port_range = port_range_t; - } - } - Err(e) => { - log::warn!("{:?}", e); - } - } - } - Ok((nat_type, hash_set.into_iter().collect(), port_range)) -} - -pub fn stun_test_nat0( - stun_servers: Vec, - default_interface: &LocalInterface, -) -> anyhow::Result<(NatType, Vec, u16)> { - let udp = bind_udp("0.0.0.0:0".parse().unwrap(), default_interface)?; - udp.set_nonblocking(false)?; - let udp: UdpSocket = udp.into(); - udp.set_read_timeout(Some(Duration::from_millis(500)))?; - let mut nat_type = NatType::Cone; - let mut min_port = u16::MAX; - let mut max_port = 0; - let mut hash_set = HashSet::new(); - let mut pub_addrs = HashSet::new(); - for x in &stun_servers { - match test_nat(&udp, x) { - Ok(addr) => { - pub_addrs.extend(addr); - } - Err(e) => { - log::warn!("stun {} error {:?} ", x, e); - } - } - } - if pub_addrs.len() > 1 { - nat_type = NatType::Symmetric; - } - for addr in &pub_addrs { - if let SocketAddr::V4(addr) = addr { - hash_set.insert(*addr.ip()); - if min_port > addr.port() { - min_port = addr.port() - } - if max_port < addr.port() { - max_port = addr.port() - } - } - } - if hash_set.is_empty() { - Ok((nat_type, vec![], 0)) - } else { - Ok(( - nat_type, - hash_set.into_iter().collect(), - max_port - min_port, - )) - } -} - -fn test_nat(udp: &UdpSocket, stun_server: &String) -> io::Result> { - udp.connect(stun_server)?; - let tid = rand::thread_rng().next_u64() as u128; - let mut addr = HashSet::new(); - let (mapped_addr1, changed_addr1) = test_nat_(&udp, stun_server, true, true, tid)?; - if mapped_addr1.is_ipv4() { - addr.insert(mapped_addr1); - } - if let Some(changed_addr1) = changed_addr1 { - if udp.connect(changed_addr1).is_ok() { - match test_nat_(&udp, stun_server, false, false, tid + 1) { - Ok((mapped_addr2, _)) => { - if mapped_addr2.is_ipv4() { - addr.insert(mapped_addr1); - } - } - Err(e) => { - log::warn!("stun {} error {:?} ", stun_server, e); - } - } - } - } - log::info!( - "stun {} mapped_addr {:?} changed_addr {:?}", - stun_server, - addr, - changed_addr1, - ); - - Ok(addr) -} - -fn test_nat_( - udp: &UdpSocket, - stun_server: &String, - change_ip: bool, - change_port: bool, - tid: u128, -) -> io::Result<(SocketAddr, Option)> { - for _ in 0..2 { - let mut buf = [0u8; 28]; - let mut msg = stun_format::MsgBuilder::from(buf.as_mut_slice()); - msg.typ(stun_format::MsgType::BindingRequest); - msg.tid(tid); - msg.add_attr(Attr::ChangeRequest { - change_ip, - change_port, - }); - udp.send(msg.as_bytes())?; - let mut buf = [0; 10240]; - let (len, _addr) = match udp.recv_from(&mut buf) { - Ok(rs) => rs, - Err(e) => { - log::warn!("stun {} error {:?}", stun_server, e); - continue; - } - }; - let msg = stun_format::Msg::from(&buf[..len]); - let mut mapped_addr = None; - let mut changed_addr = None; - for x in msg.attrs_iter() { - match x { - Attr::MappedAddress(addr) => { - if mapped_addr.is_none() { - let _ = mapped_addr.insert(stun_addr(addr)); - } - } - Attr::ChangedAddress(addr) => { - if changed_addr.is_none() { - let _ = changed_addr.insert(stun_addr(addr)); - } - } - Attr::XorMappedAddress(addr) => { - if mapped_addr.is_none() { - let _ = mapped_addr.insert(stun_addr(addr)); - } - } - _ => {} - } - if changed_addr.is_some() && mapped_addr.is_some() { - return Ok((mapped_addr.unwrap(), changed_addr)); - } - } - if let Some(addr) = mapped_addr { - return Ok((addr, changed_addr)); - } - } - Err(io::Error::new(io::ErrorKind::Other, "stun response err")) -} - -fn stun_addr(addr: stun_format::SocketAddr) -> SocketAddr { - match addr { - stun_format::SocketAddr::V4(ip, port) => { - SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from(ip), port)) - } - stun_format::SocketAddr::V6(ip, port) => { - SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::from(ip), port, 0, 0)) - } - } -} - -const TAG: u128 = 1827549368 << 64; - -pub fn send_stun_request() -> Vec { - let mut buf = [0u8; 28]; - let mut msg = stun_format::MsgBuilder::from(buf.as_mut_slice()); - msg.typ(stun_format::MsgType::BindingRequest); - let id = rand::thread_rng().next_u64() as u128; - msg.tid(id | TAG); - msg.add_attr(Attr::ChangeRequest { - change_ip: false, - change_port: false, - }); - msg.as_bytes().to_vec() -} - -pub fn recv_stun_response(buf: &[u8]) -> Option { - let msg = stun_format::Msg::from(buf); - if let Some(tid) = msg.tid() { - if tid & TAG != TAG { - return None; - } - } - for x in msg.attrs_iter() { - match x { - Attr::MappedAddress(addr) => { - return Some(stun_addr(addr)); - } - Attr::XorMappedAddress(addr) => { - return Some(stun_addr(addr)); - } - _ => {} - } - } - None -} diff --git a/vnt/src/port_mapping/mod.rs b/vnt/src/port_mapping/mod.rs deleted file mode 100644 index dccb8cd..0000000 --- a/vnt/src/port_mapping/mod.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::net::SocketAddr; -use std::str::FromStr; -use std::thread; - -use anyhow::Context; - -use crate::util::StopManager; - -mod tcp_mapping; - -mod udp_mapping; - -pub fn convert(vec: Vec) -> anyhow::Result> { - let mut rs = Vec::with_capacity(vec.len()); - for x in vec { - let string = x.trim().to_lowercase(); - if let Some(udp_mapping) = string.strip_prefix("udp:") { - let mut split = udp_mapping.split("-"); - let bind_addr = split.next().with_context(|| { - format!( - "udp_mapping error {:?},eg: udp:127.0.0.1:80-10.26.0.10:8080", - x - ) - })?; - let bind_addr = SocketAddr::from_str(bind_addr) - .with_context(|| format!("udp_mapping error {}", bind_addr))?; - let dest = split.next().with_context(|| { - format!( - "udp_mapping error {:?},eg: udp:127.0.0.1:80-10.26.0.10:8080", - x - ) - })?; - rs.push((false, bind_addr, dest.to_string())); - continue; - } - if let Some(tcp_mapping) = string.strip_prefix("tcp:") { - let mut split = tcp_mapping.split("-"); - let bind_addr = split.next().with_context(|| { - format!( - "tcp_mapping error {:?},eg: tcp:127.0.0.1:80-10.26.0.10:8080", - x - ) - })?; - let bind_addr = SocketAddr::from_str(bind_addr) - .with_context(|| format!("udp_mapping error {}", bind_addr))?; - let dest = split.next().with_context(|| { - format!( - "tcp_mapping error {:?},eg: tcp:127.0.0.1:80-10.26.0.10:8080", - x - ) - })?; - rs.push((true, bind_addr, dest.to_string())); - continue; - } - Err(anyhow::anyhow!( - "port_mapping error {:?},eg: tcp:127.0.0.1:80-10.26.0.10:8080", - x - ))?; - } - Ok(rs) -} -pub fn start_port_mapping( - stop_manager: StopManager, - vec: Vec<(bool, SocketAddr, String)>, -) -> anyhow::Result<()> { - if vec.is_empty() { - return Ok(()); - } - - let (sender, receiver) = tokio::sync::oneshot::channel::<()>(); - let worker = stop_manager.add_listener("portMapping".into(), move || { - let _ = sender.send(()); - })?; - thread::Builder::new() - .name("portMapping".into()) - .spawn(move || { - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .thread_name("portMapping") - .build().unwrap(); - runtime.block_on(start_port_mapping0(vec)).unwrap(); - runtime.block_on(async { - let _ = receiver.await; - }); - runtime.shutdown_background(); - drop(worker); - })?; - - Ok(()) -} - -async fn start_port_mapping0(vec: Vec<(bool, SocketAddr, String)>) -> anyhow::Result<()> { - for (is_tcp, bind_addr, destination) in vec { - if is_tcp { - tcp_mapping::tcp_mapping(bind_addr, destination).await?; - } else { - udp_mapping::udp_mapping(bind_addr, destination).await?; - } - } - Ok(()) -} diff --git a/vnt/src/port_mapping/tcp_mapping.rs b/vnt/src/port_mapping/tcp_mapping.rs deleted file mode 100644 index 82e780c..0000000 --- a/vnt/src/port_mapping/tcp_mapping.rs +++ /dev/null @@ -1,54 +0,0 @@ -use anyhow::Context; -use std::net::SocketAddr; -use tokio::net::{TcpListener, TcpStream}; - -pub async fn tcp_mapping(bind_addr: SocketAddr, destination: String) -> anyhow::Result<()> { - let tcp_listener = TcpListener::bind(bind_addr) - .await - .with_context(|| format!("TCP binding {:?} failed", bind_addr))?; - tokio::spawn(async move { - if let Err(e) = tcp_mapping_(bind_addr, tcp_listener, destination).await { - log::warn!("tcp_mapping {:?}", e); - } - }); - Ok(()) -} - -async fn tcp_mapping_( - bind_addr: SocketAddr, - tcp_listener: TcpListener, - destination: String, -) -> anyhow::Result<()> { - loop { - let (tcp_stream, _) = tcp_listener.accept().await?; - - let destination = destination.clone(); - tokio::spawn(async move { - if let Err(e) = copy(tcp_stream, &destination).await { - log::warn!("tcp port mapping {}->{} {:?}", bind_addr, destination, e); - } - }); - } -} - -async fn copy(source_tcp: TcpStream, destination: &String) -> anyhow::Result<()> { - // 或许这里也应该绑定最匹配的网卡,不然全局代理会影响映射 - let dest_tcp = TcpStream::connect(destination) - .await - .with_context(|| format!("TCP connection target failed {:?}", destination))?; - let _ = source_tcp.set_nodelay(true); - let _ = dest_tcp.set_nodelay(true); - - let destination = dest_tcp.peer_addr()?; - let (mut client_read, mut client_write) = source_tcp.into_split(); - let (mut server_read, mut server_write) = dest_tcp.into_split(); - tokio::spawn(async move { - if let Err(e) = tokio::io::copy(&mut client_read, &mut server_write).await { - log::warn!("client tcp proxy ->{:},{:?}", destination, e); - } - }); - if let Err(e) = tokio::io::copy(&mut server_read, &mut client_write).await { - log::warn!("server tcp proxy ->{:?},{:?}", destination, e); - } - Ok(()) -} diff --git a/vnt/src/port_mapping/udp_mapping.rs b/vnt/src/port_mapping/udp_mapping.rs deleted file mode 100644 index 56d8893..0000000 --- a/vnt/src/port_mapping/udp_mapping.rs +++ /dev/null @@ -1,107 +0,0 @@ -use anyhow::Context; -use crossbeam_utils::atomic::AtomicCell; -use parking_lot::Mutex; -use std::collections::HashMap; -use std::net::SocketAddr; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::net::UdpSocket; - -pub async fn udp_mapping(bind_addr: SocketAddr, destination: String) -> anyhow::Result<()> { - let udp = UdpSocket::bind(bind_addr) - .await - .with_context(|| format!("port proxy UDP binding {:?} failed", bind_addr))?; - let udp = Arc::new(udp); - - let inner_map: Arc, Arc>)>>> = - Arc::new(Mutex::new(HashMap::with_capacity(64))); - - tokio::spawn(async move { - let mut buf = [0; 65536]; - loop { - match udp.recv_from(&mut buf).await { - Ok((len, src_addr)) => { - if let Err(e) = - udp_mapping0(&buf[..len], src_addr, &inner_map, &udp, &destination).await - { - log::warn!("udp port mapping {}->{} {:?}", src_addr, destination, e); - } - } - Err(e) => { - log::warn!("port proxy UDP {:?}", e); - } - } - } - }); - Ok(()) -} - -async fn udp_mapping0( - buf: &[u8], - src_addr: SocketAddr, - inner_map: &Arc, Arc>)>>>, - udp_socket: &Arc, - destination: &String, -) -> anyhow::Result<()> { - let option = inner_map.lock().get(&src_addr).cloned(); - if let Some((udp, time)) = option { - time.store(Instant::now()); - udp.send(buf).await?; - } else { - let dest_udp = UdpSocket::bind("0.0.0.0:0").await?; - dest_udp.connect(destination).await?; - dest_udp.send(buf).await?; - let destination_addr = dest_udp.peer_addr()?; - let udp_socket = udp_socket.clone(); - let inner_map = inner_map.clone(); - let dest_udp = Arc::new(dest_udp); - let time = Arc::new(AtomicCell::new(Instant::now())); - inner_map - .lock() - .insert(src_addr, (dest_udp.clone(), time.clone())); - tokio::spawn(async move { - let mut buf = [0u8; 65536]; - loop { - match tokio::time::timeout(Duration::from_secs(600), dest_udp.recv(&mut buf)).await - { - Ok(rs) => match rs { - Ok(len) => match udp_socket.send_to(&buf[..len], src_addr).await { - Ok(_) => {} - Err(e) => { - log::warn!( - "udp port mapping {}->{} {:?}", - src_addr, - destination_addr, - e - ); - break; - } - }, - Err(e) => { - log::warn!( - "udp port mapping {}->{} {:?}", - src_addr, - destination_addr, - e - ); - break; - } - }, - Err(_) => { - if time.load().elapsed() > Duration::from_secs(580) { - //超时关闭 - log::warn!( - "udp port mapping timeout {}->{} ", - src_addr, - destination_addr - ); - break; - } - } - } - } - inner_map.lock().remove(&src_addr); - }); - } - Ok(()) -} diff --git a/vnt/src/protocol/body.rs b/vnt/src/protocol/body.rs deleted file mode 100644 index 07d39c0..0000000 --- a/vnt/src/protocol/body.rs +++ /dev/null @@ -1,593 +0,0 @@ -use std::{fmt, io}; - -pub const ENCRYPTION_RESERVED: usize = 16 + 32 + 12; -pub const AES_GCM_ENCRYPTION_RESERVED: usize = 32; -pub const RSA_ENCRYPTION_RESERVED: usize = 32; - -pub const RANDOM_RESERVED: usize = 4; -pub const FINGER_RESERVED: usize = 12; -pub const TAG_RESERVED: usize = 16; - -/* -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -| random(32) | -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -| finger(32) | -| finger(32) | -| finger(32) | -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -*/ -pub trait SecretTail { - fn buffer(&self) -> &[u8]; - fn exist_finger(&self) -> bool; - fn random_buf(&self) -> &[u8] { - let buf = self.buffer(); - let mut end = buf.len(); - if self.exist_finger() { - end -= FINGER_RESERVED; - } - &buf[end - RANDOM_RESERVED..end] - } - fn finger(&self) -> &[u8] { - if self.exist_finger() { - let buf = self.buffer(); - let end = buf.len(); - &buf[end - FINGER_RESERVED..end] - } else { - &[] - } - } -} - -pub trait SecretTailMut: SecretTail { - fn buffer_mut(&mut self) -> &mut [u8]; - fn set_random(&mut self, random: &[u8]) { - let f = self.exist_finger(); - let buf = self.buffer_mut(); - let mut end = buf.len(); - if f { - end -= FINGER_RESERVED; - } - buf[end - RANDOM_RESERVED..end].copy_from_slice(random); - } - fn set_finger(&mut self, finger: &[u8]) -> io::Result<()> { - if self.exist_finger() { - if finger.len() != FINGER_RESERVED { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "finger.len != 12", - )); - } - let buf = self.buffer_mut(); - let end = buf.len(); - buf[end - FINGER_RESERVED..end].copy_from_slice(finger); - Ok(()) - } else { - Err(io::Error::new( - io::ErrorKind::InvalidData, - "not exist finger", - )) - } - } -} - -/* aead加密数据体 - 0 15 31 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | 数据体 | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | tag(32) | - | tag(32) | - | tag(32) | - | tag(32) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | random(32) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | finger(32) | - | finger(32) | - | finger(32) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - 注:finger用于快速校验数据是否被修改,上层可使用token、协议头参与计算finger, - 确保服务端和客户端都能感知修改(服务端不能解密也能校验指纹) -*/ -pub struct AEADSecretBody { - buffer: B, - exist_finger: bool, -} - -impl> AEADSecretBody { - pub fn new(buffer: B, exist_finger: bool) -> io::Result> { - let len = buffer.as_ref().len(); - let min_len = if exist_finger { - TAG_RESERVED + RANDOM_RESERVED + FINGER_RESERVED - } else { - TAG_RESERVED + RANDOM_RESERVED - }; - // 不能大于udp最大载荷长度 - if len < min_len || len > 65535 - 20 - 8 - 12 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("AEADSecretBody length overflow {}", len), - )); - } - Ok(AEADSecretBody { - buffer, - exist_finger, - }) - } - pub fn data(&self) -> &[u8] { - let mut end = self.buffer.as_ref().len() - TAG_RESERVED - RANDOM_RESERVED; - if self.exist_finger { - end -= FINGER_RESERVED; - } - &self.buffer.as_ref()[..end] - } - pub fn tag(&self) -> &[u8] { - let mut end = self.buffer.as_ref().len() - RANDOM_RESERVED; - if self.exist_finger { - end -= FINGER_RESERVED; - } - &self.buffer.as_ref()[end - TAG_RESERVED..end] - } -} - -impl> SecretTail for AEADSecretBody { - #[inline] - fn buffer(&self) -> &[u8] { - self.buffer.as_ref() - } - #[inline] - fn exist_finger(&self) -> bool { - self.exist_finger - } -} - -impl + AsMut<[u8]>> SecretTailMut for AEADSecretBody { - #[inline] - fn buffer_mut(&mut self) -> &mut [u8] { - self.buffer.as_mut() - } -} - -impl + AsMut<[u8]>> AEADSecretBody { - /// 数据部分 - pub fn data_mut(&mut self) -> &mut [u8] { - let mut end = self.buffer.as_ref().len() - RANDOM_RESERVED - TAG_RESERVED; - if self.exist_finger { - end -= FINGER_RESERVED; - } - &mut self.buffer.as_mut()[..end] - } - /// 数据和tag部分 - pub fn data_tag_mut(&mut self) -> &mut [u8] { - let mut end = self.buffer.as_ref().len() - RANDOM_RESERVED; - if self.exist_finger { - end -= FINGER_RESERVED; - } - &mut self.buffer.as_mut()[..end] - } - pub fn set_tag(&mut self, tag: &[u8]) -> io::Result<()> { - if tag.len() != 16 { - return Err(io::Error::new(io::ErrorKind::InvalidData, "tag.len != 16")); - } - let mut end = self.buffer.as_ref().len() - RANDOM_RESERVED; - if self.exist_finger { - end -= FINGER_RESERVED; - } - self.buffer.as_mut()[end - TAG_RESERVED..end].copy_from_slice(tag); - Ok(()) - } -} - -/* 带随机数的加密数据体 - 0 15 31 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | 数据体 | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | random(32) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | finger(32) | - | finger(32) | - | finger(32) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - 注:finger用于快速校验数据是否被修改,上层可使用token、协议头参与计算finger, - 确保服务端和客户端都能感知修改(服务端不能解密也能校验指纹) -*/ -pub struct IVSecretBody { - buffer: B, - exist_finger: bool, -} - -impl> IVSecretBody { - pub fn new(buffer: B, exist_finger: bool) -> io::Result> { - let len = buffer.as_ref().len(); - let min_len = if exist_finger { - FINGER_RESERVED + RANDOM_RESERVED - } else { - RANDOM_RESERVED - }; - // 不能大于udp最大载荷长度 - if len < min_len || len > 65535 - 20 - 8 - 12 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("IVSecretBody length overflow {}", len), - )); - } - Ok(IVSecretBody { - buffer, - exist_finger, - }) - } - pub fn data(&self) -> &[u8] { - let mut end = self.buffer.as_ref().len() - RANDOM_RESERVED; - if self.exist_finger { - end -= FINGER_RESERVED; - } - &self.buffer.as_ref()[..end] - } -} - -impl + AsMut<[u8]>> IVSecretBody { - pub fn data_mut(&mut self) -> &mut [u8] { - let mut end = self.buffer.as_ref().len() - RANDOM_RESERVED; - if self.exist_finger { - end -= FINGER_RESERVED; - } - &mut self.buffer.as_mut()[..end] - } -} - -impl> SecretTail for IVSecretBody { - #[inline] - fn buffer(&self) -> &[u8] { - self.buffer.as_ref() - } - #[inline] - fn exist_finger(&self) -> bool { - self.exist_finger - } -} - -impl + AsMut<[u8]>> SecretTailMut for IVSecretBody { - #[inline] - fn buffer_mut(&mut self) -> &mut [u8] { - self.buffer.as_mut() - } -} - -/* aes_gcm加密数据体 - 0 15 31 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | 数据体 | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | random(32) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | tag(32) | - | tag(32) | - | tag(32) | - | tag(32) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | finger(32) | - | finger(32) | - | finger(32) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - 注:finger用于快速校验数据是否被修改,上层可使用token、协议头参与计算finger, - 确保服务端和客户端都能感知修改(服务端不能解密也能校验指纹) -*/ -pub struct SecretBody { - buffer: B, - exist_finger: bool, -} - -impl> SecretBody { - pub fn new(buffer: B, exist_finger: bool) -> io::Result> { - let len = buffer.as_ref().len(); - let min_len = if exist_finger { 32 } else { 32 - 12 }; - // 不能大于udp最大载荷长度 - if len < min_len || len > 65535 - 20 - 8 - 12 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "SecretBody length overflow", - )); - } - Ok(SecretBody { - buffer, - exist_finger, - }) - } - pub fn random(&self) -> u32 { - let mut end = self.buffer.as_ref().len() - 16; - if self.exist_finger { - end -= 12; - } - u32::from_be_bytes(self.buffer.as_ref()[end - 4..end].try_into().unwrap()) - } - pub fn body(&self) -> &[u8] { - let mut end = self.buffer.as_ref().len() - 16; - if self.exist_finger { - end -= 12; - } - &self.buffer.as_ref()[..end] - } - pub fn tag(&self) -> &[u8] { - let mut end = self.buffer.as_ref().len(); - if self.exist_finger { - end -= 12; - } - &self.buffer.as_ref()[end - 16..end] - } - /// 数据部分+tag部分 - pub fn en_body(&self) -> &[u8] { - let mut end = self.buffer.as_ref().len(); - if self.exist_finger { - end -= 12; - } - &self.buffer.as_ref()[..end] - } - pub fn finger(&self) -> &[u8] { - if self.exist_finger { - let end = self.buffer.as_ref().len(); - &self.buffer.as_ref()[end - 12..end] - } else { - &[] - } - } - pub fn buffer(&self) -> &[u8] { - self.buffer.as_ref() - } -} - -impl + AsMut<[u8]>> SecretBody { - pub fn set_random(&mut self, random: u32) { - let mut end = self.buffer.as_ref().len() - 16; - if self.exist_finger { - end -= 12; - } - self.buffer.as_mut()[end - 4..end].copy_from_slice(&random.to_be_bytes()); - } - - pub fn set_tag(&mut self, tag: &[u8]) -> io::Result<()> { - if tag.len() != 16 { - return Err(io::Error::new(io::ErrorKind::InvalidData, "tag.len != 16")); - } - let mut end = self.buffer.as_ref().len(); - if self.exist_finger { - end -= 12; - } - self.buffer.as_mut()[end - 16..end].copy_from_slice(tag); - Ok(()) - } - pub fn set_finger(&mut self, finger: &[u8]) -> io::Result<()> { - if self.exist_finger { - if finger.len() != 12 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "finger.len != 12", - )); - } - let end = self.buffer.as_ref().len(); - self.buffer.as_mut()[end - 12..end].copy_from_slice(finger); - Ok(()) - } else { - Err(io::Error::new( - io::ErrorKind::InvalidData, - "not exist finger", - )) - } - } - - /// 数据部分 - pub fn body_mut(&mut self) -> &mut [u8] { - let mut end = self.buffer.as_ref().len() - 16; - if self.exist_finger { - end -= 12; - } - &mut self.buffer.as_mut()[..end] - } - pub fn tag_mut(&mut self) -> &mut [u8] { - let mut end = self.buffer.as_ref().len(); - if self.exist_finger { - end -= 12; - } - &mut self.buffer.as_mut()[end - 16..end] - } - /// 数据部分+tag部分 - pub fn en_body_mut(&mut self) -> &mut [u8] { - let mut end = self.buffer.as_ref().len(); - if self.exist_finger { - end -= 12; - } - &mut self.buffer.as_mut()[..end] - } - pub fn buffer_mut(&mut self) -> &mut [u8] { - self.buffer.as_mut() - } -} - -impl> fmt::Debug for SecretBody { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("SecretBody") - .field("random", &self.random()) - .field("body", &self.body()) - .field("tag", &self.tag()) - .field("finger", &self.finger()) - .finish() - } -} -/* aes_cbc加密数据体 - 0 15 31 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | 数据体 | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | random(32) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | finger(32) | - | finger(32) | - | finger(32) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - 注:finger用于快速校验数据是否被修改,上层可使用token、协议头参与计算finger, - 确保服务端和客户端都能感知修改(服务端不能解密也能校验指纹) -*/ -pub struct AesCbcSecretBody { - buffer: B, - exist_finger: bool, -} - -impl> AesCbcSecretBody { - pub fn new(buffer: B, exist_finger: bool) -> io::Result> { - let len = buffer.as_ref().len(); - let min_len = if exist_finger { 16 } else { 16 - 12 }; - // 不能大于udp最大载荷长度 - if len < min_len || len > 65535 - 20 - 8 - 12 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "AesCbcSecretBody length overflow", - )); - } - Ok(AesCbcSecretBody { - buffer, - exist_finger, - }) - } - pub fn en_body(&self) -> &[u8] { - let mut end = self.buffer.as_ref().len(); - if self.exist_finger { - end -= 12; - } - &self.buffer.as_ref()[..end] - } - pub fn finger(&self) -> &[u8] { - if self.exist_finger { - let end = self.buffer.as_ref().len(); - &self.buffer.as_ref()[end - 12..end] - } else { - &[] - } - } -} - -impl + AsMut<[u8]>> AesCbcSecretBody { - pub fn set_random(&mut self, random: u32) { - let mut end = self.buffer.as_ref().len(); - if self.exist_finger { - end -= 12; - } - self.buffer.as_mut()[end - 4..end].copy_from_slice(&random.to_be_bytes()); - } - pub fn set_finger(&mut self, finger: &[u8]) -> io::Result<()> { - if self.exist_finger { - if finger.len() != 12 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "finger.len != 12", - )); - } - let end = self.buffer.as_ref().len(); - self.buffer.as_mut()[end - 12..end].copy_from_slice(finger); - Ok(()) - } else { - Err(io::Error::new( - io::ErrorKind::InvalidData, - "cbc not exist finger", - )) - } - } - pub fn en_body_mut(&mut self) -> &mut [u8] { - let mut end = self.buffer.as_ref().len(); - if self.exist_finger { - end -= 12; - } - &mut self.buffer.as_mut()[..end] - } -} - -/* rsa加密数据体 - 0 15 31 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | 数据体(n) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | random(32) | - | random(32) | - | random(32) | - | random(32) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | finger(32) | - | finger(32) | - | finger(32) | - | finger(32) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -*/ -pub struct RsaSecretBody { - buffer: B, -} - -impl> RsaSecretBody { - pub fn new(buffer: B) -> io::Result> { - let len = buffer.as_ref().len(); - // 不能大于udp最大载荷长度 - if len < 32 || len > 65535 - 20 - 8 - 12 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "length overflow", - )); - } - Ok(RsaSecretBody { buffer }) - } - pub fn data(&self) -> &[u8] { - let end = self.buffer.as_ref().len() - 32; - &self.buffer.as_ref()[..end] - } - pub fn random(&self) -> &[u8] { - let end = self.buffer.as_ref().len() - 16; - &self.buffer.as_ref()[end - 16..end] - } - pub fn body(&self) -> &[u8] { - let end = self.buffer.as_ref().len() - 16; - &self.buffer.as_ref()[..end] - } - pub fn finger(&self) -> &[u8] { - let end = self.buffer.as_ref().len() - 16; - &self.buffer.as_ref()[end..] - } - pub fn buffer(&self) -> &[u8] { - &self.buffer.as_ref() - } -} - -impl + AsMut<[u8]>> RsaSecretBody { - pub fn set_random(&mut self, random: &[u8]) -> io::Result<()> { - if random.len() != 16 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "random.len != 16", - )); - } - let end = self.buffer.as_ref().len() - 16; - self.buffer.as_mut()[end - 16..end].copy_from_slice(random); - Ok(()) - } - pub fn random_mut(&mut self) -> &mut [u8] { - let end = self.buffer.as_ref().len() - 16; - &mut self.buffer.as_mut()[end - 16..end] - } - pub fn set_finger(&mut self, finger: &[u8]) -> io::Result<()> { - if finger.len() != 16 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "finger.len != 16", - )); - } - let end = self.buffer.as_ref().len(); - self.buffer.as_mut()[end - 16..end].copy_from_slice(finger); - Ok(()) - } -} diff --git a/vnt/src/protocol/control_packet.rs b/vnt/src/protocol/control_packet.rs deleted file mode 100644 index dbd1bd6..0000000 --- a/vnt/src/protocol/control_packet.rs +++ /dev/null @@ -1,165 +0,0 @@ -use std::net::Ipv4Addr; -use std::{fmt, io}; - -#[derive(Eq, PartialEq, Copy, Clone, Debug)] -pub enum Protocol { - /// ping请求 - /* - 0 15 31 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | time | echo | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - Ping, - /* - 0 15 31 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | time | echo | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - Pong, - /// 打洞请求 - PunchRequest, - /// 打洞响应 - PunchResponse, - ///获取对端看到的地址 - AddrRequest, - AddrResponse, - Unknown(u8), -} - -impl From for Protocol { - fn from(value: u8) -> Self { - match value { - 1 => Protocol::Ping, - 2 => Protocol::Pong, - 3 => Protocol::PunchRequest, - 4 => Protocol::PunchResponse, - 5 => Protocol::AddrRequest, - 6 => Protocol::AddrResponse, - val => Protocol::Unknown(val), - } - } -} - -impl Into for Protocol { - fn into(self) -> u8 { - match self { - Protocol::Ping => 1, - Protocol::Pong => 2, - Protocol::PunchRequest => 3, - Protocol::PunchResponse => 4, - Protocol::AddrRequest => 5, - Protocol::AddrResponse => 6, - Protocol::Unknown(val) => val, - } - } -} - -pub enum ControlPacket { - PingPacket(PingPacket), - PongPacket(PongPacket), - PunchRequest, - PunchResponse, - AddrRequest, - AddrResponse(AddrPacket), -} - -impl> ControlPacket { - pub fn new(protocol: u8, buffer: B) -> io::Result> { - match Protocol::from(protocol) { - Protocol::Ping => Ok(ControlPacket::PingPacket(PingPacket::new(buffer)?)), - Protocol::Pong => Ok(ControlPacket::PongPacket(PongPacket::new(buffer)?)), - Protocol::PunchRequest => Ok(ControlPacket::PunchRequest), - Protocol::PunchResponse => Ok(ControlPacket::PunchResponse), - Protocol::AddrRequest => Ok(ControlPacket::AddrRequest), - Protocol::AddrResponse => Ok(ControlPacket::AddrResponse(AddrPacket::new(buffer)?)), - Protocol::Unknown(_) => Err(io::Error::new(io::ErrorKind::InvalidData, "Unsupported")), - } - } -} - -/// 网络探针 -pub struct PingPacket { - buffer: B, -} - -pub type PongPacket = PingPacket; - -impl> PingPacket { - pub fn new(buffer: B) -> io::Result> { - let len = buffer.as_ref().len(); - if len < 4 { - return Err(io::Error::new(io::ErrorKind::InvalidData, "len < 4")); - } - Ok(PingPacket { buffer }) - } -} - -impl> PingPacket { - pub fn time(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[..2].try_into().unwrap()) - } - pub fn epoch(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[2..4].try_into().unwrap()) - } -} - -impl + AsMut<[u8]>> PingPacket { - pub fn set_time(&mut self, time: u16) { - self.buffer.as_mut()[..2].copy_from_slice(&time.to_be_bytes()) - } - pub fn set_epoch(&mut self, epoch: u16) { - self.buffer.as_mut()[2..4].copy_from_slice(&epoch.to_be_bytes()) - } -} - -impl> fmt::Debug for PingPacket { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("PingPacket") - .field("time", &self.time()) - .field("epoch", &self.epoch()) - .finish() - } -} - -pub struct AddrPacket { - buffer: B, -} - -impl> AddrPacket { - pub fn new(buffer: B) -> io::Result> { - let len = buffer.as_ref().len(); - if len < 6 { - return Err(io::Error::new(io::ErrorKind::InvalidData, "len < 6")); - } - Ok(AddrPacket { buffer }) - } - pub fn ipv4(&self) -> Ipv4Addr { - let buf = self.buffer.as_ref(); - Ipv4Addr::new(buf[0], buf[1], buf[2], buf[3]) - } - pub fn port(&self) -> u16 { - u16::from_be_bytes(self.buffer.as_ref()[4..6].try_into().unwrap()) - } -} - -impl + AsMut<[u8]>> AddrPacket { - pub fn set_ipv4(&mut self, ip: Ipv4Addr) { - self.buffer.as_mut()[..4].copy_from_slice(&ip.octets()) - } - pub fn set_port(&mut self, port: u16) { - self.buffer.as_mut()[4..6].copy_from_slice(&port.to_be_bytes()) - } -} - -impl> fmt::Debug for AddrPacket { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("AddrPacket") - .field("ipv4", &self.ipv4()) - .field("port", &self.port()) - .finish() - } -} diff --git a/vnt/src/protocol/error_packet.rs b/vnt/src/protocol/error_packet.rs deleted file mode 100644 index cea9467..0000000 --- a/vnt/src/protocol/error_packet.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::io; - -#[derive(Eq, PartialEq, Copy, Clone, Debug)] -pub enum Protocol { - TokenError, - Disconnect, - AddressExhausted, - IpAlreadyExists, - InvalidIp, - NoKey, - Other(u8), -} - -impl From for Protocol { - fn from(value: u8) -> Self { - match value { - 1 => Self::TokenError, - 2 => Self::Disconnect, - 3 => Self::AddressExhausted, - 4 => Self::IpAlreadyExists, - 5 => Self::InvalidIp, - 6 => Self::NoKey, - val => Self::Other(val), - } - } -} - -impl Into for Protocol { - fn into(self) -> u8 { - match self { - Protocol::TokenError => 1, - Protocol::Disconnect => 2, - Protocol::AddressExhausted => 3, - Protocol::IpAlreadyExists => 4, - Protocol::InvalidIp => 5, - Protocol::NoKey => 6, - Protocol::Other(val) => val, - } - } -} - -pub enum InErrorPacket { - TokenError, - Disconnect, - AddressExhausted, - IpAlreadyExists, - InvalidIp, - NoKey, - OtherError(ErrorPacket), -} - -impl> InErrorPacket { - pub fn new(protocol: u8, buffer: B) -> io::Result> { - match Protocol::from(protocol) { - Protocol::TokenError => Ok(InErrorPacket::TokenError), - Protocol::Disconnect => Ok(InErrorPacket::Disconnect), - Protocol::AddressExhausted => Ok(InErrorPacket::AddressExhausted), - Protocol::IpAlreadyExists => Ok(InErrorPacket::IpAlreadyExists), - Protocol::InvalidIp => Ok(InErrorPacket::InvalidIp), - Protocol::NoKey => Ok(InErrorPacket::NoKey), - Protocol::Other(_) => Ok(InErrorPacket::OtherError(ErrorPacket::new(buffer)?)), - } - } -} - -pub struct ErrorPacket { - buffer: B, -} - -impl> ErrorPacket { - pub fn new(buffer: B) -> io::Result> { - Ok(Self { buffer }) - } -} - -impl> ErrorPacket { - pub fn message(&self) -> io::Result { - match String::from_utf8(self.buffer.as_ref().to_vec()) { - Ok(str) => Ok(str), - Err(_) => Err(io::Error::new(io::ErrorKind::Other, "Utf8Error")), - } - } -} - -impl + AsMut<[u8]>> ErrorPacket { - pub fn set_message(&mut self, message: &str) { - self.buffer.as_mut().copy_from_slice(message.as_bytes()) - } -} diff --git a/vnt/src/protocol/extension.rs b/vnt/src/protocol/extension.rs deleted file mode 100644 index 1e43e06..0000000 --- a/vnt/src/protocol/extension.rs +++ /dev/null @@ -1,141 +0,0 @@ -/* 扩展协议 - 0 15 31 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | 扩展数据(n) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | 扩展数据(n) | type(8) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 注:扩展数据的长度由type决定 -*/ - -use anyhow::anyhow; -use std::io; - -use crate::protocol::NetPacket; - -#[derive(Eq, PartialEq, Copy, Clone, Debug)] -pub enum ExtensionTailType { - Compression, - Unknown(u8), -} - -impl From for ExtensionTailType { - fn from(value: u8) -> Self { - if value == 0 { - ExtensionTailType::Compression - } else { - ExtensionTailType::Unknown(value) - } - } -} - -pub enum ExtensionTailPacket { - Compression(CompressionExtensionTail), - Unknown, -} - -impl + AsMut<[u8]>> NetPacket { - /// 分离尾部数据 - pub fn split_tail_packet(&mut self) -> anyhow::Result> { - if self.is_extension() { - let payload = self.payload(); - if let Some(v) = payload.last() { - return match ExtensionTailType::from(*v) { - ExtensionTailType::Compression => { - let data_len = self.data_len - 4; - self.set_data_len(data_len)?; - self.set_extension_flag(false); - Ok(ExtensionTailPacket::Compression( - CompressionExtensionTail::new( - &self.raw_buffer()[data_len..data_len + 4], - ), - )) - } - ExtensionTailType::Unknown(e) => Err(anyhow!("unknown extension {}", e)), - }; - } - } - Err(anyhow!("not extension")) - } - /// 追加压缩扩展 - pub fn append_compression_extension_tail( - &mut self, - ) -> io::Result> { - let len = self.data_len; - //增加数据长度 - self.set_data_len(self.data_len + 4)?; - self.set_extension_flag(true); - let mut tail = CompressionExtensionTail::new(&mut self.buffer_mut()[len..]); - tail.init(); - return Ok(tail); - } -} - -/* 扩展协议 - 0 15 31 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | algorithm(8) | | type(8) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 注:扩展数据的长度由type决定 -*/ -/// 压缩扩展 -pub struct CompressionExtensionTail { - buffer: B, -} - -impl> CompressionExtensionTail { - pub fn new(buffer: B) -> CompressionExtensionTail { - assert_eq!(buffer.as_ref().len(), 4); - CompressionExtensionTail { buffer } - } -} - -impl> CompressionExtensionTail { - pub fn algorithm(&self) -> CompressionAlgorithm { - self.buffer.as_ref()[0].into() - } -} - -impl + AsMut<[u8]>> CompressionExtensionTail { - pub fn init(&mut self) { - self.buffer.as_mut().fill(0); - } - pub fn set_algorithm(&mut self, algorithm: CompressionAlgorithm) { - self.buffer.as_mut()[0] = algorithm.into() - } -} - -#[derive(Eq, PartialEq, Copy, Clone, Debug)] -pub enum CompressionAlgorithm { - #[cfg(feature = "lz4_compress")] - Lz4, - #[cfg(feature = "zstd_compress")] - Zstd, - Unknown(u8), -} - -impl From for CompressionAlgorithm { - fn from(value: u8) -> Self { - match value { - #[cfg(feature = "lz4_compress")] - 1 => CompressionAlgorithm::Lz4, - #[cfg(feature = "zstd_compress")] - 2 => CompressionAlgorithm::Zstd, - v => CompressionAlgorithm::Unknown(v), - } - } -} - -impl From for u8 { - fn from(value: CompressionAlgorithm) -> Self { - match value { - #[cfg(feature = "lz4_compress")] - CompressionAlgorithm::Lz4 => 1, - #[cfg(feature = "zstd_compress")] - CompressionAlgorithm::Zstd => 2, - CompressionAlgorithm::Unknown(val) => val, - } - } -} diff --git a/vnt/src/protocol/ip_turn_packet.rs b/vnt/src/protocol/ip_turn_packet.rs deleted file mode 100644 index edccdd5..0000000 --- a/vnt/src/protocol/ip_turn_packet.rs +++ /dev/null @@ -1,120 +0,0 @@ -#![allow(dead_code)] - -use std::io; -use std::net::Ipv4Addr; - -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub enum Protocol { - Ipv4, - WGIpv4, - Ipv4Broadcast, - Unknown(u8), -} - -impl From for Protocol { - fn from(value: u8) -> Self { - match value { - 4 => Protocol::Ipv4, - 5 => Protocol::WGIpv4, - 201 => Protocol::Ipv4Broadcast, - val => Protocol::Unknown(val), - } - } -} - -impl From for u8 { - fn from(val: Protocol) -> Self { - match val { - Protocol::Ipv4 => 4, - Protocol::WGIpv4 => 5, - Protocol::Ipv4Broadcast => 201, - Protocol::Unknown(val) => val, - } - } -} - -pub struct BroadcastPacket { - buffer: B, -} - -impl> BroadcastPacket { - pub fn unchecked(buffer: B) -> Self { - Self { buffer } - } - pub fn new(buffer: B) -> io::Result { - let len = buffer.as_ref().len(); - let packet = Self::unchecked(buffer); - if len < 2 + 4 || packet.addr_num() == 0 { - Err(io::Error::new( - io::ErrorKind::InvalidData, - "BroadcastPacket InvalidData", - )) - } else { - Ok(packet) - } - } -} - -impl> BroadcastPacket { - pub fn addr_num(&self) -> u8 { - self.buffer.as_ref()[0] - } - /// 已经发送给了这些地址 - pub fn addresses(&self) -> Vec { - let num = self.addr_num() as usize; - let mut list = Vec::with_capacity(num); - let buf = self.buffer.as_ref(); - let mut offset = 1; - for _ in 0..num { - list.push(Ipv4Addr::new( - buf[offset], - buf[offset + 1], - buf[offset + 2], - buf[offset + 3], - )); - offset += 4; - } - list - } - pub fn data(&self) -> io::Result<&[u8]> { - let start = 1 + self.addr_num() as usize * 4; - if start > self.buffer.as_ref().len() { - Err(io::Error::new(io::ErrorKind::InvalidData, "InvalidData")) - } else { - Ok(&self.buffer.as_ref()[start..]) - } - } -} - -impl + AsMut<[u8]>> BroadcastPacket { - pub fn set_address(&mut self, addr: &[Ipv4Addr]) -> io::Result<()> { - let buf = self.buffer.as_mut(); - if buf.len() < 1 + addr.len() * 4 || addr.len() > u8::MAX as usize { - Err(io::Error::new( - io::ErrorKind::InvalidData, - "addr invalid data", - )) - } else { - buf[0] = addr.len() as u8; - let mut offset = 1; - for ip in addr { - buf[offset..offset + 4].copy_from_slice(&ip.octets()); - offset += 4; - } - Ok(()) - } - } - pub fn set_data(&mut self, data: &[u8]) -> io::Result<()> { - let num = self.addr_num() as usize; - let start = 1 + 4 * num; - let buf = self.buffer.as_mut(); - if start >= buf.len() || start + data.len() != buf.len() { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "data invalid data", - )); - } - buf[start..].copy_from_slice(data); - Ok(()) - } -} diff --git a/vnt/src/protocol/mod.rs b/vnt/src/protocol/mod.rs deleted file mode 100644 index e718646..0000000 --- a/vnt/src/protocol/mod.rs +++ /dev/null @@ -1,309 +0,0 @@ -use crate::protocol::body::ENCRYPTION_RESERVED; -use std::net::Ipv4Addr; -use std::{fmt, io}; - -/* - 0 15 31 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - |e |s |x |u| 版本(4) | 协议(8) | 上层协议(8) | 初始ttl(4) | 生存时间(4) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | 源ip地址(32) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | 目的ip地址(32) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | 数据体 | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 注:e为是否加密标志,s为服务端通信包标志,x扩展标志,u未使用 -*/ -pub const HEAD_LEN: usize = 12; - -pub mod body; -pub mod control_packet; -pub mod error_packet; -pub mod extension; -pub mod ip_turn_packet; -pub mod other_turn_packet; -pub mod service_packet; - -#[derive(Eq, PartialEq, Copy, Clone, Debug)] -pub enum Version { - V2, - Unknown(u8), -} - -impl From for Version { - fn from(value: u8) -> Self { - match value { - // 版本从2开始,用于和stun协议的binging响应区分开 - 2 => Version::V2, - val => Version::Unknown(val), - } - } -} - -impl Into for Version { - fn into(self) -> u8 { - match self { - Version::V2 => 2, - Version::Unknown(val) => val, - } - } -} - -#[derive(Eq, PartialEq, Copy, Clone, Debug)] -pub enum Protocol { - /// 服务包 用于和服务端交互 - Service, - /// 响应异常 - Error, - /// 控制协议 - Control, - /// 转发ip数据 - IpTurn, - /// 转发其他数据 - OtherTurn, - Unknown(u8), -} - -impl From for Protocol { - fn from(value: u8) -> Self { - match value { - 1 => Protocol::Service, - 2 => Protocol::Error, - 3 => Protocol::Control, - 4 => Protocol::IpTurn, - 5 => Protocol::OtherTurn, - val => Protocol::Unknown(val), - } - } -} - -impl Into for Protocol { - fn into(self) -> u8 { - match self { - Protocol::Service => 1, - Protocol::Error => 2, - Protocol::Control => 3, - Protocol::IpTurn => 4, - Protocol::OtherTurn => 5, - Protocol::Unknown(val) => val, - } - } -} - -pub const MAX_TTL: u8 = 0b1111; -pub const MAX_SOURCE: u8 = 0b11110000; - -#[derive(Copy, Clone)] -pub struct NetPacket { - data_len: usize, - buffer: B, -} - -impl> NetPacket { - pub fn unchecked(buffer: B) -> Self { - let data_len = buffer.as_ref().len(); - Self { data_len, buffer } - } - pub fn new(buffer: B) -> io::Result> { - let data_len = buffer.as_ref().len(); - Self::new0(data_len, buffer) - } - pub fn new_encrypt(buffer: B) -> io::Result> { - if 12 + ENCRYPTION_RESERVED > buffer.as_ref().len() { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "length overflow", - )); - } - //加密需要预留ENCRYPTION_RESERVED字节 - let data_len = buffer.as_ref().len() - ENCRYPTION_RESERVED; - Self::new0(data_len, buffer) - } - pub fn new0(data_len: usize, buffer: B) -> io::Result> { - if data_len > buffer.as_ref().len() { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "length overflow", - )); - } - if data_len < 12 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "data_len too short", - )); - } - Ok(NetPacket { data_len, buffer }) - } - #[inline] - pub fn buffer(&self) -> &[u8] { - &self.buffer.as_ref()[..self.data_len] - } - pub fn raw_buffer(&self) -> &[u8] { - self.buffer.as_ref() - } - pub fn data_len(&self) -> usize { - self.data_len - } - pub fn reserve(&self) -> usize { - self.buffer.as_ref().len() - self.data_len - } - pub fn into_buffer(self) -> B { - self.buffer - } -} - -impl> NetPacket { - /// 数据加密 - pub fn is_encrypt(&self) -> bool { - self.buffer.as_ref()[0] & 0x80 == 0x80 - } - /// 网关通信的标识 - pub fn is_gateway(&self) -> bool { - self.buffer.as_ref()[0] & 0x40 == 0x40 - } - /// 扩展协议 - pub fn is_extension(&self) -> bool { - self.buffer.as_ref()[0] & 0x20 == 0x20 - } - pub fn version(&self) -> Version { - Version::from(self.buffer.as_ref()[0] & 0x0F) - } - pub fn protocol(&self) -> Protocol { - Protocol::from(self.buffer.as_ref()[1]) - } - pub fn transport_protocol(&self) -> u8 { - self.buffer.as_ref()[2] - } - pub fn ttl(&self) -> u8 { - self.buffer.as_ref()[3] & MAX_TTL - } - pub fn source_ttl(&self) -> u8 { - self.buffer.as_ref()[3] >> 4 - } - pub fn source(&self) -> Ipv4Addr { - let tmp: [u8; 4] = self.buffer.as_ref()[4..8].try_into().unwrap(); - Ipv4Addr::from(tmp) - } - pub fn destination(&self) -> Ipv4Addr { - let tmp: [u8; 4] = self.buffer.as_ref()[8..12].try_into().unwrap(); - Ipv4Addr::from(tmp) - } - pub fn payload(&self) -> &[u8] { - &self.buffer.as_ref()[12..self.data_len] - } - pub fn head(&self) -> &[u8] { - &self.buffer.as_ref()[..12] - } -} - -impl + AsMut<[u8]>> NetPacket { - pub fn head_mut(&mut self) -> &mut [u8] { - &mut self.buffer.as_mut()[..12] - } - pub fn buffer_mut(&mut self) -> &mut [u8] { - &mut self.buffer.as_mut()[..self.data_len] - } - pub fn set_encrypt_flag(&mut self, is_encrypt: bool) { - if is_encrypt { - self.buffer.as_mut()[0] = self.buffer.as_ref()[0] | 0x80 - } else { - self.buffer.as_mut()[0] = self.buffer.as_ref()[0] & 0x7F - }; - } - pub fn set_gateway_flag(&mut self, is_gateway: bool) { - if is_gateway { - self.buffer.as_mut()[0] = self.buffer.as_ref()[0] | 0x40 - } else { - self.buffer.as_mut()[0] = self.buffer.as_ref()[0] & 0xBF - }; - } - pub fn set_extension_flag(&mut self, is_extension: bool) { - if is_extension { - self.buffer.as_mut()[0] = self.buffer.as_ref()[0] | 0x20 - } else { - self.buffer.as_mut()[0] = self.buffer.as_ref()[0] & 0xDF - }; - } - pub fn set_default_version(&mut self) { - let v: u8 = Version::V2.into(); - self.buffer.as_mut()[0] = (self.buffer.as_ref()[0] & 0xF0) | (0x0F & v); - } - pub fn set_protocol(&mut self, protocol: Protocol) { - self.buffer.as_mut()[1] = protocol.into(); - } - pub fn set_transport_protocol(&mut self, transport_protocol: u8) { - self.buffer.as_mut()[2] = transport_protocol; - } - pub fn set_transport_protocol_into>(&mut self, transport_protocol: P) { - self.buffer.as_mut()[2] = transport_protocol.into(); - } - pub fn first_set_ttl(&mut self, ttl: u8) { - self.buffer.as_mut()[3] = ttl << 4 | ttl; - } - pub fn set_ttl(&mut self, ttl: u8) { - self.buffer.as_mut()[3] = (self.buffer.as_mut()[3] & MAX_SOURCE) | (MAX_TTL & ttl); - } - pub fn incr_ttl(&mut self) -> u8 { - let ttl = self.ttl() - 1; - self.set_ttl(ttl); - ttl - } - pub fn set_source_ttl(&mut self, source_ttl: u8) { - self.buffer.as_mut()[3] = (source_ttl << 4) | (MAX_TTL & self.buffer.as_ref()[3]); - } - pub fn set_source(&mut self, source: Ipv4Addr) { - self.buffer.as_mut()[4..8].copy_from_slice(&source.octets()); - } - pub fn set_destination(&mut self, destination: Ipv4Addr) { - self.buffer.as_mut()[8..12].copy_from_slice(&destination.octets()); - } - pub fn set_payload(&mut self, payload: &[u8]) -> io::Result<()> { - if self.data_len - 12 != payload.len() { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "data_len - 12 != payload.len", - )); - } - self.buffer.as_mut()[12..self.data_len].copy_from_slice(payload); - Ok(()) - } - pub fn payload_mut(&mut self) -> &mut [u8] { - &mut self.buffer.as_mut()[12..self.data_len] - } - pub fn set_data_len(&mut self, data_len: usize) -> io::Result<()> { - if data_len > self.buffer.as_ref().len() || data_len < 12 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "data_len invalid", - )); - } - self.data_len = data_len; - Ok(()) - } - pub fn set_payload_len(&mut self, payload_len: usize) -> io::Result<()> { - let data_len = HEAD_LEN + payload_len; - self.set_data_len(data_len) - } - pub fn set_data_len_max(&mut self) { - self.data_len = self.buffer.as_ref().len(); - } -} - -impl> fmt::Debug for NetPacket { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("NetPacket") - .field("version", &self.version()) - .field("gateway", &self.is_gateway()) - .field("encrypt", &self.is_encrypt()) - .field("protocol", &self.protocol()) - .field("transport_protocol", &self.transport_protocol()) - .field("ttl", &self.ttl()) - .field("source_ttl", &self.source_ttl()) - .field("source", &self.source()) - .field("destination", &self.destination()) - .field("payload", &self.payload()) - .finish() - } -} diff --git a/vnt/src/protocol/other_turn_packet.rs b/vnt/src/protocol/other_turn_packet.rs deleted file mode 100644 index d224104..0000000 --- a/vnt/src/protocol/other_turn_packet.rs +++ /dev/null @@ -1,23 +0,0 @@ -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub enum Protocol { - Punch, - Unknown(u8), -} - -impl From for Protocol { - fn from(value: u8) -> Self { - match value { - 1 => Protocol::Punch, - val => Protocol::Unknown(val), - } - } -} - -impl Into for Protocol { - fn into(self) -> u8 { - match self { - Protocol::Punch => 1, - Protocol::Unknown(val) => val, - } - } -} diff --git a/vnt/src/protocol/service_packet.rs b/vnt/src/protocol/service_packet.rs deleted file mode 100644 index dd40f6c..0000000 --- a/vnt/src/protocol/service_packet.rs +++ /dev/null @@ -1,53 +0,0 @@ -#[derive(Eq, PartialEq, Copy, Clone, Debug)] -pub enum Protocol { - /// 注册请求 - RegistrationRequest, - /// 注册响应 - RegistrationResponse, - /// 拉取设备列表 - PullDeviceList, - /// 推送设备列表 - PushDeviceList, - /// 和服务端握手 - HandshakeRequest, - HandshakeResponse, - SecretHandshakeRequest, - SecretHandshakeResponse, - /// 客户端上报状态 - ClientStatusInfo, - Unknown(u8), -} - -impl From for Protocol { - fn from(value: u8) -> Self { - match value { - 1 => Self::RegistrationRequest, - 2 => Self::RegistrationResponse, - 3 => Self::PullDeviceList, - 4 => Self::PushDeviceList, - 5 => Self::HandshakeRequest, - 6 => Self::HandshakeResponse, - 7 => Self::SecretHandshakeRequest, - 8 => Self::SecretHandshakeResponse, - 9 => Self::ClientStatusInfo, - val => Self::Unknown(val), - } - } -} - -impl Into for Protocol { - fn into(self) -> u8 { - match self { - Self::RegistrationRequest => 1, - Self::RegistrationResponse => 2, - Self::PullDeviceList => 3, - Self::PushDeviceList => 4, - Self::HandshakeRequest => 5, - Self::HandshakeResponse => 6, - Self::SecretHandshakeRequest => 7, - Self::SecretHandshakeResponse => 8, - Self::ClientStatusInfo => 9, - Self::Unknown(val) => val, - } - } -} diff --git a/vnt/src/tun_tap_device/create_device.rs b/vnt/src/tun_tap_device/create_device.rs deleted file mode 100644 index 9a58d7c..0000000 --- a/vnt/src/tun_tap_device/create_device.rs +++ /dev/null @@ -1,241 +0,0 @@ -use std::io; -use std::net::Ipv4Addr; -use std::sync::Arc; -use tun_rs::SyncDevice; - -use crate::{DeviceConfig, ErrorInfo, ErrorType, VntCallback}; - -#[cfg(any(target_os = "windows", target_os = "linux"))] -const DEFAULT_TUN_NAME: &str = "vnt-tun"; - -pub fn create_device( - config: DeviceConfig, - call: &Call, -) -> Result, ErrorInfo> { - let device = match create_device0(&config) { - Ok(device) => device, - Err(e) => { - return Err(ErrorInfo::new_msg( - ErrorType::FailedToCrateDevice, - format!("create device {:?}", e), - )); - } - }; - #[cfg(windows)] - let index = device.if_index().unwrap(); - #[cfg(unix)] - let index = &device.name().unwrap(); - if let Err(e) = add_route(index, Ipv4Addr::BROADCAST, Ipv4Addr::BROADCAST) { - log::warn!("添加广播路由失败 ={:?}", e); - } - - if let Err(e) = add_route( - index, - Ipv4Addr::from([224, 0, 0, 0]), - Ipv4Addr::from([240, 0, 0, 0]), - ) { - log::warn!("添加组播路由失败 ={:?}", e); - } - - for (dest, mask) in config.external_route { - if let Err(e) = add_route(index, dest, mask) { - log::warn!("添加路由失败,请检查-i参数是否和现有路由冲突 ={:?}", e); - call.error(ErrorInfo::new_msg( - ErrorType::Warn, - format!( - "警告! 添加路由失败,请检查-i参数是否和现有路由冲突 ={:?}", - e - ), - )) - } - } - Ok(device) -} - -fn create_device0(config: &DeviceConfig) -> io::Result> { - let mut tun_builder = tun_rs::DeviceBuilder::default(); - tun_builder = tun_builder.ipv4(config.virtual_ip, config.virtual_netmask, None); - - match &config.device_name { - None => { - #[cfg(any(target_os = "windows", target_os = "linux"))] - { - tun_builder = tun_builder.name(DEFAULT_TUN_NAME); - } - } - Some(name) => { - tun_builder = tun_builder.name(name); - } - } - - #[cfg(target_os = "windows")] - { - let name = config - .device_name - .clone() - .unwrap_or_else(|| DEFAULT_TUN_NAME.to_string()); - _ = delete_adapter_info_from_reg(&name); - tun_builder = tun_builder.metric(0).ring_capacity(4 * 1024 * 1024); - } - - #[cfg(target_os = "linux")] - { - let device_name = config - .device_name - .clone() - .unwrap_or(DEFAULT_TUN_NAME.to_string()); - if &device_name == DEFAULT_TUN_NAME { - delete_device(DEFAULT_TUN_NAME); - } - } - - let device = tun_builder.mtu(config.mtu as u16).build_sync()?; - Ok(Arc::new(device)) -} - -#[cfg(target_os = "linux")] -fn delete_device(name: &str) { - // 删除默认网卡,此操作有风险,后续可能去除 - use std::process::Command; - let cmd = format!("ip link delete {}", name); - let delete_tun = Command::new("sh") - .arg("-c") - .arg(&cmd) - .output() - .expect("sh exec error!"); - if !delete_tun.status.success() { - log::warn!("删除网卡失败:{:?}", delete_tun); - } -} -#[cfg(windows)] -fn delete_adapter_info_from_reg(dev_name: &str) -> std::io::Result<()> { - use std::collections::HashSet; - use winreg::{enums::HKEY_LOCAL_MACHINE, enums::KEY_ALL_ACCESS, RegKey}; - let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); - let profiles_key = hklm.open_subkey_with_flags( - "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Profiles", - KEY_ALL_ACCESS, - )?; - let mut profile_guid_set = HashSet::new(); - for sub_key_name in profiles_key.enum_keys().filter_map(Result::ok) { - let sub_key = profiles_key.open_subkey(&sub_key_name)?; - match sub_key.get_value::("Description") { - Ok(profile_name) => { - if dev_name == profile_name { - match profiles_key.delete_subkey_all(&sub_key_name) { - Ok(_) => { - log::info!("deleted Profiles sub_key: {}", sub_key_name); - profile_guid_set.insert(sub_key_name); - } - Err(e) => { - log::warn!("Failed to delete Profiles sub_key {}: {}", sub_key_name, e) - } - } - } - } - Err(e) => log::warn!( - "Failed to read Description for sub_key {}: {}", - sub_key_name, - e - ), - } - } - let unmanaged_key = hklm.open_subkey_with_flags( - "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Signatures\\Unmanaged", - KEY_ALL_ACCESS, - )?; - for sub_key_name in unmanaged_key.enum_keys().filter_map(Result::ok) { - let sub_key = unmanaged_key.open_subkey(&sub_key_name)?; - match sub_key.get_value::("ProfileGuid") { - Ok(profile_guid) => { - if profile_guid_set.contains(&profile_guid) { - match unmanaged_key.delete_subkey_all(&sub_key_name) { - Ok(_) => log::info!("deleted Unmanaged sub_key: {}", sub_key_name), - Err(e) => { - log::warn!("Failed to delete Unmanaged sub_key {}: {}", sub_key_name, e) - } - } - } - } - Err(e) => log::warn!( - "Failed to read Description for sub_key {}: {}", - sub_key_name, - e - ), - } - } - Ok(()) -} - -#[cfg(target_os = "windows")] -pub fn add_route(index: u32, dest: Ipv4Addr, netmask: Ipv4Addr) -> io::Result<()> { - let cmd = format!( - "route add {:?} mask {:?} {:?} metric {} if {}", - dest, - netmask, - Ipv4Addr::UNSPECIFIED, - 1, - index - ); - exe_cmd(&cmd) -} -#[cfg(target_os = "windows")] -pub fn exe_cmd(cmd: &str) -> io::Result<()> { - use std::os::windows::process::CommandExt; - - println!("exe cmd: {}", cmd); - let out = std::process::Command::new("cmd") - .creation_flags(windows_sys::Win32::System::Threading::CREATE_NO_WINDOW) - .arg("/C") - .arg(&cmd) - .output()?; - if !out.status.success() { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("cmd={},out={:?}", cmd, String::from_utf8(out.stderr)), - )); - } - Ok(()) -} - -#[cfg(target_os = "macos")] -pub fn add_route(name: &str, address: Ipv4Addr, netmask: Ipv4Addr) -> io::Result<()> { - let cmd = format!( - "route -n add {} -netmask {} -interface {}", - address, netmask, name - ); - exe_cmd(&cmd)?; - Ok(()) -} -#[cfg(target_os = "linux")] -pub fn add_route(name: &str, address: Ipv4Addr, netmask: Ipv4Addr) -> io::Result<()> { - let cmd = if netmask.is_broadcast() { - format!("route add -host {:?} {}", address, name) - } else { - format!( - "route add -net {}/{} {}", - address, - u32::from(netmask).count_ones(), - name - ) - }; - exe_cmd(&cmd)?; - Ok(()) -} -#[cfg(any(target_os = "macos", target_os = "linux"))] -pub fn exe_cmd(cmd: &str) -> io::Result { - use std::process::Command; - println!("exe cmd: {}", cmd); - let out = Command::new("sh") - .arg("-c") - .arg(cmd) - .output() - .expect("sh exec error!"); - if !out.status.success() { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("cmd={},out={:?}", cmd, out), - )); - } - Ok(out) -} diff --git a/vnt/src/tun_tap_device/mod.rs b/vnt/src/tun_tap_device/mod.rs deleted file mode 100644 index 9519ae4..0000000 --- a/vnt/src/tun_tap_device/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] -#[cfg(feature = "integrated_tun")] -pub use create_device::create_device; - -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] -#[cfg(feature = "integrated_tun")] -mod create_device; -#[cfg(feature = "integrated_tun")] -pub mod tun_create_helper; - -pub mod vnt_device; diff --git a/vnt/src/tun_tap_device/tun_create_helper.rs b/vnt/src/tun_tap_device/tun_create_helper.rs deleted file mode 100644 index 1509248..0000000 --- a/vnt/src/tun_tap_device/tun_create_helper.rs +++ /dev/null @@ -1,141 +0,0 @@ -use std::collections::HashMap; -use std::io; -use std::net::Ipv4Addr; -use std::sync::Arc; - -use crate::channel::context::ChannelContext; -use crate::cipher::Cipher; -use crate::compression::Compressor; -use crate::external_route::ExternalRoute; -use crate::handle::tun_tap::DeviceStop; -use crate::handle::{CurrentDeviceInfo, PeerDeviceInfo}; -#[cfg(feature = "ip_proxy")] -use crate::ip_proxy::IpProxyMap; -use crate::tun_tap_device::vnt_device::DeviceWrite; -use crate::util::StopManager; -use crossbeam_utils::atomic::AtomicCell; -use parking_lot::Mutex; -use tun_rs::SyncDevice; - -#[repr(transparent)] -#[derive(Clone, Default)] -pub struct DeviceAdapter { - tun: Arc>>>, -} - -impl DeviceAdapter { - pub fn insert(&self, device: Arc) { - let r = self.tun.lock().replace(device); - assert!(r.is_none()); - } - /// 要保证先remove 再insert - pub fn remove(&self) { - drop(self.tun.lock().take()); - } -} - -impl DeviceWrite for DeviceAdapter { - #[inline] - fn write(&self, buf: &[u8]) -> io::Result { - if let Some(tun) = self.tun.lock().as_ref() { - tun.send(buf) - } else { - Err(io::Error::new(io::ErrorKind::NotFound, "not tun device")) - } - } - - fn into_device_adapter(self) -> DeviceAdapter { - self - } -} - -#[derive(Clone)] -pub struct TunDeviceHelper { - inner: Arc>, - device_adapter: DeviceAdapter, - device_stop: Arc>>, -} - -#[derive(Clone)] -struct TunDeviceHelperInner { - stop_manager: StopManager, - context: ChannelContext, - current_device: Arc>, - ip_route: ExternalRoute, - #[cfg(feature = "ip_proxy")] - ip_proxy_map: Option, - client_cipher: Cipher, - server_cipher: Cipher, - device_map: Arc)>>, - compressor: Compressor, -} - -impl TunDeviceHelper { - pub fn new( - stop_manager: StopManager, - context: ChannelContext, - current_device: Arc>, - ip_route: ExternalRoute, - #[cfg(feature = "ip_proxy")] ip_proxy_map: Option, - client_cipher: Cipher, - server_cipher: Cipher, - device_map: Arc)>>, - compressor: Compressor, - device_adapter: DeviceAdapter, - ) -> Self { - let inner = TunDeviceHelperInner { - stop_manager, - context, - current_device, - ip_route, - #[cfg(feature = "ip_proxy")] - ip_proxy_map, - client_cipher, - server_cipher, - device_map, - compressor, - }; - Self { - inner: Arc::new(Mutex::new(inner)), - device_adapter, - device_stop: Default::default(), - } - } - pub fn stop(&self) { - //先停止旧的,再启动新的,改变旧网卡的IP太麻烦 - if let Some(device_stop) = self.device_stop.lock().take() { - self.device_adapter.remove(); - loop { - device_stop.stop(); - std::thread::sleep(std::time::Duration::from_millis(300)); - //确保停止了 - if device_stop.is_stopped() { - break; - } - } - } - } - /// 要保证先stop 再start - pub fn start(&self, device: Arc, allow_wire_guard: bool) -> io::Result<()> { - self.device_adapter.insert(device.clone()); - let device_stop = DeviceStop::default(); - let s = self.device_stop.lock().replace(device_stop.clone()); - assert!(s.is_none()); - let inner = self.inner.lock().clone(); - crate::handle::tun_tap::tun_handler::start( - inner.stop_manager, - inner.context, - device, - inner.current_device, - inner.ip_route, - #[cfg(feature = "ip_proxy")] - inner.ip_proxy_map, - inner.client_cipher, - inner.server_cipher, - inner.device_map, - inner.compressor, - device_stop, - allow_wire_guard, - ) - } -} diff --git a/vnt/src/tun_tap_device/vnt_device.rs b/vnt/src/tun_tap_device/vnt_device.rs deleted file mode 100644 index 9c3e8cc..0000000 --- a/vnt/src/tun_tap_device/vnt_device.rs +++ /dev/null @@ -1,7 +0,0 @@ -use std::io; - -pub trait DeviceWrite: Clone + Send + Sync + 'static { - fn write(&self, buf: &[u8]) -> io::Result; - #[cfg(feature = "integrated_tun")] - fn into_device_adapter(self) -> crate::tun_tap_device::tun_create_helper::DeviceAdapter; -} diff --git a/vnt/src/util/counter/adder.rs b/vnt/src/util/counter/adder.rs deleted file mode 100644 index 4888074..0000000 --- a/vnt/src/util/counter/adder.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::sync::Arc; - -use crossbeam_utils::atomic::AtomicCell; - -#[derive(Clone, Default)] -pub struct U64Adder { - count: Arc>, -} - -impl U64Adder { - pub fn add(&self, num: u64) { - self.count.fetch_add(num); - } - pub fn get(&self) -> u64 { - self.count.load() - } - pub fn watch(&self) -> WatchU64Adder { - WatchU64Adder { - count: self.count.clone(), - } - } -} - -#[derive(Clone)] -pub struct WatchU64Adder { - count: Arc>, -} - -impl WatchU64Adder { - pub fn get(&self) -> u64 { - self.count.load() - } -} diff --git a/vnt/src/util/counter/mod.rs b/vnt/src/util/counter/mod.rs deleted file mode 100644 index 0ebd885..0000000 --- a/vnt/src/util/counter/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod adder; -pub use adder::*; diff --git a/vnt/src/util/dns_query.rs b/vnt/src/util/dns_query.rs deleted file mode 100644 index e3067b2..0000000 --- a/vnt/src/util/dns_query.rs +++ /dev/null @@ -1,436 +0,0 @@ -use crate::channel::socket::LocalInterface; -use anyhow::Context; -use dns_parser::{Builder, Packet, QueryClass, QueryType, RData, ResponseCode}; -use http_req::request::{RedirectPolicy, Request}; -use http_req::uri::Uri; -use std::cell::RefCell; -use std::collections::HashMap; -use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs, UdpSocket}; -use std::str::FromStr; -use std::time::Duration; -use std::{io, thread}; - -thread_local! { - static HISTORY: RefCell> = RefCell::new(HashMap::new()); -} - -/// 保留一个地址使用记录,使用过的地址后续不再选中,直到地址全使用过 -pub fn address_choose(addrs: Vec) -> anyhow::Result { - HISTORY.with(|history| { - let mut available = Vec::new(); - for x in &addrs { - let num = history.borrow().get(x).map_or(0, |v| *v); - if num < 3 { - available.push(*x); - } - } - if available.is_empty() { - available = addrs; - history.borrow_mut().clear(); - } - let addr = address_choose0(available)?; - history - .borrow_mut() - .entry(addr) - .and_modify(|v| { - *v += 1; - }) - .or_insert(1); - Ok(addr) - }) -} - -/// 后续实现选择延迟最低的可用地址,需要服务端配合 -/// 现在是选择第一个地址,优先ipv6 -fn address_choose0(addrs: Vec) -> anyhow::Result { - let v4: Vec = addrs.iter().filter(|v| v.is_ipv4()).copied().collect(); - let v6: Vec = addrs.iter().filter(|v| v.is_ipv6()).copied().collect(); - let check_addr = |addrs: &Vec| -> anyhow::Result { - let mut err = Vec::new(); - if !addrs.is_empty() { - let udp = if addrs[0].is_ipv6() { - UdpSocket::bind("[::]:0")? - } else { - UdpSocket::bind("0.0.0.0:0")? - }; - for addr in addrs { - if let Err(e) = udp.connect(addr) { - err.push((*addr, e)); - } else { - return Ok(*addr); - } - } - } - Err(anyhow::anyhow!("Unable to connect to address {:?}", err)) - }; - if v6.is_empty() { - return check_addr(&v4); - } - if v4.is_empty() { - return check_addr(&v6); - } - match check_addr(&v6) { - Ok(addr) => Ok(addr), - Err(e1) => match check_addr(&v4) { - Ok(addr) => Ok(addr), - Err(e2) => Err(anyhow::anyhow!("{} , {}", e1, e2)), - }, - } -} - -pub fn dns_query_all( - domain: &str, - mut name_servers: Vec, - default_interface: &LocalInterface, -) -> anyhow::Result> { - let mut current_domain = domain.to_string(); // 引入可变变量存储当前域名 - match SocketAddr::from_str(¤t_domain) { - Ok(addr) => Ok(vec![addr]), - Err(_) => { - // 重定向判断 http: - let current_domain_lower = current_domain.to_lowercase(); - let redirect_domain = current_domain_lower - .strip_prefix("http:") - .or_else(|| current_domain_lower.strip_prefix("https:")) - .map(|v| v.to_string()); - - // 执行重定向检查 - if let Some(stripped) = redirect_domain { - if let Some(redirected_url) = check_for_redirect(&stripped)? { - // 去掉 URL 开头的协议部分 - let final_domain = remove_http_prefix(&redirected_url); - println!("Server Address: {}", final_domain); - - // 检查是否为 IP 和端口组合 - if let Ok(socket_addr) = SocketAddr::from_str(&final_domain) { - // 如果是 IP 和端口格式,直接返回结果 - return Ok(vec![socket_addr]); - } else { - // 如果不是 IP 和端口格式,则更新为重定向地址 - current_domain = final_domain; - } - } - } - let txt_domain = current_domain - .to_lowercase() - .strip_prefix("txt:") - .map(|v| v.to_string()); - if name_servers.is_empty() { - if txt_domain.is_some() { - name_servers.push("223.5.5.5:53".into()); - name_servers.push("119.29.29.29:53".into()); - name_servers.push("114.114.114.114:53".into()); - } else { - return Ok(current_domain - .to_socket_addrs() - .with_context(|| format!("DNS query failed {:?}", current_domain))? - .collect()); - } - } - - let mut err: Option = None; - for name_server in name_servers { - if let Some(domain) = txt_domain.as_ref() { - match txt_dns(domain, name_server, default_interface) { - Ok(addr) => { - if !addr.is_empty() { - println!("TXT: {:?}", addr); - return Ok(addr); - } - } - Err(e) => { - if let Some(err) = &mut err { - *err = anyhow::anyhow!("{} {}", err, e); - } else { - err.replace(anyhow::anyhow!("{}", e)); - } - } - } - continue; - } - - let end_index = current_domain - .rfind(':') - .with_context(|| format!("{:?} not port", current_domain))?; - let host = &domain[..end_index]; - let port = u16::from_str(&domain[end_index + 1..]) - .with_context(|| format!("{:?} not port", current_domain))?; - let th1 = { - let host = host.to_string(); - let name_server = name_server.clone(); - let default_interface = default_interface.clone(); - thread::spawn(move || a_dns(host, name_server, &default_interface)) - }; - let th2 = { - let host = host.to_string(); - let name_server = name_server.clone(); - let default_interface = default_interface.clone(); - thread::spawn(move || aaaa_dns(host, name_server, &default_interface)) - }; - let mut addr = Vec::new(); - match th1.join().unwrap() { - Ok(rs) => { - for ip in rs { - addr.push(SocketAddr::new(ip.into(), port)); - } - } - Err(e) => { - err.replace(anyhow::anyhow!("{}", e)); - } - } - match th2.join().unwrap() { - Ok(rs) => { - for ip in rs { - addr.push(SocketAddr::new(ip.into(), port)); - } - } - Err(e) => { - if addr.is_empty() { - if let Some(err) = &mut err { - *err = anyhow::anyhow!("{},{}", err, e); - } else { - err.replace(anyhow::anyhow!("{}", e)); - } - continue; - } - } - } - if addr.is_empty() { - continue; - } - return Ok(addr); - } - if let Some(e) = err { - Err(e) - } else { - Err(anyhow::anyhow!("DNS query failed {:?}", current_domain)) - } - } - } -} - -fn parse_host_port(addr: &str) -> bool { - // 处理 IPv6 地址(格式为 [::1]:8080) - if addr.starts_with('[') { - if let Some(idx) = addr.rfind(']') { - if let Some(port_idx) = addr[idx + 1..].find(':') { - let port = &addr[idx + 1 + port_idx + 1..]; // 提取端口部分 - return !port.is_empty() && port.chars().all(|c| c.is_numeric()); - } - } - } else { - // 处理 IPv4 和普通域名(格式为 example.com:443 或 192.168.1.1:8080) - if let Some((_host, port)) = addr.rsplit_once(':') { - return !port.is_empty() && port.chars().all(|c| c.is_numeric()); - } - } - false -} - -fn check_for_redirect(domain: &String) -> anyhow::Result> { - // 确保域名有 http:// 或 https:// 前缀 - let mut url = if domain.starts_with("http://") || domain.starts_with("https://") { - domain.clone() - } else { - format!("http://{}", domain) - }; - - let mut count = 0; // 重定向次数计数器 - let mut last_redirect_url: Option = None; // 记录最后一个重定向的 URL - - loop { - count += 1; - if count > 3 { - println!("重定向次数超过 3 次,跳过"); - return Ok(last_redirect_url); - } - - // 解析 URL - let uri = match Uri::try_from(url.as_str()) { - Ok(u) => u, - Err(e) => { - println!("解析地址失败: {}", e); - return Ok(last_redirect_url); - } - }; - - let mut response_body = Vec::new(); - - // 发送 HTTP 请求 - let response = match Request::new(&uri) - .timeout(Duration::from_secs(10)) - .redirect_policy(RedirectPolicy::Limit(0)) - .send(&mut response_body) - { - Ok(resp) => { - println!("HTTP Status Code: {}", resp.status_code()); - resp - } - Err(e) => { - return Ok(last_redirect_url); - } - }; - - let body_str = String::from_utf8_lossy(&response_body); - let cleaned_body = body_str.replace('\n', "").replace('\r', ""); - println!("Response Body: {}", cleaned_body); - // 处理 3XX 重定向 - if response.status_code().is_redirect() { - if let Some(location) = response.headers().get("Location") { - url = location.to_string().trim_end_matches('/').to_string(); - last_redirect_url = Some(url.clone()); // 更新最后的重定向地址 - println!("Location: {}", url); - continue; - } else { - return Ok(last_redirect_url); - } - } - // 处理 200 响应 - else if response.status_code().is_success() { - for line in body_str.lines() { - let trimmed = line.trim(); - if parse_host_port(trimmed) { - println!("text: {}", trimmed); - return Ok(Some(trimmed.to_string())); - } - } - return Ok(last_redirect_url); - } - return Ok(last_redirect_url); - } -} - -/// 去掉 http:// 或 https:// 前缀 -fn remove_http_prefix(url: &str) -> String { - url.trim_start_matches("http://") - .trim_start_matches("https://") - .to_string() -} - -fn query<'a>( - udp: &UdpSocket, - domain: &str, - name_server: SocketAddr, - record_type: QueryType, - buf: &'a mut [u8], -) -> anyhow::Result> { - let mut builder = Builder::new_query(1, true); - builder.add_question(domain, false, record_type, QueryClass::IN); - let packet = builder.build().unwrap(); - - udp.connect(name_server) - .with_context(|| format!("DNS {:?} error ", name_server))?; - let mut count = 0; - let len = loop { - udp.send(&packet)?; - - match udp.recv(buf) { - Ok(len) => { - break len; - } - Err(e) => { - if e.kind() == io::ErrorKind::TimedOut || e.kind() == io::ErrorKind::WouldBlock { - count += 1; - if count < 3 { - continue; - } - } - Err(e).with_context(|| format!("DNS {:?} recv error ", name_server))? - } - }; - }; - - let pkt = Packet::parse(&buf[..len]) - .with_context(|| format!("domain {:?} DNS {:?} data error ", domain, name_server))?; - if pkt.header.response_code != ResponseCode::NoError { - return Err(anyhow::anyhow!( - "response_code {} DNS {:?} domain {:?}", - pkt.header.response_code, - name_server, - domain - )); - } - if pkt.answers.is_empty() { - return Err(anyhow::anyhow!( - "No records received DNS {:?} domain {:?}", - name_server, - domain - )); - } - - Ok(pkt) -} - -pub fn txt_dns( - domain: &str, - name_server: String, - default_interface: &LocalInterface, -) -> anyhow::Result> { - let name_server: SocketAddr = name_server.parse()?; - let udp = bind_udp(name_server, default_interface)?; - let mut buf = [0; 65536]; - let message = query(&udp, domain, name_server, QueryType::TXT, &mut buf)?; - let mut rs = Vec::new(); - for record in message.answers { - if let RData::TXT(txt) = record.data { - for x in txt.iter() { - let txt = std::str::from_utf8(x).context("record type txt is not string")?; - let addr = - SocketAddr::from_str(txt).context("record type txt is not SocketAddr")?; - rs.push(addr); - } - } - } - Ok(rs) -} - -fn bind_udp( - name_server: SocketAddr, - default_interface: &LocalInterface, -) -> anyhow::Result { - let addr: SocketAddr = if name_server.is_ipv4() { - "0.0.0.0:0".parse().unwrap() - } else { - "[::]:0".parse().unwrap() - }; - let socket = crate::channel::socket::bind_udp(addr, default_interface)?; - socket.set_nonblocking(false)?; - socket.set_read_timeout(Some(Duration::from_millis(800)))?; - Ok(socket.into()) -} - -pub fn a_dns( - domain: String, - name_server: String, - default_interface: &LocalInterface, -) -> anyhow::Result> { - let name_server: SocketAddr = name_server.parse()?; - let udp = bind_udp(name_server, default_interface)?; - let mut buf = [0; 65536]; - let message = query(&udp, &domain, name_server, QueryType::A, &mut buf)?; - let mut rs = Vec::new(); - for record in message.answers { - if let RData::A(a) = record.data { - rs.push(a.0); - } - } - Ok(rs) -} - -pub fn aaaa_dns( - domain: String, - name_server: String, - default_interface: &LocalInterface, -) -> anyhow::Result> { - let name_server: SocketAddr = name_server.parse()?; - let udp = bind_udp(name_server, default_interface)?; - let mut buf = [0; 65536]; - let message = query(&udp, &domain, name_server, QueryType::AAAA, &mut buf)?; - let mut rs = Vec::new(); - for record in message.answers { - if let RData::AAAA(a) = record.data { - rs.push(a.0); - } - } - Ok(rs) -} diff --git a/vnt/src/util/limit/mod.rs b/vnt/src/util/limit/mod.rs deleted file mode 100644 index 0492bd1..0000000 --- a/vnt/src/util/limit/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod rate_limiter; -pub use rate_limiter::*; - -mod traffic_meter; -pub use traffic_meter::*; diff --git a/vnt/src/util/limit/rate_limiter.rs b/vnt/src/util/limit/rate_limiter.rs deleted file mode 100644 index 35f8e20..0000000 --- a/vnt/src/util/limit/rate_limiter.rs +++ /dev/null @@ -1,62 +0,0 @@ -use parking_lot::Mutex; -use std::sync::Arc; -use std::time::Instant; - -#[derive(Clone)] -pub struct ConcurrentRateLimiter { - inner: Arc>, -} - -impl ConcurrentRateLimiter { - pub fn new(capacity: usize, refill_rate: usize) -> Self { - let inner = RateLimiter::new(capacity, refill_rate); - Self { - inner: Arc::new(Mutex::new(inner)), - } - } - pub fn try_acquire(&self) -> bool { - self.inner.lock().try_acquire() - } -} - -pub struct RateLimiter { - capacity: usize, - tokens: usize, - refill_rate: usize, - last_refill: Instant, -} - -impl RateLimiter { - // 初始化限流器 - pub fn new(capacity: usize, refill_rate: usize) -> Self { - Self { - capacity, - tokens: capacity, - refill_rate, - last_refill: Instant::now(), - } - } - - // 尝试获取一个令牌 - pub fn try_acquire(&mut self) -> bool { - self.refill(); - if self.tokens > 0 { - self.tokens -= 1; - true - } else { - false - } - } - - // 补充令牌 - fn refill(&mut self) { - let now = Instant::now(); - let elapsed = now.duration_since(self.last_refill).as_secs() as usize; - let new_tokens = elapsed * self.refill_rate; - - if new_tokens > 0 { - self.tokens = std::cmp::min(self.capacity, self.tokens + new_tokens); - self.last_refill = now; - } - } -} diff --git a/vnt/src/util/limit/traffic_meter.rs b/vnt/src/util/limit/traffic_meter.rs deleted file mode 100644 index 3d4eb50..0000000 --- a/vnt/src/util/limit/traffic_meter.rs +++ /dev/null @@ -1,132 +0,0 @@ -use parking_lot::Mutex; -use std::collections::{HashMap, VecDeque}; -use std::net::Ipv4Addr; -use std::sync::Arc; -use std::time::{Duration, Instant}; - -#[derive(Clone)] -pub struct TrafficMeterMultiAddress { - history_capacity: usize, - inner: Arc)>>, -} - -impl Default for TrafficMeterMultiAddress { - fn default() -> Self { - TrafficMeterMultiAddress::new(100) - } -} - -impl TrafficMeterMultiAddress { - pub fn new(history_capacity: usize) -> Self { - let inner = Arc::new(Mutex::new((0, HashMap::new()))); - Self { - inner, - history_capacity, - } - } - pub fn add_traffic(&self, ip: Ipv4Addr, amount: usize) { - let mut guard = self.inner.lock(); - guard.0 += amount as u64; - guard - .1 - .entry(ip) - .or_insert(TrafficMeter::new(self.history_capacity)) - .add_traffic(amount) - } - pub fn total(&self) -> u64 { - self.inner.lock().0 - } - pub fn get_all(&self) -> (u64, HashMap) { - let guard = self.inner.lock(); - ( - guard.0, - guard.1.iter().map(|(ip, t)| (*ip, t.total())).collect(), - ) - } - pub fn get_all_history(&self) -> (u64, HashMap)>) { - let guard = self.inner.lock(); - ( - guard.0, - guard - .1 - .iter() - .map(|(ip, t)| (*ip, (t.total(), t.get_history()))) - .collect(), - ) - } - pub fn get_history(&self, ip: &Ipv4Addr) -> Option<(u64, Vec)> { - self.inner - .lock() - .1 - .get(ip) - .map(|t| (t.total(), t.get_history())) - } -} - -#[derive(Clone)] -pub struct ConcurrentTrafficMeter { - inner: Arc>, -} - -impl ConcurrentTrafficMeter { - pub fn new(history_capacity: usize) -> Self { - let inner = Arc::new(Mutex::new(TrafficMeter::new(history_capacity))); - Self { inner } - } - pub fn add_traffic(&self, amount: usize) { - self.inner.lock().add_traffic(amount) - } - pub fn get_history(&self) -> Vec { - self.inner.lock().get_history() - } -} - -pub struct TrafficMeter { - start_time: Instant, - total: u64, - count: usize, - history_capacity: usize, - history: VecDeque, -} - -impl TrafficMeter { - // 初始化一个新的 TrafficMeter - pub fn new(history_capacity: usize) -> Self { - Self { - start_time: Instant::now(), - total: 0, - count: 0, - history: VecDeque::with_capacity(history_capacity), - history_capacity, - } - } - - // 增加流量计数 - pub fn add_traffic(&mut self, amount: usize) { - self.total += amount as u64; - self.count += amount; - self.check_time(); - } - - // 检查时间是否超过一秒,如果是,记录流量并重置计数器和时间 - fn check_time(&mut self) { - if self.start_time.elapsed() >= Duration::new(1, 0) { - // 将当前计数添加到历史记录 - if self.history.len() >= self.history_capacity { - self.history.pop_front(); // 保持历史记录不超过capacity - } - self.history.push_back(self.count); - - // 重置计数器和时间 - self.count = 0; - self.start_time = Instant::now(); - } - } - pub fn total(&self) -> u64 { - self.total - } - // 获取流量记录 - pub fn get_history(&self) -> Vec { - self.history.iter().cloned().collect() - } -} diff --git a/vnt/src/util/mod.rs b/vnt/src/util/mod.rs deleted file mode 100644 index fade277..0000000 --- a/vnt/src/util/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -mod notify; -mod scheduler; -pub use notify::{StopManager, Worker}; -pub use scheduler::Scheduler; - -// mod counter; -// pub use counter::*; - -mod dns_query; -pub use dns_query::*; - -#[cfg(feature = "upnp")] -mod upnp; -#[cfg(feature = "upnp")] -pub use upnp::*; - -pub mod limit; diff --git a/vnt/src/util/notify.rs b/vnt/src/util/notify.rs deleted file mode 100644 index 3495f8c..0000000 --- a/vnt/src/util/notify.rs +++ /dev/null @@ -1,174 +0,0 @@ -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use std::sync::Arc; -use std::thread; -use std::thread::Thread; -use std::time::Duration; - -use anyhow::anyhow; -use parking_lot::Mutex; - -#[derive(Clone)] -pub struct StopManager { - inner: Arc, -} - -impl StopManager { - pub fn new(f: F) -> Self - where - F: FnOnce() + Send + 'static, - { - Self { - inner: Arc::new(StopManagerInner::new(f)), - } - } - pub fn add_listener(&self, name: String, f: F) -> anyhow::Result - where - F: FnOnce() + Send + 'static, - { - self.inner.add_listener(name, f) - } - pub fn stop(&self) { - self.inner.stop(); - } - pub fn wait(&self) { - self.inner.wait(); - } - pub fn wait_timeout(&self, dur: Duration) -> bool { - self.inner.wait_timeout(dur) - } - pub fn is_stopped(&self) -> bool { - self.inner.is_stopped() - } -} - -struct StopManagerInner { - listeners: Mutex<(bool, Vec<(String, Box)>)>, - park_threads: Mutex>, - worker_num: AtomicUsize, - state: AtomicBool, - stop_call: Mutex>>, -} - -impl StopManagerInner { - fn new(f: F) -> Self - where - F: FnOnce() + Send + 'static, - { - Self { - listeners: Mutex::new((false, Vec::with_capacity(32))), - park_threads: Mutex::new(Vec::with_capacity(4)), - worker_num: AtomicUsize::new(0), - state: AtomicBool::new(false), - stop_call: Mutex::new(Some(Box::new(f))), - } - } - fn add_listener(self: &Arc, name: String, f: F) -> anyhow::Result - where - F: FnOnce() + Send + 'static, - { - if name.is_empty() { - return Err(anyhow!("name cannot be empty")); - } - let mut guard = self.listeners.lock(); - if guard.0 { - return Err(anyhow!("stopped")); - } - for (n, _) in &guard.1 { - if &name == n { - return Err(anyhow!("stop add_listener {:?} name already exists", name)); - } - } - guard.1.push((name.clone(), Box::new(f))); - Ok(Worker::new(name, self.clone())) - } - fn stop(&self) { - self.state.store(true, Ordering::Release); - let mut guard = self.listeners.lock(); - guard.0 = true; - for (_name, listener) in guard.1.drain(..) { - listener(); - } - } - pub fn is_stopped(&self) -> bool { - self.worker_num.load(Ordering::Acquire) == 0 - } - fn wait(&self) { - { - let mut guard = self.park_threads.lock(); - guard.push(thread::current()); - drop(guard); - } - loop { - if self.worker_num.load(Ordering::Acquire) == 0 { - return; - } - thread::park() - } - } - fn wait_timeout(&self, dur: Duration) -> bool { - { - let mut guard = self.park_threads.lock(); - guard.push(thread::current()); - drop(guard); - } - if self.worker_num.load(Ordering::Acquire) == 0 { - return true; - } - thread::park_timeout(dur); - self.worker_num.load(Ordering::Acquire) == 0 - } - fn stop_call(&self) { - self.stop(); - if let Some(call) = self.stop_call.lock().take() { - call(); - } - } -} - -pub struct Worker { - name: String, - inner: Arc, -} - -impl Worker { - fn new(name: String, inner: Arc) -> Self { - let _ = inner.worker_num.fetch_add(1, Ordering::AcqRel); - Self { name, inner } - } - fn release0(&self) { - let inner = &self.inner; - let worker_name = &self.name; - { - let mut mutex_guard = inner.listeners.lock(); - if let Some(pos) = mutex_guard - .1 - .iter() - .position(|(name, _)| name == worker_name) - { - let (_, listener) = mutex_guard.1.remove(pos); - listener(); - } - } - - let count = inner.worker_num.fetch_sub(1, Ordering::AcqRel); - if count == 1 { - for x in inner.park_threads.lock().drain(..) { - x.unpark(); - } - self.inner.stop_call(); - } - } - pub fn stop_all(self) { - self.inner.stop() - } - pub fn stop_self(self) { - drop(self) - } -} - -impl Drop for Worker { - fn drop(&mut self) { - self.release0(); - log::info!("stop {}", self.name); - } -} diff --git a/vnt/src/util/scheduler.rs b/vnt/src/util/scheduler.rs deleted file mode 100644 index 808cafd..0000000 --- a/vnt/src/util/scheduler.rs +++ /dev/null @@ -1,176 +0,0 @@ -use crate::util::StopManager; -use crossbeam_utils::atomic::AtomicCell; -use std::collections::BinaryHeap; -use std::sync::mpsc::TrySendError; -use std::sync::Arc; -use std::{ - cmp::Ordering, - sync::mpsc::{sync_channel, Receiver, SyncSender}, - time::{Duration, Instant}, -}; - -struct DelayedTask { - f: Box, - next: Instant, -} - -impl Eq for DelayedTask {} - -impl PartialEq for DelayedTask { - fn eq(&self, other: &Self) -> bool { - self.next.eq(&other.next) - } -} - -impl PartialOrd for DelayedTask { - fn partial_cmp(&self, other: &Self) -> Option { - self.next.partial_cmp(&other.next).map(|ord| ord.reverse()) - } -} - -impl Ord for DelayedTask { - fn cmp(&self, other: &Self) -> Ordering { - self.next.cmp(&other.next).reverse() - } -} - -enum Op { - Task(DelayedTask), - Stop, -} - -#[derive(Clone)] -pub struct Scheduler { - sender: SyncSender, - state: Arc>, -} - -#[derive(Copy, Clone, Eq, PartialEq)] -enum SchedulerState { - Running, - ShutdownNow, // 立即停止任务执行,队列中剩余的任务不再执行 - _Shutdown, //执行完队列中剩余的任务再停止 -} - -impl Scheduler { - pub fn new(stop_manager: StopManager) -> anyhow::Result { - let (sender, receiver) = sync_channel::(32); - let state = Arc::new(AtomicCell::new(SchedulerState::Running)); - let s = Self { sender, state }; - let s_inner = s.clone(); - let worker = { - let scheduler = s.clone(); - stop_manager.add_listener("Scheduler".into(), move || { - scheduler.shutdown_now(); - })? - }; - std::thread::Builder::new() - .name("Scheduler".into()) - .spawn(move || { - run(receiver, &s_inner); - s_inner.shutdown_now(); - worker.stop_all(); - }) - .expect("Scheduler"); - Ok(s) - } - pub fn timeout(&self, time: Duration, f: F) -> bool - where - F: FnOnce(&Scheduler) + Send + 'static, - { - if self.state.load() != SchedulerState::Running { - log::error!("定时任务执行停止"); - return false; - } - let task = DelayedTask { - f: Box::new(f), - next: Instant::now().checked_add(time).unwrap(), - }; - // 如果是任务中调用此方法,那这里用send可能会导致整个定时任务阻塞 - // 任务总数不能大于或等于通道长度,所以改成try_send快速失败 - match self.sender.try_send(Op::Task(task)) { - Ok(_) => true, - Err(e) => { - match e { - TrySendError::Full(_) => { - log::error!("定时任务队列达到上限"); - } - TrySendError::Disconnected(_) => { - log::error!("定时任务执行停止 通道关闭"); - } - } - false - } - } - } - pub fn shutdown_now(&self) { - self.state.store(SchedulerState::ShutdownNow); - let _ = self.sender.send(Op::Stop); - } -} - -fn run(receiver: Receiver, s_inner: &Scheduler) { - let mut binary_heap = BinaryHeap::::with_capacity(32); - loop { - while let Some(task) = binary_heap.peek() { - if s_inner.state.load() == SchedulerState::ShutdownNow { - return; - } - let now = Instant::now(); - if now < task.next { - //需要等待对应时间 - match receiver.recv_timeout(task.next - now) { - Ok(op) => { - if add_task(op, &mut binary_heap) { - continue; - } - return; - } - Err(e) => match e { - std::sync::mpsc::RecvTimeoutError::Timeout => continue, - std::sync::mpsc::RecvTimeoutError::Disconnected => return, - }, - } - } else { - if let Some(task) = binary_heap.pop() { - (task.f)(s_inner); - } - } - } - //取出所有任务 - loop { - match receiver.try_recv() { - Ok(op) => { - if add_task(op, &mut binary_heap) { - continue; - } - return; - } - Err(e) => match e { - std::sync::mpsc::TryRecvError::Empty => break, - std::sync::mpsc::TryRecvError::Disconnected => return, - }, - } - } - - if binary_heap.is_empty() { - //任务队列为空时陷入等待 - if let Ok(op) = receiver.recv() { - if add_task(op, &mut binary_heap) { - continue; - } - } - return; - } - } -} - -fn add_task(op: Op, binary_heap: &mut BinaryHeap) -> bool { - return match op { - Op::Task(task) => { - binary_heap.push(task); - true - } - Op::Stop => false, - }; -} diff --git a/vnt/src/util/upnp.rs b/vnt/src/util/upnp.rs deleted file mode 100644 index ae3755d..0000000 --- a/vnt/src/util/upnp.rs +++ /dev/null @@ -1,81 +0,0 @@ -use igd::{search_gateway, PortMappingProtocol}; -use std::net::{Ipv4Addr, SocketAddrV4}; -use std::ops::Deref; -use std::sync::Arc; - -use parking_lot::Mutex; - -#[derive(Clone, Default)] -pub struct UPnP { - inner: Arc, -} - -impl Deref for UPnP { - type Target = UpnpInner; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -#[derive(Default)] -pub struct UpnpInner { - list: Mutex>, -} - -impl UpnpInner { - pub fn add_tcp_port(&self, port: u16) { - self.list.lock().push((PortMappingProtocol::TCP, port)); - } - pub fn add_udp_port(&self, port: u16) { - self.list.lock().push((PortMappingProtocol::UDP, port)); - } - pub fn reset(&self, local_ip: Ipv4Addr) { - let gateway = match search_gateway(Default::default()) { - Ok(gateway) => gateway, - Err(e) => { - log::warn!("search_gateway {:?}", e); - return; - } - }; - let guard = self.list.lock(); - - // 不支持upnp的情况会阻塞30秒,之后再改这个库 - for (protocol, port) in guard.iter() { - let local_addr = SocketAddrV4::new(local_ip, *port); - log::info!("add upnp protocol={} {}", protocol, local_addr); - if let Err(e) = gateway.add_port(*protocol, *port, local_addr, 700, "upnp") { - log::warn!( - "add upnp failed protocol={},port={} err:{:?}", - protocol, - port, - e - ); - } - } - } -} - -impl Drop for UpnpInner { - fn drop(&mut self) { - // let gateway = match search_gateway(Default::default()) { - // Ok(gateway) => gateway, - // Err(e) => { - // log::warn!("search_gateway {:?}", e); - // return; - // } - // }; - // - // let guard = self.list.lock(); - // for (protocol, port) in guard.iter() { - // if let Err(e) = gateway.remove_port(*protocol, *port) { - // log::warn!( - // "remove upnp failed protocol={},port={} err:{:?}", - // protocol, - // port, - // e - // ); - // } - // } - } -}