2021-09-19 00:36:40 -07:00
|
|
|
# Phantun
|
|
|
|
|
|
|
|
A lightweight and fast UDP to TCP obfuscator.
|
|
|
|
|
2022-04-10 18:37:06 +08:00
|
|
|
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/dndx/phantun/Rust?style=flat-square)
|
|
|
|
![docs.rs](https://img.shields.io/docsrs/fake-tcp)
|
|
|
|
|
2021-09-19 00:38:11 -07:00
|
|
|
Table of Contents
|
|
|
|
=================
|
|
|
|
|
|
|
|
* [Phantun](#phantun)
|
2021-09-19 02:59:50 -07:00
|
|
|
* [Latest release](#latest-release)
|
2021-09-19 00:38:11 -07:00
|
|
|
* [Overview](#overview)
|
|
|
|
* [Usage](#usage)
|
2021-09-20 04:15:55 -07:00
|
|
|
* [1. Enable Kernel IP forwarding](#1-enable-kernel-ip-forwarding)
|
|
|
|
* [2. Add required firewall rules](#2-add-required-firewall-rules)
|
2021-09-19 00:38:11 -07:00
|
|
|
* [Client](#client)
|
2021-09-20 04:15:55 -07:00
|
|
|
* [Using nftables](#using-nftables)
|
|
|
|
* [Using iptables](#using-iptables)
|
2021-09-19 00:38:11 -07:00
|
|
|
* [Server](#server)
|
2021-09-20 04:15:55 -07:00
|
|
|
* [Using nftables](#using-nftables)
|
|
|
|
* [Using iptables](#using-iptables)
|
|
|
|
* [3. Run Phantun binaries as non-root (Optional)](#3-run-phantun-binaries-as-non-root-optional)
|
|
|
|
* [4. Start Phantun daemon](#4-start-phantun-daemon)
|
2021-09-19 00:38:11 -07:00
|
|
|
* [Server](#server)
|
|
|
|
* [Client](#client)
|
|
|
|
* [MTU overhead](#mtu-overhead)
|
2021-09-19 03:53:47 -07:00
|
|
|
* [MTU calculation for WireGuard](#mtu-calculation-for-wireguard)
|
2021-09-19 00:38:11 -07:00
|
|
|
* [Version compatibility](#version-compatibility)
|
2022-04-08 21:15:09 -07:00
|
|
|
* [Documentations](#documentations)
|
2021-09-19 01:02:53 -07:00
|
|
|
* [Performance](#performance)
|
|
|
|
* [Future plans](#future-plans)
|
2021-09-19 00:38:11 -07:00
|
|
|
* [Compariation to udp2raw](#compariation-to-udp2raw)
|
|
|
|
* [License](#license)
|
|
|
|
|
2021-09-19 02:59:50 -07:00
|
|
|
# Latest release
|
|
|
|
|
2022-04-10 06:14:30 -07:00
|
|
|
[v0.3.2](https://github.com/dndx/phantun/releases/tag/v0.3.2)
|
2021-09-19 02:59:50 -07:00
|
|
|
|
2021-09-19 00:38:11 -07:00
|
|
|
# Overview
|
2021-09-19 00:36:40 -07:00
|
|
|
|
2021-12-01 15:10:52 +08:00
|
|
|
Phantun is a project that obfuscated UDP packets into TCP connections. It aims to
|
2021-09-19 00:36:40 -07:00
|
|
|
achieve maximum performance with minimum processing and encapsulation overhead.
|
|
|
|
|
|
|
|
It is commonly used in environments where UDP is blocked/throttled but TCP is allowed through.
|
|
|
|
|
2021-12-01 15:10:52 +08:00
|
|
|
Phantun simply converts a stream of UDP packets into obfuscated TCP stream packets. The TCP stack
|
2021-09-19 00:36:40 -07:00
|
|
|
used by Phantun is designed to pass through most L3/L4 stateful/stateless firewalls/NAT
|
|
|
|
devices. It will **not** be able to pass through L7 proxies.
|
|
|
|
However, the advantage of this approach is that none of the common UDP over TCP performance killer
|
|
|
|
such as retransmissions and flow control will occur. The underlying UDP properties such as
|
|
|
|
out-of-order delivery are fully preserved even if the connection ends up looking like a TCP
|
|
|
|
connection from the perspective of firewalls/NAT devices.
|
|
|
|
|
2021-09-19 01:05:08 -07:00
|
|
|
Phantun means Phantom TUN, as it is an obfuscator for UDP traffic that does just enough work
|
|
|
|
to make it pass through stateful firewall/NATs as TCP packets.
|
|
|
|
|
2022-04-10 01:44:33 -07:00
|
|
|
Phantun is written in 100% safe Rust. It has been optimized extensively to scale well on multi-core
|
|
|
|
systems and has no issue saturating all available CPU resources on a fast connection.
|
|
|
|
See the [Performance](#performance) section for benchmarking results.
|
|
|
|
|
2022-04-10 06:14:30 -07:00
|
|
|
![Phantun benchmark results](images/phantun-vs-udp2raw-benchmark-result.png)
|
2021-09-19 00:36:40 -07:00
|
|
|
![Traffic flow diagram](images/traffic-flow.png)
|
|
|
|
|
2021-09-19 00:38:11 -07:00
|
|
|
# Usage
|
2021-09-19 00:36:40 -07:00
|
|
|
|
2021-09-24 08:44:42 -07:00
|
|
|
For the example below, it is assumed that **Phantun Server** listens for incoming Phantun Client connections at
|
|
|
|
port `4567` (the `--local` option for server), and it forwards UDP packets to UDP server at `127.0.0.1:1234`
|
|
|
|
(the `--remote` option for server).
|
|
|
|
|
|
|
|
It is also assumed that **Phantun Client** listens for incoming UDP packets at
|
|
|
|
`127.0.0.1:1234` (the `--local` option for client) and connects to Phantun Server at `10.0.0.1:4567`
|
|
|
|
(the `--remote` option for client).
|
|
|
|
|
2021-09-19 00:36:40 -07:00
|
|
|
Phantun creates TUN interface for both the Client and Server. For Client, Phantun assigns itself the IP address
|
2021-09-28 23:31:07 -07:00
|
|
|
`192.168.200.2` by default and for Server, it assigns `192.168.201.2` by default. Therefore, your Kernel must have
|
2021-09-19 00:36:40 -07:00
|
|
|
`net.ipv4.ip_forward` enabled and setup appropriate iptables rules for NAT between your physical
|
|
|
|
NIC address and Phantun's TUN interface address.
|
|
|
|
|
2021-09-28 23:31:07 -07:00
|
|
|
You may customize the name of Tun interface created by Phantun and the assigned addresses. Please
|
|
|
|
run the executable with `-h` options to see how to change them.
|
|
|
|
|
2022-03-22 05:14:44 -07:00
|
|
|
Another way to help understand this network topology (please see the diagram above for an illustration of this topology):
|
2021-09-24 08:44:42 -07:00
|
|
|
|
|
|
|
Phantun Client is like a machine with private IP address (`192.168.200.2`) behind a router.
|
|
|
|
In order for it to reach the Internet, you will need to SNAT the private IP address before it's traffic
|
|
|
|
leaves the NIC.
|
|
|
|
|
|
|
|
Phantun Server is like a server with private IP address (`192.168.201.2`) behind a router.
|
|
|
|
In order to access it from the Internet, you need to `DNAT` it's listening port on the router
|
|
|
|
and change the destination IP address to where the server is listening for incoming connections.
|
|
|
|
|
|
|
|
In those cases, the machine/iptables running Phantun acts as the "router" that allows Phantun
|
|
|
|
to communicate with outside using it's private IP addresses.
|
|
|
|
|
2021-10-30 09:21:23 -07:00
|
|
|
As of Phantun v0.2.2, IPv6 support for UDP endpoints has been added, however Fake TCP IPv6 support
|
|
|
|
has not been finished yet. To specify an IPv6 address, use the following format: `[::1]:1234` with
|
|
|
|
the command line options.
|
|
|
|
|
2021-09-19 02:59:50 -07:00
|
|
|
[Back to TOC](#table-of-contents)
|
|
|
|
|
2021-09-20 04:15:55 -07:00
|
|
|
## 1. Enable Kernel IP forwarding
|
2021-09-19 00:36:40 -07:00
|
|
|
|
|
|
|
Edit `/etc/sysctl.conf`, add `net.ipv4.ip_forward=1` and run `sudo sysctl -p /etc/sysctl.conf`.
|
|
|
|
|
2021-09-19 00:38:11 -07:00
|
|
|
[Back to TOC](#table-of-contents)
|
2021-09-19 00:36:40 -07:00
|
|
|
|
2021-09-20 04:15:55 -07:00
|
|
|
## 2. Add required firewall rules
|
|
|
|
|
2021-09-19 00:38:11 -07:00
|
|
|
|
|
|
|
### Client
|
2021-09-19 00:36:40 -07:00
|
|
|
|
|
|
|
Client simply need SNAT enabled on the physical interface to translate Phantun's address into
|
|
|
|
one that can be used on the physical network. This can be done simply with masquerade.
|
|
|
|
|
|
|
|
Note: change `eth0` to whatever actual physical interface name is
|
|
|
|
|
2021-09-20 04:15:55 -07:00
|
|
|
[Back to TOC](#table-of-contents)
|
|
|
|
|
|
|
|
#### Using nftables
|
|
|
|
|
2021-09-19 00:36:40 -07:00
|
|
|
```
|
|
|
|
table inet nat {
|
|
|
|
chain postrouting {
|
|
|
|
type nat hook postrouting priority srcnat; policy accept;
|
|
|
|
iifname tun0 oif eth0 masquerade
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2021-09-19 00:38:11 -07:00
|
|
|
[Back to TOC](#table-of-contents)
|
|
|
|
|
2021-09-20 04:15:55 -07:00
|
|
|
#### Using iptables
|
|
|
|
|
|
|
|
```
|
2021-09-22 22:23:00 -07:00
|
|
|
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
2021-09-20 04:15:55 -07:00
|
|
|
```
|
|
|
|
|
|
|
|
[Back to TOC](#table-of-contents)
|
|
|
|
|
2021-09-19 00:38:11 -07:00
|
|
|
### Server
|
2021-09-19 00:36:40 -07:00
|
|
|
|
|
|
|
Server needs to DNAT the TCP listening port to Phantun's TUN interface address.
|
|
|
|
|
|
|
|
Note: change `eth0` to whatever actual physical interface name is and `4567` to
|
2021-12-01 15:10:52 +08:00
|
|
|
actual TCP port number used by Phantun server
|
2021-09-19 00:36:40 -07:00
|
|
|
|
2021-09-20 04:15:55 -07:00
|
|
|
[Back to TOC](#table-of-contents)
|
|
|
|
|
|
|
|
#### Using nftables
|
|
|
|
|
2021-09-19 00:36:40 -07:00
|
|
|
```
|
|
|
|
table ip nat {
|
|
|
|
chain prerouting {
|
|
|
|
type nat hook prerouting priority dstnat; policy accept;
|
|
|
|
iif eth0 tcp dport 4567 dnat to 192.168.201.2
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2021-09-19 00:38:11 -07:00
|
|
|
[Back to TOC](#table-of-contents)
|
|
|
|
|
2021-09-20 04:15:55 -07:00
|
|
|
#### Using iptables
|
|
|
|
|
|
|
|
```
|
|
|
|
iptables -t nat -A PREROUTING -p tcp -i eth0 --dport 4567 -j DNAT --to-destination 192.168.201.2
|
|
|
|
```
|
|
|
|
|
|
|
|
[Back to TOC](#table-of-contents)
|
|
|
|
|
|
|
|
## 3. Run Phantun binaries as non-root (Optional)
|
2021-09-19 00:36:40 -07:00
|
|
|
|
|
|
|
It is ill-advised to run network facing applications as root user. Phantun can be run fully
|
|
|
|
as non-root user with the `cap_net_admin` capability.
|
|
|
|
|
|
|
|
```
|
|
|
|
sudo setcap cap_net_admin=+pe phantun_server
|
|
|
|
sudo setcap cap_net_admin=+pe phantun_client
|
|
|
|
```
|
|
|
|
|
|
|
|
|
2021-09-19 00:38:11 -07:00
|
|
|
[Back to TOC](#table-of-contents)
|
2021-09-19 00:36:40 -07:00
|
|
|
|
2021-09-20 04:15:55 -07:00
|
|
|
## 4. Start Phantun daemon
|
2021-09-19 00:38:11 -07:00
|
|
|
|
2021-09-28 23:31:07 -07:00
|
|
|
**Note:** Run Phantun executable with `-h` option to see full detailed options.
|
|
|
|
|
2022-04-08 21:15:09 -07:00
|
|
|
[Back to TOC](#table-of-contents)
|
|
|
|
|
2021-09-19 00:38:11 -07:00
|
|
|
### Server
|
2021-09-19 00:36:40 -07:00
|
|
|
|
|
|
|
Note: `4567` is the TCP port Phantun should listen on and must corresponds to the DNAT
|
|
|
|
rule specified above. `127.0.0.1:1234` is the UDP Server to connect to for new connections.
|
|
|
|
|
|
|
|
```
|
|
|
|
RUST_LOG=info /usr/local/bin/phantun_server --local 4567 --remote 127.0.0.1:1234
|
|
|
|
```
|
|
|
|
|
2021-11-18 20:48:53 -08:00
|
|
|
Or use host name with `--remote`:
|
|
|
|
|
|
|
|
```
|
|
|
|
RUST_LOG=info /usr/local/bin/phantun_server --local 4567 --remote example.com:1234
|
|
|
|
```
|
|
|
|
|
2021-09-19 00:38:11 -07:00
|
|
|
[Back to TOC](#table-of-contents)
|
|
|
|
|
|
|
|
### Client
|
2021-09-19 00:36:40 -07:00
|
|
|
|
|
|
|
Note: `127.0.0.1:1234` is the UDP address and port Phantun should listen on. `10.0.0.1:4567` is
|
|
|
|
the Phantun Server to connect.
|
|
|
|
|
|
|
|
```
|
|
|
|
RUST_LOG=info /usr/local/bin/phantun_client --local 127.0.0.1:1234 --remote 10.0.0.1:4567
|
|
|
|
```
|
|
|
|
|
2021-11-18 20:48:53 -08:00
|
|
|
Or use host name with `--remote`:
|
|
|
|
|
|
|
|
```
|
|
|
|
RUST_LOG=info /usr/local/bin/phantun_client --local 127.0.0.1:1234 --remote example.com:4567
|
|
|
|
```
|
|
|
|
|
2021-09-19 00:38:11 -07:00
|
|
|
[Back to TOC](#table-of-contents)
|
|
|
|
|
|
|
|
# MTU overhead
|
2021-09-19 00:36:40 -07:00
|
|
|
|
|
|
|
Phantun aims to keep tunneling overhead to the minimum. The overhead compared to a plain UDP packet
|
|
|
|
is the following:
|
|
|
|
|
2022-04-10 18:46:46 +08:00
|
|
|
**Standard UDP packet:** `20 byte IP header + 8 byte UDP header = 28 bytes`
|
2021-09-19 03:58:35 -07:00
|
|
|
|
2022-04-10 18:46:46 +08:00
|
|
|
**Obfuscated packet:** `20 byte IP header + 20 byte TCP header = 40 bytes`
|
2021-09-19 00:36:40 -07:00
|
|
|
|
2021-09-19 03:58:35 -07:00
|
|
|
|
|
|
|
Note that Phantun does not add any additional header other than IP and TCP headers in order to pass through
|
|
|
|
stateful packet inspection!
|
|
|
|
|
2022-04-10 18:46:46 +08:00
|
|
|
Phantun's additional overhead: `12 bytes`. I other words, when using Phantun, the usable payload for
|
2021-09-19 00:36:40 -07:00
|
|
|
UDP packet is reduced by 12 bytes. This is the minimum overhead possible when doing such kind
|
|
|
|
of obfuscation.
|
|
|
|
|
2022-04-10 03:25:32 -07:00
|
|
|
![Packet header diagram](images/packet-headers.png)
|
|
|
|
|
2021-09-19 00:38:11 -07:00
|
|
|
[Back to TOC](#table-of-contents)
|
|
|
|
|
2021-09-19 03:53:47 -07:00
|
|
|
## MTU calculation for WireGuard
|
2021-09-19 02:59:50 -07:00
|
|
|
|
2021-09-19 03:53:47 -07:00
|
|
|
For people who use Phantun to tunnel [WireGuard®](https://www.wireguard.com) UDP packets, here are some guidelines on figuring
|
2021-09-19 02:59:50 -07:00
|
|
|
out the correct MTU to use for your WireGuard interface.
|
|
|
|
|
2022-04-10 18:46:46 +08:00
|
|
|
```
|
2021-10-16 10:55:01 -07:00
|
|
|
WireGuard MTU = Interface MTU - IP header (20 bytes) - TCP header (20 bytes) - WireGuard overhead (32 bytes)
|
2022-04-10 18:46:46 +08:00
|
|
|
```
|
2021-09-19 02:59:50 -07:00
|
|
|
|
|
|
|
For example, for a Ethernet interface with 1500 bytes MTU, the WireGuard interface MTU should be set as:
|
|
|
|
|
2022-04-10 18:46:46 +08:00
|
|
|
```
|
2021-10-16 10:55:01 -07:00
|
|
|
1500 - 20 - 20 - 32 = 1428 bytes
|
2022-04-10 18:46:46 +08:00
|
|
|
```
|
2021-09-19 02:59:50 -07:00
|
|
|
|
2021-10-16 10:55:01 -07:00
|
|
|
The resulted Phantun TCP data packet will be 1500 bytes which does not exceed the
|
2022-04-10 18:46:46 +08:00
|
|
|
interface MTU of 1500. Please note it is strongly recommended to use the same interface
|
|
|
|
MTU for both ends of a WireGuard tunnel, or unexected packet loss may occur and these issues are
|
|
|
|
generally very hard to troubleshoot.
|
2021-09-19 02:59:50 -07:00
|
|
|
|
|
|
|
[Back to TOC](#table-of-contents)
|
|
|
|
|
2021-09-19 00:38:11 -07:00
|
|
|
# Version compatibility
|
2021-09-19 00:36:40 -07:00
|
|
|
|
|
|
|
While the TCP stack is fairly stable, the general expectation is that you should run same minor versions
|
|
|
|
of Server/Client of Phantun on both ends to ensure maximum compatibility.
|
|
|
|
|
2021-09-19 00:38:11 -07:00
|
|
|
[Back to TOC](#table-of-contents)
|
|
|
|
|
2022-04-08 21:15:09 -07:00
|
|
|
# Documentations
|
|
|
|
|
|
|
|
For users who wish to use `fake-tcp` library inside their own project, refer to the documentations for the library at:
|
|
|
|
[https://docs.rs/fake-tcp](https://docs.rs/fake-tcp).
|
|
|
|
|
|
|
|
[Back to TOC](#table-of-contents)
|
|
|
|
|
2021-09-19 01:02:53 -07:00
|
|
|
# Performance
|
|
|
|
|
2022-04-10 01:35:07 -07:00
|
|
|
Performance was tested on 2 AWS `t4g.xlarge` instances with 4 vCPUs and 5 Gb/s NIC over LAN. `nftables` was used to redirect
|
|
|
|
UDP stream of `iperf3` to go through the Phantun/udp2raw tunnel between two test instances and MTU has been tuned to avoid fragmentation.
|
2021-09-19 01:02:53 -07:00
|
|
|
|
2022-04-10 06:14:30 -07:00
|
|
|
Phantun `v0.3.2` and `udp2raw_arm_asm_aes` `20200818.0` was used. These were the latest release of both projects as of Apr 2022.
|
|
|
|
|
2022-04-10 01:35:07 -07:00
|
|
|
Test command: `iperf3 -c <IP> -p <PORT> -R -u -l 1400 -b 1000m -t 30 -P 5`
|
|
|
|
|
2022-04-10 06:14:30 -07:00
|
|
|
| Mode | Send Speed | Receive Speed | Overall CPU Usage |
|
|
|
|
|---------------------------------------------------------------------------------|----------------|----------------|-----------------------------------------------------|
|
|
|
|
| Direct (1 stream) | 3.00 Gbits/sec | 2.37 Gbits/sec | 25% (1 core at 100%) |
|
|
|
|
| Phantun (1 stream) | 1.30 Gbits/sec | 1.20 Gbits/sec | 60% (1 core at 100%, 3 cores at 50%) |
|
|
|
|
| udp2raw (`cipher-mode=none` `auth-mode=none` `disable-anti-replay`) (1 stream) | 1.30 Gbits/sec | 715 Mbits/sec | 40% (1 core at 100%, 1 core at 50%, 2 cores idling) |
|
|
|
|
| Direct connection (5 streams) | 5.00 Gbits/sec | 3.64 Gbits/sec | 25% (1 core at 100%) |
|
|
|
|
| Phantun (5 streams) | 5.00 Gbits/sec | 2.38 Gbits/sec | 95% (all cores utilized) |
|
|
|
|
| udp2raw (`cipher-mode=none` `auth-mode=none` `disable-anti-replay`) (5 streams) | 5.00 Gbits/sec | 770 Mbits/sec | 50% (2 cores at 100%) |
|
2021-09-19 01:02:53 -07:00
|
|
|
|
|
|
|
[Back to TOC](#table-of-contents)
|
|
|
|
|
|
|
|
# Future plans
|
|
|
|
|
2021-10-30 09:21:23 -07:00
|
|
|
* IPv6 support for fake-tcp
|
2021-09-19 01:02:53 -07:00
|
|
|
* Load balancing a single UDP stream into multiple TCP streams
|
2021-09-20 04:09:12 -07:00
|
|
|
* Integration tests
|
2021-09-19 03:09:16 -07:00
|
|
|
* Auto insertion/removal of required firewall rules
|
2021-09-19 01:02:53 -07:00
|
|
|
|
|
|
|
[Back to TOC](#table-of-contents)
|
|
|
|
|
2021-09-19 00:38:11 -07:00
|
|
|
# Compariation to udp2raw
|
2021-09-19 00:36:40 -07:00
|
|
|
[udp2raw](https://github.com/wangyu-/udp2raw-tunnel) is another popular project by [@wangyu-](https://github.com/wangyu-)
|
2021-09-19 01:02:53 -07:00
|
|
|
that is very similar to what Phantun can do. In fact I took inspirations of Phantun from udp2raw. The biggest reason for
|
2021-12-01 15:10:52 +08:00
|
|
|
developing Phantun is because of lack of performance when running udp2raw (especially on multi-core systems such as Raspberry Pi).
|
2021-09-19 00:36:40 -07:00
|
|
|
However, the goal is never to be as feature complete as udp2raw and only support the most common use cases. Most notably, UDP over ICMP
|
|
|
|
and UDP over UDP mode are not supported and there is no anti-replay nor encryption support. The benefit of this is much better
|
|
|
|
performance overall and less MTU overhead because lack of additional headers inside the TCP payload.
|
|
|
|
|
|
|
|
Here is a quick overview of comparison between those two to help you choose:
|
|
|
|
|
|
|
|
| | Phantun | udp2raw |
|
|
|
|
|--------------------------------------------------|:-------------:|:-----------------:|
|
|
|
|
| UDP over FakeTCP obfuscation | ✅ | ✅ |
|
|
|
|
| UDP over ICMP obfuscation | ❌ | ✅ |
|
|
|
|
| UDP over UDP obfuscation | ❌ | ✅ |
|
|
|
|
| Multi-threaded | ✅ | ❌ |
|
|
|
|
| Throughput | Better | Good |
|
2022-04-10 03:25:32 -07:00
|
|
|
| L4 IP mode | TUN interface | Raw sockets + BPF |
|
2021-09-19 00:36:40 -07:00
|
|
|
| Tunneling MTU overhead | 12 bytes | 44 bytes |
|
|
|
|
| Seprate TCP connections for each UDP connection | Client/Server | Server only |
|
|
|
|
| Anti-replay, encryption | ❌ | ✅ |
|
2021-10-30 09:21:23 -07:00
|
|
|
| IPv6 | UDP only | ✅ |
|
2021-09-19 00:36:40 -07:00
|
|
|
|
2021-09-19 00:38:11 -07:00
|
|
|
[Back to TOC](#table-of-contents)
|
|
|
|
|
|
|
|
# License
|
2021-09-19 00:36:40 -07:00
|
|
|
|
2022-01-03 07:57:09 -08:00
|
|
|
Copyright 2021-2022 Datong Sun (dndx@idndx.com)
|
2021-09-19 00:36:40 -07:00
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
|
|
|
[https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0)> or the MIT license
|
|
|
|
<LICENSE-MIT or [https://opensource.org/licenses/MIT](https://opensource.org/licenses/MIT)>, at your
|
|
|
|
option. Files in the project may not be
|
|
|
|
copied, modified, or distributed except according to those terms.
|
2021-09-19 00:38:11 -07:00
|
|
|
|
|
|
|
[Back to TOC](#table-of-contents)
|
|
|
|
|