├── .gitignore ├── README.md ├── backTCP.py ├── recv.py ├── send.py ├── testch.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Python Cache 2 | *.py[cod] 3 | __pycache__/ 4 | 5 | # Vim 6 | *.swp 7 | *.sw_ 8 | *~ 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # backTCP Python Template 2 | 3 | This is a template code for the backTCP course lab of « Computer Networking » in fall 2019 of USTC. 4 | 5 | ## Instructions 6 | 7 | You should read this document and all comments in `backTCP.py` file, and then fill out code segments marked as `TODO` in `backTCP.py`. There's no need to change any other file but you're free to if you want. 8 | 9 | Note that all binary data are assumed to be of the type `bytes` in this program. 10 | 11 | It's recommended that you write this program on Python 3.6 or newer. Any issue caused by an incompatible Python version (namely, Python 3.5 or lower) will be disregarded. 12 | 13 | ### Running 14 | 15 | The current version of the code contains a minimal proof of concept. You can verify that it sends and receives a file *unreliably* using the following commands: 16 | 17 | ```shell 18 | # Generate a file for input 19 | dd if=/dev/urandom of=input.bin bs=64k count=1 status=none 20 | 21 | # Start a server and listen for connection 22 | python3 recv.py output.bin 23 | 24 | # Open another terminal and run 25 | python3 send.py input.bin 26 | 27 | # Compare the results 28 | cmp input.bin output.bin 29 | ``` 30 | 31 | It will generate an `output.bin` that's identical to `input.bin`. This verifies that the code template is correct. 32 | 33 | ### Testing 34 | 35 | After you've filled out all required code parts, you can run the same commands against a test channel to verify your implementation. 36 | 37 | There's a simple test channel implementation in `testch.py`. You should run the programs in this order: 38 | 39 | - Start the receiver (pay attention to the address and port) 40 | - Start the test channel. Use `-a` and `-p` options to give information (namely, address and port) about the receiver to the test channel. You can also specify the address and port to listen for the sender using `-A` and `-P` options. 41 | - Finally, start the sender. Note that the default port for the sender is the same as that of the receiver, so you'll probably want to change (at least) the port using the `-p` option so it sends to the test channel. 42 | 43 | Here's an example of what you'd like to run for the test: 44 | 45 | ```shell 46 | # Generate an input file, same command as shown above 47 | 48 | # Start the receiver (default port 6666) 49 | python3 recv.py output.bin 50 | 51 | # Start the test channel. By default it connects to port 6666 and listens on 6667 52 | python3 testch.py 53 | 54 | # Finally, start the sender. You should redirect it to the test channel 55 | python3 send.py -p 6667 input.bin 56 | ``` 57 | 58 | If you started those programs properly, packets coming out from the sender will be randomly manipulated by the test channel before going into the receiver. As of the current version of the test channel, packets will randomly encounter one of the following disorders: 59 | 60 | - Nothing happens, packets pass through as normal 61 | - A packet is dropped (unless it's a retransmitted packet, which will never be dropped - this is consistent with the lab specification) 62 | - Two packets are swapped 63 | - Three packets are shuffled and up to one of them may be duplicated or dropped (still, retransmitted packets won't be dropped, but may be shuffled or duplicated) 64 | 65 | You should consult the man page or use `--help` if you're unsure about which command does what. 66 | 67 | After the sender exits, both the test channel and the receiver will exit automatically. You can then verify the integrity of the received file using the same `cmp` command. 68 | 69 | ## Regulations 70 | 71 | If you submit your lab based on this template, you must retain the origin notice located at the end of this README document. 72 | Other than that, the code in this repository is licensed under the MIT license. 73 | The original author holds no liability for this repository. 74 | 75 | > ### Origin Disclosure 76 | > 77 | > The backTCP-python code template is created by [iBug](https://github.com/iBug) 78 | -------------------------------------------------------------------------------- /backTCP.py: -------------------------------------------------------------------------------- 1 | import os 2 | import socket 3 | 4 | from utils import log 5 | 6 | 7 | class BTcpConnection: 8 | def __init__(self, mode, addr, port): 9 | # Create a TCP socket 10 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 11 | self.conn = None 12 | self.remote_addr = None 13 | 14 | if mode == 'send': 15 | self.remote_addr = addr, port 16 | self.sock.connect(self.remote_addr) 17 | self.conn = self.sock 18 | 19 | elif mode == 'recv': 20 | self.sock.bind((addr, port)) 21 | log('info', f"Listening on {addr} port {port}") 22 | self.sock.listen(1) 23 | self.conn, self.remote_addr = self.sock.accept() 24 | log('info', f"Accepted connection from {self.remote_addr[0]} port {self.remote_addr[1]}") 25 | else: 26 | raise ValueError(f"Unexpected mode {mode}") 27 | 28 | def __del__(self): 29 | self.close() 30 | 31 | def close(self): 32 | try: 33 | self.conn.shutdown(socket.SHUT_RDWR) 34 | self.conn.close() 35 | except Exception: 36 | pass 37 | try: 38 | self.sock.close() 39 | except Exception: 40 | pass 41 | # set them to None so other code knows 42 | self.conn = None 43 | self.sock = None 44 | 45 | def settimeout(self, timeout): 46 | self.sock.settimeout(timeout) 47 | 48 | def send(self, packet): 49 | if packet is None: 50 | packet = b'' 51 | self.conn.sendall(bytes(packet)) 52 | 53 | def recv(self, size=None): 54 | return BTcpPacket.from_bytes(self.conn.recv(size or (7 + 64))) 55 | 56 | 57 | 58 | class BTcpPacket: 59 | def __init__(self, sport=0, dport=0, seq=0, ack=0, data_off=0, win_size=0, flag=0, data=b""): 60 | self.sport = sport 61 | self.dport = dport 62 | self.seq = seq 63 | self.ack = ack 64 | self.data_off = data_off 65 | self.win_size = win_size 66 | self.flag = flag 67 | self.data = data 68 | 69 | def regulate(self): 70 | # Make sure the values don't stir up 71 | self.seq &= 0xFF 72 | self.ack &= 0xFF 73 | self.data_off &= 0xFF 74 | self.win_size &= 0xFF 75 | self.flag &= 1 # Could be 0xFF, but we only need "retransmission" flag 76 | 77 | def __bytes__(self): 78 | self.regulate() 79 | return bytes([ 80 | self.sport, self.dport, self.seq, self.ack, 81 | self.data_off, self.win_size, self.flag, 82 | ]) + bytes(self.data) 83 | 84 | @staticmethod 85 | def from_bytes(data): 86 | if not data: 87 | return None 88 | return BTcpPacket( 89 | sport=data[0], dport=data[1], seq=data[2], ack=data[3], 90 | data_off=data[4], win_size=data[5], flag=data[6], data=data[7:] 91 | ) 92 | 93 | def __repr__(self): 94 | if len(self.data) > 1: 95 | s = f"<{len(self.data)} bytes>" 96 | elif len(self.data) == 0: 97 | s = "" 98 | else: 99 | s = "<1 byte>" 100 | return f"BTcpPacket(seq={self.seq}, ack={self.ack}, win_size={self.win_size}, flag={self.flag}, data={s})" 101 | 102 | 103 | def send(data, addr, port): 104 | conn = BTcpConnection('send', addr, port) 105 | 106 | chunks = [data[x * 64:x * 64 + 64] for x in range((len(data) - 1) // 64 + 1)] 107 | packets = [BTcpPacket(seq=i & 0xFF, data_off=7, data=chunk) for i, chunk in enumerate(chunks)] 108 | 109 | # TODO: "data" is a bytes object 110 | # You should split it up into BTcpPacket objects, and call conn.send(pkt) on each one 111 | # Example: > p = BTcpPacket(data=b"hello") 112 | # > conn.send(p) 113 | 114 | # TODO: Delete the following code and add your own 115 | 116 | for p in packets: 117 | conn.send(p) 118 | 119 | # End of your own code 120 | return 121 | 122 | 123 | def recv(addr, port): 124 | conn = BTcpConnection('recv', addr, port) 125 | 126 | data = b'' # Nothing received yet 127 | 128 | # TODO: Call conn.recv to receive packets 129 | # Received packets are of class BTcpPacket, so you can access packet information and content easily 130 | # Example: > p = conn.recv() 131 | # Now p.seq, p.ack, p.data (and everything else) are available 132 | 133 | # TODO: Assemble received binary data into `data` variable. 134 | # Make sure you're handling disorder and timeouts properly 135 | 136 | conn.settimeout(0.010) # 10ms timeout 137 | while True: 138 | p = conn.recv() 139 | if p is None: # No more packets 140 | break 141 | data += p.data 142 | 143 | # End of your own code 144 | 145 | return data 146 | -------------------------------------------------------------------------------- /recv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import os 5 | import argparse 6 | 7 | import backTCP 8 | from utils import * 9 | 10 | 11 | def parse_args(): 12 | parser = argparse.ArgumentParser(description="receive a file from backTCP", epilog="This program is created by iBug") 13 | parser.add_argument('filename', metavar="file", help="the name to save received file as") 14 | parser.add_argument('-a', '-A', '--address', metavar="addr", help="address to listen for", default="0.0.0.0") 15 | parser.add_argument('-p', '--port', metavar="port", type=int, help="port to listen on", default=6666) 16 | parser.add_argument('-l', '--log-level', metavar="level", help="logging level", default=LOG_WARNING) 17 | return parser.parse_args() 18 | 19 | 20 | def main(): 21 | args = parse_args() 22 | set_log_level(args.log_level) 23 | 24 | with open(args.filename, "wb") as f: 25 | f.write(backTCP.recv(args.address, args.port)) 26 | 27 | 28 | if __name__ == '__main__': 29 | main() 30 | -------------------------------------------------------------------------------- /send.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import os 5 | import argparse 6 | 7 | import backTCP 8 | from utils import * 9 | 10 | 11 | def parse_args(): 12 | parser = argparse.ArgumentParser(description="send a file via backTCP", epilog="This program is created by iBug") 13 | parser.add_argument('filename', metavar="file", help="the name of the file to send") 14 | parser.add_argument('-a', '-A', '--address', metavar="addr", help="address of target to send", default="127.0.0.1") 15 | parser.add_argument('-p', '--port', metavar="port", type=int, help="port of target to send", default=6666) 16 | parser.add_argument('-l', '--log-level', metavar="level", help="logging level", default=LOG_WARNING) 17 | return parser.parse_args() 18 | 19 | 20 | def main(): 21 | args = parse_args() 22 | set_log_level(args.log_level) 23 | 24 | with open(args.filename, "rb") as f: 25 | data = f.read() 26 | backTCP.send(data, args.address, args.port) 27 | 28 | 29 | if __name__ == '__main__': 30 | main() 31 | -------------------------------------------------------------------------------- /testch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import os 5 | import argparse 6 | import random 7 | import threading 8 | 9 | import backTCP 10 | from utils import * 11 | 12 | 13 | # Actions: What to do for a stream of incoming packets 14 | # 0: Do nothing and forward 15 | # 1: Drop unless retransmitted 16 | # 2: Swap two packets 17 | # 3: Randomly order 3 packets and maybe drop one and maybe duplicate one 18 | # 19 | # You can configure the following list to change the possibility of each action 20 | ACTIONS = [0] * 7 + [1] * 5 + [2] * 5 + [3] * 3 21 | 22 | 23 | def pass_through(from_socket, to_socket): 24 | def handler(from_socket, to_socket): 25 | while True: 26 | if from_socket.sock is None: 27 | # Closed - don't waste CPU 28 | break 29 | try: 30 | # Blindly forward packets 31 | p = from_socket.recv() 32 | to_socket.send(p) 33 | except Exception: 34 | pass 35 | 36 | # Run in background and don't worry anymore 37 | t = threading.Thread(target=handler, args=(from_socket, to_socket), daemon=True) 38 | t.start() 39 | return t 40 | 41 | 42 | def btMITM(out_addr, out_port, in_addr, in_port): 43 | # This is going to be challenging: listen and send at the same time while manipulating packets 44 | in_sock = backTCP.BTcpConnection('recv', in_addr, in_port) 45 | out_sock = backTCP.BTcpConnection('send', out_addr, out_port) 46 | 47 | # We're not going to manipulate server responses 48 | pass_through(out_sock, in_sock) 49 | 50 | packets = [] 51 | 52 | while True: 53 | action = random.choice(ACTIONS) 54 | log('debug', f"Action: {action}") 55 | packet_needed = max(1, action) 56 | packet_count = 0 57 | 58 | while packet_count < packet_needed: 59 | try: 60 | p = in_sock.recv() 61 | except socket.error as e: 62 | if e.errno == errno.ECONNRESET: 63 | p = None 64 | else: 65 | raise 66 | if p is None: 67 | # The last ones aren't manipulated 68 | for p in packets: 69 | out_sock.send(p) 70 | out_sock.send(None) # Tell the receiver to close 71 | in_sock.close() 72 | out_sock.close() 73 | return 74 | packet_count += 1 75 | packets.append(p) 76 | 77 | if action == 0: 78 | pass # through 79 | elif action == 1: 80 | if not packets[0].flag & 1: 81 | # Packet loss 82 | packets.pop() 83 | elif action == 2: 84 | # Swap packets 85 | packets = packets[::-1] 86 | else: 87 | # Shuffle three packets ... 88 | random.shuffle(packets) 89 | for i in range(len(packets)): 90 | if random.random() >= 0.8: 91 | # ... and maybe duplicate one ... 92 | packets.append(random.choice(packets)) 93 | break 94 | if not packets[i].flag & 1 and random.random() >= 0.5: 95 | # ... or drop up to 1 at random 96 | packets.pop(i) 97 | break 98 | 99 | for p in packets: 100 | out_sock.send(p) 101 | packets = [] 102 | 103 | 104 | def parse_args(): 105 | parser = argparse.ArgumentParser(description="starts a backTCP test channel", epilog="This program is created by iBug") 106 | parser.add_argument('-a', '--out-addr', '--address', metavar="addr", help="address of receiver", default="127.0.0.1") 107 | parser.add_argument('-p', '--out-port', '--port', metavar="port", type=int, help="port of receiver", default=6666) 108 | parser.add_argument('-A', '--in-addr', metavar="addr", help="address to listen for sender", default="0.0.0.0") 109 | parser.add_argument('-P', '--in-port', metavar="port", type=int, help="port to listen for sender", default=6667) 110 | parser.add_argument('-l', '--log-level', metavar="level", help="logging level", default=LOG_WARNING) 111 | return parser.parse_args() 112 | 113 | 114 | def main(): 115 | args = parse_args() 116 | set_log_level(args.log_level) 117 | 118 | btMITM(args.out_addr, args.out_port, args.in_addr, args.in_port) 119 | 120 | 121 | if __name__ == '__main__': 122 | main() 123 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | LOG_DEBUG = 0 5 | LOG_INFO = 1 6 | LOG_WARNING = 2 7 | LOG_ERROR = 3 8 | 9 | LOG_PREFIX = { 10 | LOG_DEBUG: "\x1B[0m[D]\x1B[0m", 11 | LOG_INFO: "\x1B[36m[I]\x1B[0m", 12 | LOG_WARNING: "\x1B[33m[W]\x1B[0m", 13 | LOG_ERROR: "\x1B[31m[E]\x1B[0m", 14 | } 15 | LOG_STR_MAPPING = { 16 | 'debug': LOG_DEBUG, 17 | 'info': LOG_INFO, 18 | 'warn': LOG_WARNING, 19 | 'warning': LOG_WARNING, 20 | 'error': LOG_ERROR, 21 | 'critical': LOG_ERROR, 22 | } 23 | 24 | 25 | log_level = LOG_WARNING 26 | 27 | 28 | def validate_log_level(level): 29 | try: 30 | level = int(level) 31 | if 0 <= level <= LOG_ERROR: 32 | return level 33 | except Exception: 34 | pass 35 | 36 | if isinstance(level, str): 37 | try: 38 | return LOG_STR_MAPPING[level.lower()] 39 | except KeyError: 40 | raise ValueError(f"Invalid log level {level!r}") from None 41 | 42 | 43 | def set_log_level(level): 44 | global log_level 45 | log_level = validate_log_level(level) 46 | 47 | 48 | def log(level, *args): 49 | if not isinstance(level, int): 50 | try: 51 | level = LOG_STR_MAPPING[level.lower()] 52 | except KeyError: 53 | raise ValueError(f"Unexpected logging level {level!r}") from None 54 | if level < log_level: 55 | return # Discard unwanted logs 56 | s = "{} {}".format(LOG_PREFIX[level], " ".join(map(str, args))) 57 | print(s, file=sys.stderr) 58 | --------------------------------------------------------------------------------