-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
3032e13
commit eb13281
Showing
11 changed files
with
729 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
__pycache__ | ||
.DS_Store | ||
/dist |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2023 Peter Story | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,47 @@ | ||
# pydiode | ||
# PyDiode | ||
|
||
Transfer data through a unidirectional network (i.e., a data diode). | ||
|
||
## Installation | ||
|
||
Install from PyPI: | ||
``` | ||
pip install pydiode | ||
``` | ||
|
||
To install from source, clone the repo then run: | ||
``` | ||
pip install . | ||
``` | ||
|
||
## Usage | ||
|
||
Documentation: | ||
``` | ||
pydiode --help | ||
pydiode send --help | ||
pydiode receive --help | ||
``` | ||
|
||
Start a receiver on localhost: | ||
``` | ||
pydiode --debug receive 127.0.0.1 | ||
``` | ||
|
||
Send data to the receiver, from localhost to localhost: | ||
``` | ||
pydiode --debug send 127.0.0.1 127.0.0.1 | ||
``` | ||
|
||
Type some information into the receiver. When finished, press enter, then type Control-D to signal the end-of-file. The receiver should print the received information. | ||
|
||
With debug-level logging, you will see details about each packet sent and received. Omit the `--debug` paramater when sending large amount of data, since debug-level logging incurs significant CPU usage. | ||
|
||
## Development | ||
|
||
Run unit tests: | ||
``` | ||
python -m unittest tests.tests | ||
``` | ||
|
||
Since [the unit tests run on the installed code](https://blog.ionelmc.ro/2014/05/25/python-packaging/), remember to install the latest version of the code before running the unit tests. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
[build-system] | ||
requires = ["hatchling"] | ||
build-backend = "hatchling.build" | ||
|
||
[project] | ||
name = "pydiode" | ||
version = "0.0.1" | ||
authors = [ | ||
{ name="Peter Story", email="[email protected]" }, | ||
] | ||
description = "Transfer data through a unidirectional network (i.e., a data diode)" | ||
readme = "README.md" | ||
requires-python = ">=3.11" | ||
classifiers = [ | ||
"Programming Language :: Python :: 3", | ||
"License :: OSI Approved :: MIT License", | ||
"Operating System :: OS Independent", | ||
] | ||
|
||
[project.urls] | ||
"Homepage" = "https://github.com/ClarkuCSCI/pydiode" | ||
"Bug Tracker" = "https://github.com/ClarkuCSCI/pydiode/issues" | ||
|
||
[project.scripts] | ||
pydiode = "pydiode.main:main" |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import logging | ||
import struct | ||
|
||
# How much data will fit in each packet we send? | ||
# Experimentally, this is the maximum UDP payload I can send on macOS. | ||
UDP_MAX_BYTES = 9216 | ||
|
||
# Number of bits in a byte | ||
BYTE = 8 | ||
|
||
# Color, represented as a character: Red, Blue, and blacK (1 byte). | ||
# Number of packets, represented as an unsigned short (2 bytes) | ||
# Sequence number, represented as an unsigned short (2 bytes) | ||
# The payload, represented as an array of bytes | ||
PACKET_HEADER = struct.Struct("<cHH") | ||
|
||
# Maximum payload length | ||
MAX_PAYLOAD = UDP_MAX_BYTES - PACKET_HEADER.size | ||
|
||
# Whether to log details about each packet sent/received. | ||
# Only log packet details when debugging, due to CPU overhead. | ||
LOG_PACKETS = False | ||
|
||
|
||
def log_packet(prefix, data): | ||
if LOG_PACKETS: | ||
color, n_packets, seq = PACKET_HEADER.unpack(data[: PACKET_HEADER.size]) | ||
payload_length = len(data) - PACKET_HEADER.size | ||
logging.debug( | ||
f"{prefix} <Packet color={color} n_packets={n_packets} " | ||
f"seq={seq} payload_length={payload_length}>" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import argparse | ||
import hashlib | ||
import random | ||
|
||
|
||
def generate_data(byte_count, seed, output): | ||
""" | ||
:param byte_count: Number of bytes | ||
:param seed: Seed used to generate the data | ||
:param output: Output file name | ||
:returns: A checksum of the data | ||
""" | ||
# It's faster to generate data in chunks, instead of one byte at a time | ||
WRITE_CHUNK = 1000 | ||
random.seed(seed) | ||
with open(output, "wb") as f: | ||
for i in range(int(byte_count / WRITE_CHUNK)): | ||
f.write(random.randbytes(WRITE_CHUNK)) | ||
f.write(random.randbytes(byte_count % WRITE_CHUNK)) | ||
with open(output, "rb") as f: | ||
return hashlib.file_digest(f, "sha256").hexdigest() | ||
|
||
|
||
def main(): | ||
parser = argparse.ArgumentParser( | ||
description="Generate a file containing random data." | ||
) | ||
parser.add_argument( | ||
"output", | ||
help="Output file name", | ||
) | ||
parser.add_argument( | ||
"--byte-count", | ||
help="Number of bytes to generate", | ||
type=int, | ||
default=125000, | ||
) | ||
parser.add_argument( | ||
"--seed", | ||
help="Random seed for generating data", | ||
type=int, | ||
default=random.randint(0, 1000000), | ||
) | ||
args = parser.parse_args() | ||
print(generate_data(args.byte_count, args.seed, args.output)) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
import argparse | ||
import asyncio | ||
import logging | ||
|
||
import pydiode.common | ||
from .common import BYTE, MAX_PAYLOAD, PACKET_HEADER, UDP_MAX_BYTES | ||
from .send import read_data, send_data | ||
from .receive import AsyncWriter, receive_data | ||
|
||
|
||
async def async_main(): | ||
parser = argparse.ArgumentParser( | ||
description="Send and receive data through a data diode via UDP." | ||
) | ||
subparsers = parser.add_subparsers( | ||
help="Whether to run in send or receive mode" | ||
) | ||
parser.add_argument( | ||
"--debug", | ||
help="Print DEBUG logging", | ||
action="store_const", | ||
dest="loglevel", | ||
const=logging.DEBUG, | ||
default=logging.WARNING, | ||
) | ||
parser.add_argument( | ||
"--verbose", | ||
help="Print INFO logging", | ||
action="store_const", | ||
dest="loglevel", | ||
const=logging.INFO, | ||
) | ||
|
||
send_parser = subparsers.add_parser("send", help="Send data") | ||
send_parser.add_argument( | ||
"read_ip", help="The IP of the interface data is read from" | ||
) | ||
send_parser.add_argument( | ||
"write_ip", | ||
help="The IP of the interface data is written to", | ||
) | ||
send_parser.add_argument( | ||
"--port", | ||
type=int, | ||
help="Send and receive data using this port", | ||
default=1234, | ||
) | ||
send_parser.add_argument( | ||
"--max-bitrate", | ||
type=int, | ||
help="Maximum number of bits transferred per second", | ||
default=1000000000, | ||
) | ||
send_parser.add_argument( | ||
"--chunk-duration", | ||
type=float, | ||
help="Send each chunk for this many seconds", | ||
) | ||
send_parser.add_argument( | ||
"--chunk-max-packets", | ||
type=int, | ||
help="The maximum number of packets a chunk should contain", | ||
) | ||
send_parser.add_argument( | ||
"--redundancy", | ||
type=int, | ||
help="How many times to send each chunk", | ||
default=2, | ||
) | ||
|
||
receive_parser = subparsers.add_parser("receive", help="Receive data") | ||
receive_parser.add_argument( | ||
"read_ip", help="The IP of the interface data is read from" | ||
) | ||
receive_parser.add_argument( | ||
"--port", | ||
type=int, | ||
help="Send and receive data using this port", | ||
default=1234, | ||
) | ||
|
||
args = parser.parse_args() | ||
logging.basicConfig(level=args.loglevel) | ||
# Only log packet details when debugging, due to CPU overhead | ||
if args.loglevel == logging.DEBUG: | ||
pydiode.common.LOG_PACKETS = True | ||
|
||
# If we are sending data | ||
if "write_ip" in args: | ||
if args.chunk_duration and args.chunk_max_packets: | ||
raise ValueError( | ||
"Supply either --chunk-duration or --chunk-max-packets" | ||
) | ||
elif not args.chunk_duration and not args.chunk_max_packets: | ||
args.chunk_max_packets = 100 | ||
|
||
# Calculate chunk_duration based on chunk_max_packets, or vice versa | ||
if args.chunk_max_packets: | ||
# How many seconds do we need to send this many fully loaded | ||
# packets without exceeding max_bitrate? | ||
args.chunk_duration = ( | ||
args.chunk_max_packets * UDP_MAX_BYTES * BYTE / args.max_bitrate | ||
) | ||
else: | ||
# How many fully loaded packets can be sent per second without | ||
# exceeding max_bitrate? | ||
# TODO Consider whether to account for UDP and IPv4 headers. | ||
args.chunk_max_packets = ( | ||
args.chunk_duration * args.max_bitrate / BYTE / UDP_MAX_BYTES | ||
) | ||
logging.debug(f"chunk_max_packets={args.chunk_max_packets}") | ||
logging.debug(f"chunk_duration={args.chunk_duration}") | ||
|
||
# How much data will fit in these packets? | ||
chunk_max_data_bytes = int(args.chunk_max_packets * MAX_PAYLOAD) | ||
logging.debug(f"PACKET_HEADER.size={PACKET_HEADER.size}") | ||
logging.debug(f"MAX_PAYLOAD={MAX_PAYLOAD}") | ||
logging.debug(f"chunk_max_data_bytes={chunk_max_data_bytes}") | ||
|
||
# Queue of chunks to be sent | ||
chunks = [] | ||
# Read and send data concurrently | ||
await asyncio.gather( | ||
read_data(chunks, chunk_max_data_bytes, args.chunk_duration), | ||
send_data( | ||
chunks, | ||
args.chunk_duration, | ||
args.redundancy, | ||
args.read_ip, | ||
args.write_ip, | ||
args.port, | ||
), | ||
) | ||
# If we are receiving data | ||
else: | ||
queue = asyncio.Queue() | ||
writer = AsyncWriter(queue) | ||
await asyncio.gather( | ||
receive_data(queue, args.read_ip, args.port), writer.write() | ||
) | ||
|
||
|
||
def main(): | ||
asyncio.run(async_main()) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
Oops, something went wrong.