├── requirements.txt ├── .gitignore ├── images ├── mongo_find.png ├── decode_as_mongo.png └── tcp_payload_decode_as.png ├── example └── straceSample.pcap ├── tools ├── cli_tshark_protocol_render │ ├── statsd.sh │ ├── memcache.sh │ ├── es_http_json.sh │ ├── fcgi.sh │ ├── redis_resp.sh │ ├── mongo.sh │ └── mongo_q_timings.sh └── strace_convert │ └── xx2generic.py ├── strace2pcap.sh ├── strace2pcap-pipe.sh ├── .github └── workflows │ └── pylint.yml ├── process_cascade.py ├── grpc_seed.py ├── tests ├── test_inet_passthrough.py └── test_unix_tcp.py ├── tcp_utils.py ├── http2_tools.py ├── py_strace2pcap.py ├── README.md ├── strace_parser_2_packet.py ├── unix_tcp_synth.py ├── strace_parser.py └── LICENSE /requirements.txt: -------------------------------------------------------------------------------- 1 | scapy==2.5.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | __pycache__ 3 | -------------------------------------------------------------------------------- /images/mongo_find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comboshreddies/py-strace2pcap/HEAD/images/mongo_find.png -------------------------------------------------------------------------------- /example/straceSample.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comboshreddies/py-strace2pcap/HEAD/example/straceSample.pcap -------------------------------------------------------------------------------- /images/decode_as_mongo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comboshreddies/py-strace2pcap/HEAD/images/decode_as_mongo.png -------------------------------------------------------------------------------- /images/tcp_payload_decode_as.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comboshreddies/py-strace2pcap/HEAD/images/tcp_payload_decode_as.png -------------------------------------------------------------------------------- /tools/cli_tshark_protocol_render/statsd.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | FILE=$1 4 | 5 | tshark -r "$FILE" -T pdml -Y 'udp.port==9125' |\ 6 | grep -e 'frame.time_epoch' -e 'udp.payload' |\ 7 | sed 's/.*frame.time_epoch.*show="\([0123456789.]*\)".*/{"time": "\1"},/g' |\ 8 | sed 's|.* value="\(.*\)"/>|\1\n\r|g' |\ 9 | awk '{if($1 ~ /{/) print $0 ; else {print "" ;system("echo "$0" | xxd -r -p ")}}' 10 | -------------------------------------------------------------------------------- /tools/cli_tshark_protocol_render/memcache.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | FILE=$1 4 | 5 | tshark -r "$FILE" -T pdml -Y 'tcp.port==11211' |\ 6 | grep -e 'frame.time_epoch' -e 'tcp.payload' |\ 7 | sed 's/.*frame.time_epoch.*show="\([0123456789.]*\)".*/{"time": "\1"},/g' |\ 8 | sed 's|.* value="\(.*\)"/>|\1,\n\r|g' |\ 9 | awk '{if($1 ~ /{/) print $0 ; else {print "" ;system("echo "$0" | xxd -r -p ")}}' 10 | -------------------------------------------------------------------------------- /tools/cli_tshark_protocol_render/es_http_json.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | FILE=$1 4 | 5 | tshark -r "$FILE" -T pdml -Y 'tcp.port==9200' |\ 6 | grep -e 'frame.time_epoch' -e 'tcp.payload' |\ 7 | sed 's/.*"http.request.uri".* show="\(.*\)" value=.*/{"q":"\1"},/g' |\ 8 | sed 's/.*frame.time_epoch.*show="\([0123456789.]*\)".*/{"time": "\1"},/g' |\ 9 | sed 's|.* value="\(.*\)"/>|\1,\n\r|g' | \ 10 | awk '{if($1 ~ /{/) print $0 ; else {print "" ;system("echo "$0" | xxd -r -p ")}}' 11 | -------------------------------------------------------------------------------- /tools/cli_tshark_protocol_render/fcgi.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | FILE=$1 4 | 5 | tshark -r "$FILE" -T pdml -Y 'tcp.port==9000' |\ 6 | grep -e 'frame.time_epoch' -e 'tcp.payload' |\ 7 | sed 's/.*"http.request.uri".* show="\(.*\)" value=.*/{"q":"\1"},/g' |\ 8 | sed 's/.*frame.time_epoch.*show="\([0123456789.]*\)".*/{"time": "\1"},/g' |\ 9 | sed 's|.* value="\(.*\)"/>|\1,\n\r|g' | \ 10 | awk '{if($1 ~ /{/) print $0 ; else {print "" ;system("echo "$0" | xxd -r -p ")}}' 11 | -------------------------------------------------------------------------------- /tools/cli_tshark_protocol_render/redis_resp.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | FILE=$1 4 | 5 | tshark -r "$FILE" -T pdml -Y "tcp.port==$2" |\ 6 | grep -e 'frame.time_epoch' -e 'tcp.payload' |\ 7 | sed 's/.*"http.request.uri".* show="\(.*\)" value=.*/{"q":"\1"},/g' |\ 8 | sed 's/.*frame.time_epoch.*show="\([0123456789.]*\)".*/{"time": "\1"},/g' |\ 9 | sed 's|.* value="\(.*\)"/>|\1,\n\r|g' | \ 10 | awk '{if($1 ~ /{/) print $0 ; else {print "" ;system("echo "$0" | xxd -r -p ")}}' 11 | -------------------------------------------------------------------------------- /tools/cli_tshark_protocol_render/mongo.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | FILE=$1 4 | 5 | tshark -r "$FILE" -T pdml -Y 'tcp.port==27017' |\ 6 | grep -e 'frame.time_epoch' -e 'tcp.payload' |\ 7 | sed 's/.*"http.request.uri".* show="\(.*\)" value=.*/{"q":"\1"},/g' |\ 8 | sed 's/.*frame.time_epoch.*show="\([0123456789.]*\)".*/{"time": "\1"},/g' |\ 9 | sed 's|.* value="\(.*\)"/>|\1,\n\r|g' | \ 10 | awk '{if($1 ~ /{/) print $0 ; else {print "" ;system("echo "$0" | xxd -r -p ")}}' 11 | 12 | -------------------------------------------------------------------------------- /strace2pcap.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | if [ $# -ne 2 ] ; then 4 | echo "$0 \"command to execute or -p , ie strace arguments>\"" 5 | echo "note -> keep strace arugments within qoutes" 6 | exit 1 7 | fi 8 | 9 | OUT_FILE=$1 10 | STRACE_ARGS=$2 11 | 12 | echo "import scapy" | python3 13 | ERR=$? 14 | if [ $ERR -ne 0 ] ; then 15 | echo to run conversion to pcap, please install scapy python module 16 | exit 2 17 | fi 18 | 19 | strace -f -s655350 -o "! ./py_strace2pcap.py $OUT_FILE" -ttt -T -yy -xx -e trace=network $STRACE_ARGS 20 | 21 | -------------------------------------------------------------------------------- /strace2pcap-pipe.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ $# -ne 1 ] ; then 4 | echo "$0 \"command to execute or -p , ie strace arguments>\"" 5 | echo "note -> keep strace arugments within qoutes" 6 | exit 1 7 | fi 8 | 9 | OUT_FILE=$(mktemp /tmp/strace2pcap-pipe.XXXX) 10 | STRACE_ARGS="$1" 11 | 12 | echo "import scapy" | python3 13 | ERR=$? 14 | if [ $ERR -ne 0 ] ; then 15 | echo to run conversion to pcap, please install scapy python module 16 | exit 2 17 | fi 18 | 19 | strace -f -s655350 -o "! ./py_strace2pcap.py " "$OUT_FILE" -ttt -T -yy -xx -e trace=network $STRACE_ARGS > /dev/null & 20 | STRACE_PID=$! 21 | tail --pid=$STRACE_PID -c 1000000 -f "$OUT_FILE" 22 | rm "$OUT_FILE" 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/pylint.yml: -------------------------------------------------------------------------------- 1 | name: Pylint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v3 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install pylint 21 | - name: Analysing the code with pylint 22 | run: | 23 | pylint --disable=F0401 --disable=E0611 $(git ls-files --exclude-standard '*.py') 24 | -------------------------------------------------------------------------------- /process_cascade.py: -------------------------------------------------------------------------------- 1 | """ stream cascade processor """ 2 | 3 | 4 | class ProcessCascade(): 5 | """ strace line parser generator """ 6 | def __init__(self, processor, in_stream): 7 | if not (hasattr(in_stream, '__next__') and 8 | callable(in_stream.__next__)): 9 | raise ValueError('invalid input') 10 | self.input = in_stream 11 | self.processor = processor() 12 | 13 | def __iter__(self): 14 | return self 15 | 16 | def __next__(self): 17 | """ read next line from stream, until it's parsable """ 18 | if self.processor.has_split_cache(): 19 | return self.processor.get_split_cache() 20 | new_chunk = self.input.__next__() 21 | while new_chunk: 22 | parsed = self.processor.process(new_chunk) 23 | if parsed: 24 | return parsed 25 | new_chunk = self.input.__next__() 26 | raise StopIteration() 27 | -------------------------------------------------------------------------------- /grpc_seed.py: -------------------------------------------------------------------------------- 1 | """gRPC and HTTP/2 seed frames and heuristics.""" 2 | 3 | from __future__ import annotations 4 | 5 | HTTP2_PREFACE = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" 6 | HTTP2_CLIENT_SETTINGS_FRAME = b"\x00\x00\x00\x04\x00\x00\x00\x00\x00" 7 | HTTP2_SETTINGS_ACK_FRAME = b"\x00\x00\x00\x04\x01\x00\x00\x00\x00" 8 | HTTP2_CLIENT_SEED = HTTP2_PREFACE + HTTP2_CLIENT_SETTINGS_FRAME 9 | 10 | GRPC_HEADERS_FRAME = bytes.fromhex( 11 | "00008101040000000140073a6d6574686f6404504f535440073a736368656d65046874747040053a70" 12 | "6174681b2f706c616365686f6c6465722e536572766963652f4d6574686f64400a3a617574686f7269" 13 | "7479096c6f63616c686f7374400c636f6e74656e742d74797065106170706c69636174696f6e2f6770" 14 | "72634002746508747261696c657273" 15 | ) 16 | 17 | _GRPC_KEYWORDS = ( 18 | b"application/grpc", 19 | b"grpc-status", 20 | b"content-type", 21 | b"/Service/", 22 | ) 23 | 24 | 25 | def frame_has_grpc_evidence(frame_type: int, payload: bytes) -> bool: 26 | """Return True when the payload suggests gRPC traffic.""" 27 | if frame_type == 0 and len(payload) >= 5: 28 | compressed = payload[0] 29 | msg_len = int.from_bytes(payload[1:5], "big") 30 | if compressed in (0, 1) and msg_len <= len(payload) - 5: 31 | return True 32 | if frame_type in (1, 9): 33 | return any(marker in payload for marker in _GRPC_KEYWORDS) 34 | return False 35 | -------------------------------------------------------------------------------- /tests/test_inet_passthrough.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import subprocess 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) 7 | 8 | 9 | def _run_converter(tmp_path: Path, name: str, lines) -> Path: 10 | pcap_path = tmp_path / name 11 | cmd = [ 12 | sys.executable, 13 | 'py_strace2pcap.py', 14 | str(pcap_path), 15 | ] 16 | payload = '\n'.join(lines) + '\n' 17 | subprocess.run(cmd, input=payload, text=True, check=True) 18 | return pcap_path 19 | 20 | 21 | def _read_pcap(path: Path): 22 | packets = [] 23 | with path.open('rb') as fh: 24 | header = fh.read(24) 25 | magic, _, _, _, _, _, network = struct.unpack('5) print "R "$0;}}' | awk '{if($1=="Q") {k=sprintf("%15s %5s %15s %5s",$3,$5,$4,$6);t[k]=$2;ops="";for(i=7;i<=NF;i++) ops=ops" "$i;op[k]=ops} if($1=="R") {k=sprintf("%15s %5s %15s %5s",$4,$6,$3,$5); if(t[k]){printf("%12.6f %12.6f %12.6f %s %s\n",t[k],$2,$2-t[k],k,op[k])}}}' 15 | 16 | 17 | -------------------------------------------------------------------------------- /tcp_utils.py: -------------------------------------------------------------------------------- 1 | """Tiny helpers for building Ethernet/IP/TCP records.""" 2 | 3 | from __future__ import annotations 4 | 5 | import ipaddress 6 | import struct 7 | 8 | ETHERTYPE_IPV4 = 0x0800 9 | TCP_FLAG_FIN = 0x01 10 | TCP_FLAG_SYN = 0x02 11 | TCP_FLAG_PSH = 0x08 12 | TCP_FLAG_ACK = 0x10 13 | MAX_TCP_PAYLOAD = 65535 - 20 - 20 14 | 15 | 16 | def checksum(data: bytes) -> int: 17 | if len(data) % 2: 18 | data += b"\x00" 19 | total = 0 20 | for idx in range(0, len(data), 2): 21 | total += (data[idx] << 8) + data[idx + 1] 22 | while total >> 16: 23 | total = (total & 0xFFFF) + (total >> 16) 24 | return (~total) & 0xFFFF 25 | 26 | 27 | def build_ipv4_header(src_ip: str, dst_ip: str, payload_len: int, ip_id: int) -> bytes: 28 | total_length = 20 + payload_len 29 | header = struct.pack( 30 | "!BBHHHBBH4s4s", 31 | 0x45, 32 | 0, 33 | total_length, 34 | ip_id & 0xFFFF, 35 | 0, 36 | 64, 37 | 6, 38 | 0, 39 | ipaddress.IPv4Address(src_ip).packed, 40 | ipaddress.IPv4Address(dst_ip).packed, 41 | ) 42 | hdr_checksum = checksum(header) 43 | return header[:10] + struct.pack("!H", hdr_checksum) + header[12:] 44 | 45 | 46 | def build_tcp_header( 47 | src_ip: str, 48 | dst_ip: str, 49 | src_port: int, 50 | dst_port: int, 51 | seq: int, 52 | ack: int, 53 | flags: int, 54 | payload: bytes, 55 | ) -> bytes: 56 | base = struct.pack( 57 | "!HHIIHHHH", 58 | src_port, 59 | dst_port, 60 | seq, 61 | ack, 62 | (5 << 12) | flags, 63 | 65535, 64 | 0, 65 | 0, 66 | ) 67 | pseudo = ( 68 | ipaddress.IPv4Address(src_ip).packed 69 | + ipaddress.IPv4Address(dst_ip).packed 70 | + struct.pack("!BBH", 0, 6, len(base) + len(payload)) 71 | ) 72 | tcp_checksum = checksum(pseudo + base + payload) 73 | return struct.pack( 74 | "!HHIIHHHH", 75 | src_port, 76 | dst_port, 77 | seq, 78 | ack, 79 | (5 << 12) | flags, 80 | 65535, 81 | tcp_checksum, 82 | 0, 83 | ) 84 | -------------------------------------------------------------------------------- /tools/strace_convert/xx2generic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ tool for converting strace -xx format to ascii_oct, a generic format """ 3 | 4 | def hex_2_ascii_and_oct(hex_chunk) : 5 | """ helper function for decoding hex array to ascii or oct """ 6 | generic = "" 7 | parts=hex_chunk.split('\\x') 8 | for part in parts[1:] : 9 | value=int(part,16) 10 | if value > 127 or value < 32 : 11 | byte ="\\"+oct(value)[2:] 12 | else : 13 | byte=chr(value) 14 | generic+=byte 15 | return generic 16 | 17 | def convert_gt_part(chunks): 18 | """ deeper part of conversion of < > section """ 19 | gt_part_line="" 20 | for part in chunks[1:] : 21 | gt_part_line += '"' 22 | if len(part) > 2 : 23 | if not (part[0]=='\\' and part[1]=='x') : 24 | gt_part_line += part 25 | else: 26 | gt_part_line += hex_2_ascii_and_oct(part) 27 | return gt_part_line 28 | 29 | 30 | def convert(strace_line) : 31 | """ line format converter """ 32 | args = strace_line.split(' ') 33 | new_line = args[0] 34 | if len(args) > 1 : 35 | new_line += " " 36 | for arg in args[1:] : 37 | chunks = arg.split('"') 38 | if len(chunks) > 0 : 39 | new_line += chunks[0] 40 | new_line += convert_gt_part(chunks) 41 | else : 42 | new_line += " " + arg 43 | new_line += " " 44 | args = new_line.split('<') 45 | n2_line = args[0] 46 | if len(args) > 1 : 47 | n2_line += "<" 48 | for arg in args[1:] : 49 | chunks = arg.split('>') 50 | if len(chunks[0])>2 and chunks[0][0]=='\\' and chunks[0][1]=='x' : 51 | n2_line += hex_2_ascii_and_oct(chunks[0]) 52 | else: 53 | n2_line += chunks[0] 54 | if len(chunks) > 0 : 55 | for part in chunks[1:] : 56 | n2_line += '>' + part 57 | n2_line += '>' 58 | else : 59 | n2_line += chunks[0] 60 | n2_line += "<" 61 | 62 | return n2_line 63 | 64 | if __name__ == '__main__': 65 | import sys 66 | 67 | for line in sys.stdin : 68 | print(convert(line[:-1])) 69 | -------------------------------------------------------------------------------- /http2_tools.py: -------------------------------------------------------------------------------- 1 | """Minimal HTTP/2 frame parsing helpers for the UNIX TCP synthesiser.""" 2 | 3 | from __future__ import annotations 4 | 5 | from dataclasses import dataclass 6 | from typing import List, Optional 7 | 8 | MAX_HTTP2_FRAME_SIZE = (1 << 24) - 1 9 | VALID_STREAM_ZERO_TYPES = {0x4, 0x6, 0x7, 0x8} 10 | 11 | 12 | @dataclass 13 | class HTTP2Header: 14 | length: int 15 | frame_type: int 16 | flags: int 17 | stream_id: int 18 | 19 | 20 | @dataclass 21 | class Chunk: 22 | kind: str # "frame" or "opaque" 23 | data: bytes 24 | header: Optional[HTTP2Header] = None 25 | 26 | 27 | class HTTP2Splitter: 28 | """Accumulate bytes and yield HTTP/2-aligned chunks.""" 29 | 30 | def __init__(self) -> None: 31 | self._buffer = bytearray() 32 | 33 | def push(self, data: bytes) -> None: 34 | if data: 35 | self._buffer.extend(data) 36 | 37 | def pop(self, *, final: bool = False) -> List[Chunk]: 38 | chunks: List[Chunk] = [] 39 | while True: 40 | if len(self._buffer) < 9: 41 | break 42 | header = _parse_header(self._buffer[:9]) 43 | if header is None: 44 | offset = _find_alignment(self._buffer) 45 | if offset is None: 46 | break 47 | if offset: 48 | raw = bytes(self._buffer[:offset]) 49 | del self._buffer[:offset] 50 | chunks.append(Chunk("opaque", raw, None)) 51 | continue 52 | break 53 | total = 9 + header.length 54 | if total > len(self._buffer): 55 | break 56 | raw = bytes(self._buffer[:total]) 57 | del self._buffer[:total] 58 | chunks.append(Chunk("frame", raw, header)) 59 | if final and self._buffer: 60 | chunks.append(Chunk("opaque", bytes(self._buffer), None)) 61 | self._buffer.clear() 62 | return chunks 63 | 64 | def has_pending(self) -> bool: 65 | return bool(self._buffer) 66 | 67 | 68 | def _parse_header(prefix: bytes) -> Optional[HTTP2Header]: 69 | if len(prefix) < 9: 70 | return None 71 | length = int.from_bytes(prefix[:3], "big") 72 | if length > MAX_HTTP2_FRAME_SIZE: 73 | return None 74 | frame_type = prefix[3] 75 | if frame_type > 0x9: 76 | return None 77 | flags = prefix[4] 78 | stream_id = int.from_bytes(prefix[5:9], "big") & 0x7FFFFFFF 79 | if stream_id == 0 and frame_type not in VALID_STREAM_ZERO_TYPES: 80 | return None 81 | return HTTP2Header(length, frame_type, flags, stream_id) 82 | 83 | 84 | def _find_alignment(buffer: bytearray) -> Optional[int]: 85 | for offset in range(1, len(buffer)): 86 | if len(buffer) - offset < 9: 87 | return None 88 | if _parse_header(buffer[offset : offset + 9]) is not None: 89 | return offset 90 | return None 91 | -------------------------------------------------------------------------------- /py_strace2pcap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ tool for converting strace format to synthetic pcap 3 | 1) pip3 install scapy 4 | 2) strace -f -s65535 -o /tmp/straceSample -ttt -T -yy command 5 | 3) py_strace2pcap.py file_to_store.pcap < /tmp/straceSample 6 | 4) wireshark file_to_store.pcap """ 7 | 8 | import inspect 9 | 10 | from scapy.all import RawPcapWriter 11 | 12 | from strace_parser import StraceParser 13 | from strace_parser_2_packet import StraceParser2Packet 14 | from unix_tcp_synth import UnixTCPManager 15 | 16 | 17 | def _write_record(pktdump, record): 18 | """Write a raw packet record, handling scapy compatibility quirks.""" 19 | 20 | if not getattr(pktdump, "header_present", False): 21 | pktdump._write_header(None) 22 | kwargs = { 23 | "sec": record.ts_sec, 24 | "usec": record.ts_usec, 25 | "caplen": len(record.data), 26 | "wirelen": len(record.data), 27 | } 28 | try: 29 | pktdump.write_packet(record.data, **kwargs) 30 | except TypeError: 31 | # Older scapy releases expect the linktype positional argument. 32 | params = inspect.signature(pktdump._write_packet).parameters 33 | if "linktype" in params: 34 | kwargs["linktype"] = getattr(pktdump, "linktype", None) 35 | pktdump._write_packet(record.data, **kwargs) 36 | 37 | 38 | 39 | 40 | def _iterate_events(strace_parser, stream): 41 | for line in stream: 42 | event = strace_parser.process(line) 43 | while event: 44 | yield event 45 | if strace_parser.has_split_cache(): 46 | event = strace_parser.get_split_cache() 47 | else: 48 | break 49 | 50 | 51 | if __name__ == '__main__': 52 | import argparse 53 | import sys 54 | 55 | parser = argparse.ArgumentParser(description='Convert strace output to PCAP') 56 | parser.add_argument('pcap_filename') 57 | 58 | def _add_boolean_flag(flag: str, *, default: bool, enable_help: str, disable_help: str) -> None: 59 | dest = flag.lstrip('-').replace('-', '_') 60 | if hasattr(argparse, 'BooleanOptionalAction'): 61 | parser.add_argument( 62 | flag, 63 | action=argparse.BooleanOptionalAction, 64 | default=default, 65 | help=enable_help, 66 | ) 67 | else: # pragma: no cover - Python <3.9 fallback 68 | group = parser.add_mutually_exclusive_group() 69 | group.add_argument(flag, dest=dest, action='store_true', help=enable_help) 70 | group.add_argument(f"--no-{flag.lstrip('-')}", dest=dest, action='store_false', help=disable_help) 71 | parser.set_defaults(**{dest: default}) 72 | 73 | _add_boolean_flag( 74 | '--capture-unix-socket', 75 | default=False, 76 | enable_help='Enable synthetic TCP capture for AF_UNIX stream sockets', 77 | disable_help='Disable synthetic TCP capture for AF_UNIX stream sockets', 78 | ) 79 | _add_boolean_flag( 80 | '--capture-net', 81 | default=True, 82 | enable_help='Enable capture of AF_INET/AF_INET6 sockets (default: enabled)', 83 | disable_help='Disable capture of AF_INET/AF_INET6 sockets', 84 | ) 85 | parser.add_argument( 86 | '--seed-http2', 87 | action='store_true', 88 | default=False, 89 | help='Seed UNIX TCP flows with the HTTP/2 client preface and SETTINGS frames', 90 | ) 91 | parser.add_argument( 92 | '--seed-grpc', 93 | action='store_true', 94 | default=False, 95 | help='Seed UNIX TCP flows with a gRPC HEADERS frame when evidence is observed (requires --seed-http2)', 96 | ) 97 | 98 | args = parser.parse_args() 99 | 100 | if args.seed_grpc and not args.seed_http2: 101 | parser.error('--seed-grpc requires --seed-http2') 102 | 103 | linktype_value = 1 # DLT_EN10MB 104 | inet_linktype = 'ether' 105 | 106 | pktdump = RawPcapWriter(args.pcap_filename, linktype=linktype_value) 107 | 108 | strace_parser = StraceParser() 109 | inet_packetizer = StraceParser2Packet() if args.capture_net else None 110 | unix_manager = None 111 | if args.capture_unix_socket: 112 | unix_manager = UnixTCPManager( 113 | seed_http2=args.seed_http2, 114 | seed_grpc=args.seed_grpc, 115 | ) 116 | 117 | for event in _iterate_events(strace_parser, sys.stdin): 118 | if not event: 119 | continue 120 | protocol = event.get('protocol') 121 | if protocol and protocol.startswith('UNIX'): 122 | if not unix_manager: 123 | continue 124 | if protocol != 'UNIX-STREAM': 125 | continue 126 | for record in unix_manager.handle_event(event): 127 | _write_record(pktdump, record) 128 | continue 129 | if not inet_packetizer: 130 | continue 131 | packet = inet_packetizer.process(event) 132 | if packet: 133 | pktdump.write(packet) 134 | 135 | if unix_manager: 136 | for record in unix_manager.flush(): 137 | _write_record(pktdump, record) 138 | -------------------------------------------------------------------------------- /tests/test_unix_tcp.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import subprocess 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) 7 | 8 | from grpc_seed import GRPC_HEADERS_FRAME, HTTP2_CLIENT_SEED, HTTP2_SETTINGS_ACK_FRAME # type: ignore # pylint: disable=import-error 9 | 10 | TCP_FLAG_PSH = 0x08 11 | TCP_FLAG_FIN = 0x01 12 | 13 | 14 | def _escape(data: bytes) -> str: 15 | return ''.join('\\x{:02x}'.format(b) for b in data) 16 | 17 | 18 | def _run_converter(tmp_path: Path, name: str, lines, *extra_args) -> Path: 19 | pcap_path = tmp_path / name 20 | cmd = [ 21 | sys.executable, 22 | 'py_strace2pcap.py', 23 | '--capture-unix-socket', 24 | '--no-capture-net', 25 | *extra_args, 26 | str(pcap_path), 27 | ] 28 | payload = '\n'.join(lines) + '\n' 29 | subprocess.run(cmd, input=payload, text=True, check=True) 30 | return pcap_path 31 | 32 | 33 | def _read_pcap(path: Path): 34 | packets = [] 35 | with path.open('rb') as fh: 36 | header = fh.read(24) 37 | magic, _, _, _, _, _, network = struct.unpack('> 4) * 4 53 | return tcp_offset, data_offset 54 | 55 | 56 | def _tcp_flags(pkt): 57 | tcp_offset, _ = _tcp_header_slice(pkt) 58 | return struct.unpack('!H', pkt[tcp_offset + 12:tcp_offset + 14])[0] & 0x01FF 59 | 60 | 61 | def _tcp_ports(pkt): 62 | tcp_offset, _ = _tcp_header_slice(pkt) 63 | return struct.unpack('!HH', pkt[tcp_offset:tcp_offset + 4]) 64 | 65 | 66 | def _tcp_payload(pkt): 67 | tcp_offset, data_offset = _tcp_header_slice(pkt) 68 | return pkt[tcp_offset + data_offset:] 69 | 70 | 71 | def _tcp_checksum(pkt): 72 | tcp_offset, _ = _tcp_header_slice(pkt) 73 | return struct.unpack('!H', pkt[tcp_offset + 16:tcp_offset + 18])[0] 74 | 75 | 76 | def test_frame_alignment_and_close(tmp_path): 77 | client_frame = b'\x00\x00\x05\x00\x01\x00\x00\x00\x01HELLO' 78 | server_frame = b'\x00\x00\x03\x00\x01\x00\x00\x00\x01ACK' 79 | lines = [ 80 | f"1234 1760606087.10 write(54, \"{_escape(client_frame[:5])}\", 5) = 5", 81 | f"1234 1760606087.10 write(54, \"{_escape(client_frame[5:])}\", {len(client_frame) - 5}) = {len(client_frame) - 5}", 82 | f"1234 1760606087.10 read(54, \"{_escape(server_frame)}\", {len(server_frame)}) = {len(server_frame)}", 83 | "1234 1760606087.10 close(54) = 0", 84 | ] 85 | pcap_path = _run_converter(tmp_path, 'aligned.pcap', lines) 86 | packets = _read_pcap(pcap_path) 87 | data_packets = [pkt for _, _, pkt in packets if _tcp_flags(pkt) & TCP_FLAG_PSH] 88 | assert any(_tcp_payload(pkt) == client_frame for pkt in data_packets) 89 | assert any(_tcp_payload(pkt) == server_frame for pkt in data_packets) 90 | assert any(_tcp_flags(pkt) & TCP_FLAG_FIN for _, _, pkt in packets) 91 | assert _tcp_checksum(data_packets[0]) != 0 92 | 93 | 94 | def test_seed_http2(tmp_path): 95 | server_frame = b'\x00\x00\x02\x00\x00\x00\x00\x00\x01OK' 96 | lines = [ 97 | f"4321 1760606087.20 read(77, \"{_escape(server_frame)}\", {len(server_frame)}) = {len(server_frame)}", 98 | "4321 1760606087.20 close(77) = 0", 99 | ] 100 | pcap_path = _run_converter(tmp_path, 'seed_http2.pcap', lines, '--seed-http2') 101 | payloads = [_tcp_payload(pkt) for _, _, pkt in _read_pcap(pcap_path) if _tcp_flags(pkt) & TCP_FLAG_PSH] 102 | assert HTTP2_CLIENT_SEED in payloads 103 | assert HTTP2_SETTINGS_ACK_FRAME in payloads 104 | assert server_frame in payloads 105 | 106 | 107 | def test_seed_grpc_auto(tmp_path): 108 | grpc_data = b'\x00\x00\x05\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00' 109 | lines = [ 110 | f"7777 1760606087.30 write(31, \"{_escape(grpc_data)}\", {len(grpc_data)}) = {len(grpc_data)}", 111 | "7777 1760606087.30 close(31) = 0", 112 | ] 113 | pcap_path = _run_converter(tmp_path, 'seed_grpc.pcap', lines, '--seed-http2', '--seed-grpc') 114 | payloads = [_tcp_payload(pkt) for _, _, pkt in _read_pcap(pcap_path) if _tcp_flags(pkt) & TCP_FLAG_PSH] 115 | assert GRPC_HEADERS_FRAME in payloads 116 | assert grpc_data in payloads 117 | 118 | 119 | def test_residual_flush(tmp_path): 120 | payload = b'PARTIAL' 121 | lines = [ 122 | f"5555 1760606087.40 write(22, \"{_escape(payload)}\", {len(payload)}) = {len(payload)}", 123 | "5555 1760606087.40 close(22) = 0", 124 | ] 125 | pcap_path = _run_converter(tmp_path, 'residual.pcap', lines) 126 | payloads = [_tcp_payload(pkt) for _, _, pkt in _read_pcap(pcap_path) if _tcp_flags(pkt) & TCP_FLAG_PSH] 127 | assert payload in payloads 128 | 129 | 130 | def test_retval_limits_payload(tmp_path): 131 | write_payload = b'ABCDEF' 132 | read_payload = b'GHIJKL' 133 | lines = [ 134 | f"2024 1760606087.45 write(33, \"{_escape(write_payload)}\", {len(write_payload)}) = 3", 135 | f"2024 1760606087.45 read(33, \"{_escape(read_payload)}\", {len(read_payload)}) = 2", 136 | "2024 1760606087.45 close(33) = 0", 137 | ] 138 | pcap_path = _run_converter(tmp_path, 'retval.pcap', lines) 139 | packets = [pkt for _, _, pkt in _read_pcap(pcap_path) if _tcp_flags(pkt) & TCP_FLAG_PSH] 140 | client_payloads = [_tcp_payload(pkt) for pkt in packets if _tcp_ports(pkt)[1] == 50051] 141 | server_payloads = [_tcp_payload(pkt) for pkt in packets if _tcp_ports(pkt)[0] == 50051] 142 | assert client_payloads == [b'ABC'] 143 | assert any(payload == b'GH' for payload in server_payloads) 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # py_strace2pcap 2 | convert specific strace output file to pcap using scapy python library 3 | 4 | # idea behind: 5 | It would be great if strace could directly write pcap, but it's not that modular to support custom formating. 6 | It would be great if wireshark library could be safely used from strace module to produce pcap output, 7 | but 3rd party usage of libwireshark is not [encouraged](https://stackoverflow.com/questions/10308127/using-libwireshark-to-get-wireshark-functionality-programmatically). 8 | What is achievable is to make a tool that can read strace output file and generate pcap file - then one can read pcap with wireshark, tcpdump, tshark. 9 | 10 | # purpose : 11 | I wanted to check some non encrypted traffic protocol that use binary encoding - it was not observable in ascii. 12 | It was on production, process is already running. It was in namespace, and not the only process that is running in that namespace. 13 | I was able to strace the process, I knew that I have all the bytes (above tcp layer), and I wanted 14 | to dissect that with tshark to get details of communication. This tool is created for that purpose. 15 | 16 | ![example wireshark](https://github.com/comboshreddies/py-strace2pcap/blob/main/images/mongo_find.png?raw=true) 17 | 18 | 19 | # setup 20 | install needed python module 21 | ```console 22 | pip3 install scapy 23 | ``` 24 | or 25 | ```console 26 | pip3 install -r requirements.txt 27 | ``` 28 | 29 | note: you might want to use python venv if you don't want scappy installed globally 30 | 31 | # get strace file in specific format 32 | start strace 33 | ```console 34 | strace -f -s655350 -o /tmp/straceSample -ttt -T -yy -xx command 35 | ``` 36 | or for already running process 37 | ``` console 38 | strace -f -s655350 -o /tmp/straceSample -ttt -T -yy -xx -p 39 | ``` 40 | 41 | # run py\_strace2pcap.py 42 | start conversion from strace to pcap 43 | ```console 44 | py_strace2pcap.py file_to_store.pcap < /tmp/straceSample 45 | ``` 46 | 47 | ## UNIX domain sockets 48 | 49 | By default the converter emits the original AF_INET/AF_INET6 packets only. 50 | Pass `--capture-unix-socket` to synthesise Ethernet/IP/TCP flows for UNIX 51 | stream sockets. Each inode is mapped to a deterministic 5-tuple 52 | (`10.0.0.X:ephemeral → 10.0.1.X:50051`) so Wireshark can decode HTTP/2 and 53 | gRPC traffic. The synthesiser emits a single handshake per flow, keeps FIN/ACK 54 | pairs in sync with the observed `close()`/`shutdown()`, and buffers writes 55 | until complete HTTP/2 frames are available. During resynchronisation any stray 56 | bytes are forwarded as "opaque" TCP payloads so nothing is discarded. Disable 57 | AF_INET/6 processing with `--no-capture-net` when you only need the UNIX side 58 | of the conversation. 59 | 60 | ```console 61 | py_strace2pcap.py --capture-unix-socket --no-capture-net output.pcap < trace.log 62 | ``` 63 | 64 | Two optional seeders help Wireshark recover missing protocol prefaces when the 65 | capture starts mid-stream: 66 | 67 | * `--seed-http2` injects the HTTP/2 client preface and minimal SETTINGS/ACK 68 | frames before the first payload so Wireshark enables the HTTP/2 dissector 69 | automatically. 70 | * `--seed-grpc` (requires `--seed-http2`) adds a placeholder gRPC HEADERS frame 71 | once real traffic provides evidence (HTTP/2 DATA frames with the gRPC 72 | five-byte prefix or HEADERS mentioning `content-type: application/grpc`). 73 | 74 | All synthesised TCP segments carry valid IPv4/TCP checksums. Per-direction byte 75 | accounting is logged to confirm that the emitted payload matches the strace 76 | return values. 77 | 78 | ## Link-layer options 79 | 80 | When UNIX TCP synthesis is enabled the converter always writes Ethernet frames 81 | (DLT_EN10MB) so both UNIX and INET traffic share the same link-layer without 82 | needing extra flags. 83 | 84 | TLS-encrypted payloads remain opaque because the plaintext is not available in 85 | the strace logs. 86 | 87 | # play with your pcap 88 | read network traffic from strace with wireshark, tshark, or tcpdump 89 | ```console 90 | wireshark file_to_store.pcap 91 | ``` 92 | 93 | # helpers 94 | 1) there is example straceSample in example directory, along with example straceSample.pcap 95 | 96 | 2) when protocol is not recognized in wireshark/tshark, do use decode packet on tcp payload (check screenshots in images directory) 97 | or specify dissector in commandline while running tshark/wireshark 98 | texample below is for mongo protocol 99 | ```console 100 | wireshark ./example/straceSample.pcap -d tcp.port==27017,mongo 101 | ``` 102 | 3) if program observed with strace logs too much operations, and file becomes too large 103 | try to add **-e trace=network** to strace command, to isolate just network traffic 104 | 105 | 4) strace data encodings in pcap: 106 | 107 | * PID from strace file is encoded in eth.addr (src or dst depending on direction of a packet). PID is encoded as a decimal within hex/byte of ethernet mac, so for PID 123456 you should see mac address 00:00:00:12:34:56 108 | 109 | * FD (file descriptor) from strace is encoded in vlan ID (802.1q), for example FD 17 is encoded as VlanID 17 110 | 111 | * session (unique fd session, as same fd can be closed an opened more than once) is encoded in other eth.addr (src or dst, other than PID, depending on direction of a packet) at lower part of mac starting from eth.addr[5] 112 | 113 | * system call is encoded along with session on eth.addr[1] 114 | * read = 1 115 | * write = 2 116 | * sendmsg = 3 117 | * recvmsg = 4 118 | * recvfrom = 5 119 | * sendto = 6 120 | 121 | example filter for PID 654321 and FD 7 : eth.addr == 00:00:00:65:43:21 && vlan.id == 7 122 | 123 | 5) if you like to see old and familiar, default, strace output, there is a tool in tools directory that will convert -xx format to generic format 124 | ``` console 125 | ./tools/strace_convert/xx2generic.py < StraceOutFile_with_-xx > StraceOutFile_ascii_readable 126 | ``` 127 | 128 | 6) if you want to have a single command for catching just observed pid or command pcap file do use script that wraps strace execution: 129 | ``` console 130 | ./strace2pcap.sh /tmp/sameFile.pcap "strace args" 131 | ``` 132 | for example: 133 | ``` console 134 | ./strace2pcap.sh /tmp/OUT2.pcap "curl http://www.google.com" 135 | ``` 136 | or: 137 | ``` console 138 | ./strace2pcap.sh /tmp/OUT2.pcap "-p some_pid_of_interest" 139 | ``` 140 | note: to run ./strace2pcap.sh you will need scapy python module installed 141 | 142 | 7) if you want to pipe pcap content to wireshark or tcpdump use: 143 | ``` console 144 | ./strace2pcap-pipe.sh /tmp/OUT2.pcap "curl http://www.github.com" | tcpdump -A -r - 145 | ``` 146 | 147 | 8) wireshark can't show more than 256 bytes of some protocol payload, for that reason I've created cli tshark based renders, so one can take payload of a protocl (like es query, or statsd message) from recorder strace, ie converted pcap file. look at tools/cli_tshark_protocol_render 148 | 149 | # reporting issues 150 | please send strace command you've used and strace output 151 | 152 | -------------------------------------------------------------------------------- /strace_parser_2_packet.py: -------------------------------------------------------------------------------- 1 | """Parse strace line to scapy packets.""" 2 | 3 | from scapy.all import Ether, Dot1Q, IP, IPv6, TCP, UDP, Raw 4 | 5 | 6 | class StraceParser2Packet(): 7 | """ Strace Parser to scapy Packet """ 8 | 9 | op_encode = {} 10 | op_encode['read'] = 1 11 | op_encode['write'] = 2 12 | op_encode['sendmsg'] = 3 13 | op_encode['recvmsg'] = 4 14 | op_encode['recvfrom'] = 5 15 | op_encode['sendto'] = 6 16 | op_encode['close'] = 7 17 | 18 | def __init__(self, *, linktype: str = 'ether'): 19 | self.sequence = {} 20 | self.linktype = linktype 21 | self.ip_id = 0 22 | 23 | def has_split_cache(self): 24 | """ cheks is there split cache, but not implemented so False """ 25 | return False 26 | 27 | def get_split_cache(self): 28 | """return next cached packet""" 29 | return False 30 | 31 | def encode_decimal2mac(self, enc): 32 | """ encode int to mac, we're econding pid , fd , steram and such """ 33 | mac6 = (enc) % 100 34 | mac5 = int(enc / 100) % 100 35 | mac4 = int(enc / 10000) % 100 36 | mac3 = int(enc / 1000000) % 100 37 | mac2 = int(enc / 100000000) % 100 38 | mac1 = int(enc / 10000000000) 39 | return f"{mac1:#02d}:{mac2:#02d}:{mac3:#02d}:{mac4:#02d}:{mac5:#02d}:{mac6:#02d}" 40 | 41 | def generate_sequence(self, c): 42 | """ generate sequence """ 43 | return (c['fd'] * 100 + c['pid'] * 10000 + c['session']) % 4294967296 44 | 45 | def generate_sequence_key(self, c): 46 | """ generate sequence_key """ 47 | return f"{c['source_ip']}:{c['source_port']}_{c['destination_ip']}:\ 48 | {c['destination_port']}_{c['pid']}:{c['fd']}{c['session']}" 49 | 50 | def _next_ip_id(self): 51 | self.ip_id = (self.ip_id + 1) % 65536 52 | return self.ip_id 53 | 54 | def generate_tcp_packet(self, p): 55 | """ generate tcp packet """ 56 | seq_key = self.generate_sequence_key(p) 57 | if seq_key not in self.sequence: 58 | self.sequence[seq_key] = self.generate_sequence(p) 59 | payload = p['payload'] 60 | base_layer = None 61 | if self.linktype == 'ether': 62 | if p['direction_out']: 63 | source_mac = self.encode_decimal2mac(p['pid']) 64 | destination_mac = self.encode_decimal2mac( 65 | 100000000 * self.op_encode[p['syscall']] + p['session']) 66 | else: 67 | destination_mac = self.encode_decimal2mac(p['pid']) 68 | source_mac = self.encode_decimal2mac( 69 | 100000000 * self.op_encode[p['syscall']] + p['session']) 70 | base_layer = Ether(src=source_mac, dst=destination_mac) / Dot1Q(vlan=p['fd']) 71 | ip_layer = IP(src=p['source_ip'], dst=p['destination_ip']) 72 | if self.linktype != 'ether': 73 | ip_layer.id = self._next_ip_id() 74 | tcp_layer = TCP( 75 | flags='PA', sport=p['source_port'], dport=p['destination_port'], 76 | seq=self.sequence[seq_key]) 77 | packet = ip_layer / tcp_layer 78 | if payload: 79 | packet = packet / Raw(payload) 80 | packet.time = p['time'] 81 | if base_layer is not None: 82 | packet = base_layer / packet 83 | packet.time = p['time'] 84 | packet[Ether].time = p['time'] 85 | self.sequence[seq_key] += len(payload) 86 | return packet 87 | 88 | def generate_udp_packet(self, p): 89 | """ generate udp packet """ 90 | payload = p['payload'] 91 | base_layer = None 92 | if self.linktype == 'ether': 93 | if p['direction_out']: 94 | source_mac = self.encode_decimal2mac(p['pid']) 95 | destination_mac = self.encode_decimal2mac( 96 | 100000000 * self.op_encode[p['syscall']] + p['session']) 97 | else: 98 | destination_mac = self.encode_decimal2mac(p['pid']) 99 | source_mac = self.encode_decimal2mac( 100 | 100000000 * self.op_encode[p['syscall']] + p['session']) 101 | base_layer = Ether(src=source_mac, dst=destination_mac) / Dot1Q(vlan=p['fd']) 102 | ip_layer = IP(src=p['source_ip'], dst=p['destination_ip']) 103 | if self.linktype != 'ether': 104 | ip_layer.id = self._next_ip_id() 105 | udp_layer = UDP(sport=p['source_port'], dport=p['destination_port']) 106 | packet = ip_layer / udp_layer 107 | if payload: 108 | packet = packet / Raw(payload) 109 | packet.time = p['time'] 110 | if base_layer is not None: 111 | packet = base_layer / packet 112 | packet.time = p['time'] 113 | packet[Ether].time = p['time'] 114 | return packet 115 | 116 | def generate_tcp_packet_v6(self, src_mac, dst_mac, vlan, p): 117 | """ generate tcp packet """ 118 | seq_key = self.generate_sequence_key(p) 119 | if seq_key not in self.sequence: 120 | self.sequence[seq_key] = self.generate_sequence(p) 121 | payload = p['payload'] 122 | base_layer = None 123 | if self.linktype == 'ether': 124 | base_layer = Ether(src=src_mac, dst=dst_mac) / Dot1Q(vlan=vlan) 125 | ipv6_layer = IPv6(src=p['source_ip'], dst=p['destination_ip']) 126 | tcp_layer = TCP( 127 | flags='PA', sport=p['source_port'], dport=p['destination_port'], 128 | seq=self.sequence[seq_key]) 129 | packet = ipv6_layer / tcp_layer 130 | if payload: 131 | packet = packet / Raw(payload) 132 | packet.time = p['time'] 133 | if base_layer is not None: 134 | packet = base_layer / packet 135 | packet.time = p['time'] 136 | packet[Ether].time = p['time'] 137 | self.sequence[seq_key] = (len(payload) + self.sequence[seq_key]) % 4294967296 138 | return packet 139 | 140 | def generate_udp_packet_v6(self, src_mac, dst_mac, vlan, p): 141 | """ generate udp packet """ 142 | payload = p['payload'] 143 | base_layer = None 144 | if self.linktype == 'ether': 145 | base_layer = Ether(src=src_mac, dst=dst_mac) / Dot1Q(vlan=vlan) 146 | ipv6_layer = IPv6(src=p['source_ip'], dst=p['destination_ip']) 147 | udp_layer = UDP(sport=p['source_port'], dport=p['destination_port']) 148 | packet = ipv6_layer / udp_layer 149 | if payload: 150 | packet = packet / Raw(payload) 151 | packet.time = p['time'] 152 | if base_layer is not None: 153 | packet = base_layer / packet 154 | packet.time = p['time'] 155 | packet[Ether].time = p['time'] 156 | return packet 157 | 158 | def generate_pcap_packet(self, c): 159 | """ from parsed content generate pcap packet """ 160 | if not c: 161 | return False 162 | 163 | if c.get('protocol', '').startswith('UNIX'): 164 | return False 165 | 166 | if c.get('protocol') == "TCP": 167 | return self.generate_tcp_packet(c) 168 | if c.get('protocol') == "UDP": 169 | return self.generate_udp_packet(c) 170 | if c.get('protocol') == "TCPv6": 171 | if c['direction_out']: 172 | source_mac = self.encode_decimal2mac(c['pid']) 173 | destination_mac = self.encode_decimal2mac( 174 | 100000000 * self.op_encode[c['syscall']] + c['session']) 175 | else: 176 | destination_mac = self.encode_decimal2mac(c['pid']) 177 | source_mac = self.encode_decimal2mac( 178 | 100000000 * self.op_encode[c['syscall']] + c['session']) 179 | fd_vlan = c['fd'] 180 | return self.generate_tcp_packet_v6(source_mac, destination_mac, fd_vlan, c) 181 | if c.get('protocol') == "UDPv6": 182 | if c['direction_out']: 183 | source_mac = self.encode_decimal2mac(c['pid']) 184 | destination_mac = self.encode_decimal2mac( 185 | 100000000 * self.op_encode[c['syscall']] + c['session']) 186 | else: 187 | destination_mac = self.encode_decimal2mac(c['pid']) 188 | source_mac = self.encode_decimal2mac( 189 | 100000000 * self.op_encode[c['syscall']] + c['session']) 190 | fd_vlan = c['fd'] 191 | return self.generate_udp_packet_v6(source_mac, destination_mac, fd_vlan, c) 192 | 193 | return False 194 | 195 | def process(self, c): 196 | """ call to reserved process method, used by higher level generator """ 197 | return self.generate_pcap_packet(c) 198 | -------------------------------------------------------------------------------- /unix_tcp_synth.py: -------------------------------------------------------------------------------- 1 | """Synthetic TCP/IP emitter for UNIX stream sockets.""" 2 | 3 | from __future__ import annotations 4 | 5 | import ipaddress 6 | import logging 7 | from dataclasses import dataclass, field 8 | from typing import Dict, List, Optional, Tuple 9 | 10 | from grpc_seed import ( 11 | GRPC_HEADERS_FRAME, 12 | HTTP2_CLIENT_SEED, 13 | HTTP2_SETTINGS_ACK_FRAME, 14 | frame_has_grpc_evidence, 15 | ) 16 | from http2_tools import Chunk, HTTP2Splitter 17 | from tcp_utils import ( 18 | ETHERTYPE_IPV4, 19 | MAX_TCP_PAYLOAD, 20 | TCP_FLAG_ACK, 21 | TCP_FLAG_FIN, 22 | TCP_FLAG_PSH, 23 | TCP_FLAG_SYN, 24 | build_ipv4_header, 25 | build_tcp_header, 26 | ) 27 | 28 | WRITE_SYSCALLS = {"write", "sendmsg", "sendto"} 29 | READ_SYSCALLS = {"read", "recvmsg", "recvfrom"} 30 | STATE_SYSCALLS = {"close", "shutdown"} 31 | 32 | HANDSHAKE_DELTA = 0.0001 33 | TIMESTAMP_EPSILON = 1e-6 34 | 35 | 36 | @dataclass 37 | class PacketRecord: 38 | ts_sec: int 39 | ts_usec: int 40 | data: bytes 41 | 42 | 43 | @dataclass 44 | class SideState: 45 | splitter: HTTP2Splitter = field(default_factory=HTTP2Splitter) 46 | first_ts: Optional[float] = None 47 | last_ts: Optional[float] = None 48 | in_bytes: int = 0 49 | out_bytes: int = 0 50 | closed: bool = False 51 | accounted: bool = False 52 | 53 | 54 | @dataclass 55 | class Endpoint: 56 | mac: bytes 57 | ip: str 58 | port: int 59 | isn: int 60 | 61 | 62 | @dataclass 63 | class Flow: 64 | inode: Optional[int] 65 | index: int 66 | client: Endpoint 67 | server: Endpoint 68 | next_seq: Dict[str, int] 69 | handshake_done: bool = False 70 | seed_http2_done: bool = False 71 | seed_grpc_done: bool = False 72 | grpc_evidence: bool = False 73 | owners: Dict[Tuple[int, int, int], str] = field(default_factory=dict) 74 | sides: Dict[str, SideState] = field(init=False) 75 | 76 | def __post_init__(self) -> None: 77 | self.sides = {"A": SideState(), "B": SideState()} 78 | 79 | def side_for(self, owner: Tuple[int, int, int]) -> str: 80 | side = self.owners.get(owner) 81 | if side: 82 | return side 83 | assigned = set(self.owners.values()) 84 | side = "A" if "A" not in assigned else "B" 85 | self.owners[owner] = side 86 | return side 87 | 88 | def peer(self, side: str) -> str: 89 | return "B" if side == "A" else "A" 90 | 91 | 92 | class UnixTCPManager: 93 | """Emit Ethernet/IP/TCP packets for UNIX stream activity.""" 94 | 95 | def __init__( 96 | self, 97 | *, 98 | base_src_ip: str = "10.0.0.0", 99 | base_dst_ip: str = "10.0.1.0", 100 | base_sport: int = 30000, 101 | dst_port: int = 50051, 102 | seed_http2: bool = False, 103 | seed_grpc: bool = False, 104 | logger: Optional[logging.Logger] = None, 105 | ) -> None: 106 | self._flows: Dict[Tuple, Flow] = {} 107 | self._base_src_ip = ipaddress.IPv4Address(base_src_ip) 108 | self._base_dst_ip = ipaddress.IPv4Address(base_dst_ip) 109 | self._base_sport = base_sport 110 | self._dst_port = dst_port 111 | self._seed_http2 = seed_http2 112 | self._seed_grpc = seed_grpc 113 | self._logger = logger or logging.getLogger(__name__) 114 | self._next_index = 0 115 | self._ip_id = 0 116 | 117 | def handle_event(self, event: Dict) -> List[PacketRecord]: 118 | if not event or event.get("protocol") != "UNIX-STREAM": 119 | return [] 120 | syscall = event.get("syscall", "") 121 | result = event.get("result") 122 | payload: bytes = event.get("payload") or b"" 123 | timestamp = float(event.get("time", 0.0)) 124 | owner_key = ( 125 | int(event.get("pid") or 0), 126 | int(event.get("fd") or 0), 127 | int(event.get("session") or 0), 128 | ) 129 | flow = self._flow_for_event(event) 130 | side = flow.side_for(owner_key) 131 | 132 | if syscall in WRITE_SYSCALLS: 133 | return self._queue_bytes(flow, side, result, payload, timestamp) 134 | if syscall in READ_SYSCALLS: 135 | return self._queue_bytes(flow, flow.peer(side), result, payload, timestamp) 136 | if syscall in STATE_SYSCALLS: 137 | return self._handle_close(flow, side, timestamp, syscall, result) 138 | return [] 139 | 140 | def flush(self) -> List[PacketRecord]: 141 | packets: List[PacketRecord] = [] 142 | for flow in list(self._flows.values()): 143 | packets.extend(self._drain_side(flow, "A", final=True)) 144 | packets.extend(self._drain_side(flow, "B", final=True)) 145 | return packets 146 | 147 | # ------------------------------------------------------------------ 148 | def _flow_for_event(self, event: Dict) -> Flow: 149 | inode = event.get("inode") 150 | if isinstance(inode, int): 151 | key = ("inode", inode) 152 | else: 153 | key = ( 154 | "anon", 155 | event.get("protocol"), 156 | int(event.get("pid") or 0), 157 | int(event.get("fd") or 0), 158 | int(event.get("session") or 0), 159 | ) 160 | flow = self._flows.get(key) 161 | if flow is None: 162 | flow = self._create_flow(inode if isinstance(inode, int) else None) 163 | self._flows[key] = flow 164 | return flow 165 | 166 | def _create_flow(self, inode: Optional[int]) -> Flow: 167 | index = self._next_index 168 | self._next_index += 1 169 | client_ip = str(self._base_src_ip + index + 1) 170 | server_ip = str(self._base_dst_ip + index + 1) 171 | client_mac = self._mac_for(index, client=True) 172 | server_mac = self._mac_for(index, client=False) 173 | inode_seed = inode if inode is not None else 100000 + index 174 | client_port = self._base_sport + (inode_seed % 10000) 175 | client_isn = 0x10000000 + inode_seed 176 | server_isn = 0x20000000 + inode_seed 177 | return Flow( 178 | inode=inode, 179 | index=index, 180 | client=Endpoint(client_mac, client_ip, client_port, client_isn), 181 | server=Endpoint(server_mac, server_ip, self._dst_port, server_isn), 182 | next_seq={"A": client_isn, "B": server_isn}, 183 | ) 184 | 185 | def _mac_for(self, index: int, *, client: bool) -> bytes: 186 | base = 0x020000000000 if client else 0x020000000100 187 | return (base + index).to_bytes(6, "big") 188 | 189 | # ------------------------------------------------------------------ 190 | def _queue_bytes( 191 | self, 192 | flow: Flow, 193 | side: str, 194 | result: Optional[int], 195 | payload: bytes, 196 | timestamp: float, 197 | ) -> List[PacketRecord]: 198 | if not isinstance(result, int) or result <= 0: 199 | return [] 200 | chunk = payload[:result] 201 | if not chunk: 202 | return [] 203 | state = flow.sides[side] 204 | if state.first_ts is None: 205 | state.first_ts = timestamp 206 | state.last_ts = timestamp 207 | state.splitter.push(chunk) 208 | state.in_bytes += len(chunk) 209 | return self._drain_side(flow, side, final=False) 210 | 211 | def _handle_close( 212 | self, 213 | flow: Flow, 214 | side: str, 215 | timestamp: float, 216 | syscall: str, 217 | result: Optional[int], 218 | ) -> List[PacketRecord]: 219 | if syscall == "shutdown" and result not in (0, None): 220 | return [] 221 | flow.sides[side].last_ts = timestamp 222 | packets = self._drain_side(flow, side, final=True) 223 | peer = flow.peer(side) 224 | flow.sides[peer].last_ts = timestamp 225 | packets.extend(self._drain_side(flow, peer, final=True)) 226 | packets.extend(self._ensure_warmup(flow, timestamp)) 227 | state = flow.sides[side] 228 | if not state.closed: 229 | seq = flow.next_seq[side] 230 | ack = flow.next_seq[peer] 231 | packets.append(self._build_record(flow, side, seq, ack, TCP_FLAG_FIN | TCP_FLAG_ACK, b"", timestamp)) 232 | flow.next_seq[side] += 1 233 | state.closed = True 234 | ack_time = max(timestamp + HANDSHAKE_DELTA / 2, timestamp) 235 | packets.append( 236 | self._build_record(flow, peer, flow.next_seq[peer], flow.next_seq[side], TCP_FLAG_ACK, b"", ack_time) 237 | ) 238 | return packets 239 | 240 | def _drain_side(self, flow: Flow, side: str, *, final: bool) -> List[PacketRecord]: 241 | state = flow.sides[side] 242 | chunks = state.splitter.pop(final=final) 243 | if not chunks: 244 | if final: 245 | self._log_accounting(flow, side) 246 | return [] 247 | packets: List[PacketRecord] = [] 248 | start_ts = state.first_ts if state.first_ts is not None else (state.last_ts or 0.0) 249 | for idx, chunk in enumerate(chunks): 250 | ts = start_ts + idx * TIMESTAMP_EPSILON 251 | packets.extend(self._emit_chunk(flow, side, chunk, ts)) 252 | state.first_ts = start_ts + len(chunks) * TIMESTAMP_EPSILON if state.splitter.has_pending() else None 253 | if final: 254 | self._log_accounting(flow, side) 255 | return packets 256 | 257 | def _emit_chunk(self, flow: Flow, side: str, chunk: Chunk, ts: float) -> List[PacketRecord]: 258 | payload = chunk.data 259 | if not payload: 260 | return self._ensure_warmup(flow, ts) 261 | if chunk.kind == "frame" and chunk.header: 262 | frame_payload = payload[9:] 263 | if self._seed_grpc and not flow.seed_grpc_done and frame_has_grpc_evidence(chunk.header.frame_type, frame_payload): 264 | flow.grpc_evidence = True 265 | packets = self._ensure_warmup(flow, ts) 266 | state = flow.sides[side] 267 | peer = flow.peer(side) 268 | seq = flow.next_seq[side] 269 | ack = flow.next_seq[peer] 270 | for offset in range(0, len(payload), MAX_TCP_PAYLOAD): 271 | part = payload[offset : offset + MAX_TCP_PAYLOAD] 272 | ts_part = ts + offset * TIMESTAMP_EPSILON 273 | packets.append( 274 | self._build_record(flow, side, seq + offset, ack, TCP_FLAG_PSH | TCP_FLAG_ACK, part, ts_part) 275 | ) 276 | flow.next_seq[side] += len(payload) 277 | state.out_bytes += len(payload) 278 | if chunk.kind != "frame": 279 | reason = "opaque" if chunk.kind == "opaque" else "trailing" 280 | self._logger.warning("unix flow %s flushed %d %s bytes", flow.inode, len(payload), reason) 281 | return packets 282 | 283 | def _ensure_warmup(self, flow: Flow, ts: float) -> List[PacketRecord]: 284 | packets: List[PacketRecord] = [] 285 | if not flow.handshake_done: 286 | packets.extend(self._emit_handshake(flow, ts)) 287 | if self._seed_http2 and not flow.seed_http2_done: 288 | packets.extend(self._emit_http2_seed(flow, ts)) 289 | if self._seed_grpc and flow.grpc_evidence and not flow.seed_grpc_done: 290 | packets.extend(self._emit_grpc_seed(flow, ts)) 291 | return packets 292 | 293 | def _emit_handshake(self, flow: Flow, ts: float) -> List[PacketRecord]: 294 | base = max(ts - 3 * HANDSHAKE_DELTA, 0.0) 295 | seq_a = flow.next_seq["A"] 296 | seq_b = flow.next_seq["B"] 297 | steps = ( 298 | ("A", seq_a, 0, TCP_FLAG_SYN, base), 299 | ("B", seq_b, seq_a + 1, TCP_FLAG_SYN | TCP_FLAG_ACK, base + HANDSHAKE_DELTA), 300 | ("A", seq_a + 1, seq_b + 1, TCP_FLAG_ACK, base + 2 * HANDSHAKE_DELTA), 301 | ) 302 | packets = [self._build_record(flow, side, seq, ack, flags, b"", when) for side, seq, ack, flags, when in steps] 303 | flow.next_seq["A"] = seq_a + 1 304 | flow.next_seq["B"] = seq_b + 1 305 | flow.handshake_done = True 306 | return packets 307 | 308 | def _emit_http2_seed(self, flow: Flow, ts: float) -> List[PacketRecord]: 309 | schedule = ( 310 | ("A", HTTP2_CLIENT_SEED, 0), 311 | ("B", HTTP2_SETTINGS_ACK_FRAME, 1), 312 | ("A", HTTP2_SETTINGS_ACK_FRAME, 2), 313 | ) 314 | packets: List[PacketRecord] = [] 315 | for side, payload, index in schedule: 316 | send_ts = max(ts - (len(schedule) - 1 - index) * TIMESTAMP_EPSILON, 0.0) 317 | packets.append( 318 | self._build_record( 319 | flow, 320 | side, 321 | flow.next_seq[side], 322 | flow.next_seq[flow.peer(side)], 323 | TCP_FLAG_PSH | TCP_FLAG_ACK, 324 | payload, 325 | send_ts, 326 | ) 327 | ) 328 | flow.next_seq[side] += len(payload) 329 | flow.seed_http2_done = True 330 | return packets 331 | 332 | def _emit_grpc_seed(self, flow: Flow, ts: float) -> List[PacketRecord]: 333 | packets = [ 334 | self._build_record( 335 | flow, 336 | "A", 337 | flow.next_seq["A"], 338 | flow.next_seq["B"], 339 | TCP_FLAG_PSH | TCP_FLAG_ACK, 340 | GRPC_HEADERS_FRAME, 341 | max(ts - TIMESTAMP_EPSILON, 0.0), 342 | ) 343 | ] 344 | flow.next_seq["A"] += len(GRPC_HEADERS_FRAME) 345 | flow.seed_grpc_done = True 346 | return packets 347 | 348 | def _log_accounting(self, flow: Flow, side: str) -> None: 349 | state = flow.sides[side] 350 | if state.accounted: 351 | return 352 | delta = state.in_bytes - state.out_bytes 353 | level = logging.INFO if delta == 0 else logging.WARNING 354 | self._logger.log( 355 | level, 356 | "unix flow %s side %s delivered %d/%d bytes (delta=%d)", 357 | flow.inode, 358 | side, 359 | state.out_bytes, 360 | state.in_bytes, 361 | delta, 362 | ) 363 | state.accounted = True 364 | 365 | def _build_record( 366 | self, 367 | flow: Flow, 368 | side: str, 369 | seq: int, 370 | ack: int, 371 | flags: int, 372 | payload: bytes, 373 | ts: float, 374 | ) -> PacketRecord: 375 | endpoint = flow.client if side == "A" else flow.server 376 | peer = flow.server if side == "A" else flow.client 377 | ethernet = peer.mac + endpoint.mac + ETHERTYPE_IPV4.to_bytes(2, "big") 378 | self._ip_id = (self._ip_id + 1) & 0xFFFF 379 | tcp_header = build_tcp_header(endpoint.ip, peer.ip, endpoint.port, peer.port, seq, ack, flags, payload) 380 | ip_header = build_ipv4_header(endpoint.ip, peer.ip, len(tcp_header) + len(payload), self._ip_id) 381 | packet = ethernet + ip_header + tcp_header + payload 382 | ts_sec, ts_usec = _split_ts(ts) 383 | return PacketRecord(ts_sec=ts_sec, ts_usec=ts_usec, data=packet) 384 | 385 | 386 | def _split_ts(ts: float) -> Tuple[int, int]: 387 | if ts < 0: 388 | ts = 0.0 389 | sec = int(ts) 390 | usec = int(round((ts - sec) * 1_000_000)) 391 | if usec >= 1_000_000: 392 | sec += 1 393 | usec -= 1_000_000 394 | return sec, usec 395 | -------------------------------------------------------------------------------- /strace_parser.py: -------------------------------------------------------------------------------- 1 | """ strace -o /tmp/ -f -yy -ttt -xx -T parser """ 2 | 3 | import re 4 | 5 | 6 | class FileDescriptorTracker(): 7 | """ pid-fd tracker helper class """ 8 | 9 | def __init__(self): 10 | self.fd_track = {} 11 | 12 | def start_track(self, key): 13 | """ start tracking if not tracked """ 14 | if key not in self.fd_track: 15 | self.fd_track[key] = 1 16 | 17 | def increase(self, key): 18 | """ closed fd arrived, increase fd track counter """ 19 | if key in self.fd_track: 20 | self.fd_track[key] += 1 21 | return self.fd_track[key] 22 | return False 23 | 24 | def get(self, key): 25 | """ get current fd track counter """ 26 | if key in self.fd_track: 27 | return self.fd_track[key] 28 | return False 29 | 30 | 31 | class UnfinishedResume(): 32 | """ keep track of unfinished lines """ 33 | 34 | def __init__(self): 35 | self.unfinish_resume = {} 36 | 37 | def store_line(self, key, args): 38 | """ store unfinished line """ 39 | self.unfinish_resume[key] = ' '.join(args[:-2]) 40 | 41 | def reconstruct_resumed(self, key, args): 42 | """ return reconstructed unfinished/resumed line """ 43 | if key in self.unfinish_resume: 44 | # reconstruct strace line 45 | new_line = self.unfinish_resume[key] + '"' + ' '.join(args[4:])[9:] 46 | del self.unfinish_resume[key] 47 | return new_line 48 | return False 49 | 50 | 51 | class StraceParser(): 52 | """ strace parser class """ 53 | 54 | syscalls_all = [ 55 | 'sendto', 'recvfrom', 'recvmsg', 'read', 56 | 'write', 'sendmsg', 'close', 'shutdown'] 57 | protocols = ['TCP', 'UDP', 'TCPv6', 'UDPv6', 'UNIX', 'UNIX-STREAM', 'UNIX-DGRAM'] 58 | protocols_ipv6 = ['TCPv6', 'UDPv6'] 59 | protocols_ipv4 = ['TCP', 'UDP'] 60 | protocols_unix = ['UNIX', 'UNIX-STREAM', 'UNIX-DGRAM'] 61 | 62 | syscalls_format = {} 63 | syscalls_format['single_chunk_payload'] = ['sendto', 'recvfrom', 'read', 'write'] 64 | syscalls_format['vector_payload'] = ['sendmsg', 'recvmsg'] 65 | syscalls_format['state'] = ['close', 'shutdown'] 66 | syscalls_out = ['write', 'sendto', 'sendmsg'] 67 | syscalls_in = ['read', 'recvfrom', 'recvmsg'] 68 | syscalls_broken = ['sendto'] 69 | 70 | # there are more E-messages 71 | nop_results = ['EAGAIN', 'EINPROGRESS', 'EBADF', 'ECONNRESET'] 72 | 73 | split_cache_packet = {} 74 | scapy_max_payload = 65480 75 | 76 | def __init__(self): 77 | self.fd_track = FileDescriptorTracker() 78 | self.syscall_track = UnfinishedResume() 79 | 80 | def has_split_cache(self): 81 | """ checks is there cached packet object """ 82 | if self.split_cache_packet: 83 | return True 84 | return False 85 | 86 | def get_split_cache(self): 87 | """ returns cached packet object """ 88 | full_payload = self.split_cache_packet['payload'] 89 | parsed = dict(self.split_cache_packet) 90 | if len(full_payload) > self.scapy_max_payload: 91 | parsed['payload'] = full_payload[:self.scapy_max_payload] 92 | self.split_cache_packet = dict(parsed) 93 | self.split_cache_packet['payload'] = full_payload[self.scapy_max_payload:] 94 | else: 95 | parsed['payload'] = full_payload 96 | self.split_cache_packet = {} 97 | return parsed 98 | 99 | def is_stop_or_signal_line(self, line_args): 100 | """ is this line with exit and signals """ 101 | if line_args[2] == '+++': 102 | return True 103 | return False 104 | 105 | def is_unwanted_resumed_syscall(self, args): 106 | """ skip unwanted resumed sycalls """ 107 | if args[2] == '<...' and not args[3] in self.syscalls_all: 108 | return True 109 | return False 110 | 111 | def is_unwanted_syscall(self, args): 112 | """ skip unwanted syscalls, arg2 syscall """ 113 | syscall = args[2].split('(')[0] 114 | if args[2] != '<...' and syscall not in self.syscalls_all: 115 | return True 116 | return False 117 | 118 | def is_error_return_code(self, raw_line): 119 | """ if return code is -1 and belongs to nop_results """ 120 | if (len(raw_line.split(')')) > 1 and 121 | len(raw_line.split(')')[1].split(' ')) > 3 and 122 | raw_line.split(')')[1].split(' ')[3] in self.nop_results): 123 | return True 124 | return False 125 | 126 | def is_unwanted_protocol(self, line_args): 127 | """ is this unwanted protocol """ 128 | if len(line_args[2].split('<')) > 1: 129 | protocol = line_args[2].split('<')[1].split(':')[0] 130 | # do not prase unwanted protocols 131 | if line_args[2] != '<...' and protocol not in self.protocols: 132 | return True 133 | return False 134 | 135 | def is_unfinished(self, args): 136 | """ is line with unfinished syscall """ 137 | return (args[-2] == '') 138 | 139 | def is_resumed(self, args): 140 | """ is line with resumed syscall """ 141 | return (args[2] == '<...' and args[4][0:7] == 'resumed') 142 | 143 | def filter_and_reconstruct_line(self, parse_line): 144 | """ filter non wanted lines, reconstruct resumed, or return wanted lines """ 145 | args = parse_line.split(' ') 146 | if args[1]: 147 | new_line = parse_line 148 | else: # strace version 6 put 2 blanks after pid 149 | while args[1] == '': 150 | del args[1] 151 | new_line = ' '.join(args) 152 | 153 | pid = int(args[0]) 154 | syscall = args[2].split('(')[0] 155 | if (self.is_stop_or_signal_line(args) or 156 | self.is_unwanted_resumed_syscall(args) or 157 | self.is_unwanted_syscall(args) or 158 | self.is_error_return_code(parse_line) or 159 | self.is_unwanted_protocol(args)): 160 | return False 161 | 162 | if self.is_unfinished(args): 163 | key = f'{pid}-{syscall}' 164 | self.syscall_track.store_line(key, args) 165 | return False 166 | 167 | if self.is_resumed(args): 168 | resumed_syscall = args[3] 169 | key = f'{pid}-{resumed_syscall}' 170 | return self.syscall_track.reconstruct_resumed(key, args) 171 | 172 | return new_line 173 | 174 | def get_payload_chunk(self, syscall, args): 175 | """ scape payload from multiple payload strace encodings """ 176 | payload = "" 177 | if syscall in self.syscalls_format['single_chunk_payload']: 178 | payload = args[3].split('"')[1] 179 | 180 | if syscall in self.syscalls_format['vector_payload']: 181 | vector = ' '.join(args[3:-4]) 182 | msg_iov = vector.split('[')[1].split(']')[0] 183 | chunks = msg_iov.split('"') 184 | for segment in range(1, len(chunks), 2): 185 | payload += chunks[segment] 186 | return payload 187 | 188 | def parse_tcpip_v4(self, tcpip_chunk, syscall, args): 189 | """ from strace fd part that has tcpip content, parse src/dst ip/port 190 | content may be srcip:srcport->dstip:dstport or 191 | number, and if it's a number, we return 127.0.0.x """ 192 | if '->' in tcpip_chunk: 193 | first_ip = tcpip_chunk.split(':')[0] 194 | first_port = int(tcpip_chunk.split(':')[1].split('-')[0]) 195 | second_ip = tcpip_chunk.split('>')[1].split(':')[0] 196 | second_port = int(tcpip_chunk.split('>')[1].split(':')[1]) 197 | else: 198 | # set fake tcpip data, as real tcpip is partial or missing 199 | first_ip = '127.0.0.1' 200 | first_port = 11111 201 | second_ip = '127.0.0.2' 202 | second_port = 22222 203 | # in some cases, on some systemcalls, there is sockaddr at the end 204 | if syscall in self.syscalls_broken: 205 | reconstruct_line = ' '.join(args) 206 | brace_section = reconstruct_line.split('{') 207 | if not len(brace_section) > 1: 208 | return False 209 | sockaddr_part = brace_section[1].split('}')[0] 210 | second_port = int(sockaddr_part.split('sin_port=htons(')[1].split(')')[0]) 211 | second_ip_hex = sockaddr_part.split('sin_addr=inet_addr("')[1].split('"')[0] 212 | if second_ip_hex[0] == '\\': 213 | second_ip = "" 214 | for i in ",0x".join(second_ip_hex.split('\\x'))[1:].split(','): 215 | second_ip += chr(int(i, 16)) 216 | # in such cases, close might contain source port, 217 | # so we could recollect it 218 | # but we have to track all previous usage of this pid-fd 219 | else: 220 | second_ip = second_ip_hex 221 | return [first_ip, first_port, second_ip, second_port] 222 | 223 | def sorted_tcpip_v4_params(self, syscall, net_info, args): 224 | """ parse tcpip and put in right order src/dst for pcap """ 225 | parsed_tcpip = self.parse_tcpip_v4(net_info, syscall, args) 226 | if not parsed_tcpip: 227 | return False 228 | (first_ip, first_port, second_ip, second_port) = parsed_tcpip 229 | if syscall in self.syscalls_out: 230 | return [first_ip, first_port, second_ip, second_port] 231 | return [second_ip, second_port, first_ip, first_port] 232 | 233 | def parse_tcpip_v6(self, tcpip_chunk): 234 | """ from strace fd part that has tcpip content, parse src/dst ip/port 235 | content may be srcip:srcport->dstip:dstport or 236 | number, and if it's a number, we return 127.0.0.x """ 237 | if '->' in tcpip_chunk: 238 | first_part = tcpip_chunk.split('->')[0][1:] 239 | first_ip = first_part.split(']')[0] 240 | first_port = int(first_part.split(':')[-1]) 241 | second_part = tcpip_chunk.split('->')[1][1:] 242 | second_ip = second_part.split(']')[0] 243 | second_port = int(second_part.split(':')[-1]) 244 | else: 245 | # set fake tcpip data, as real tcpip is partial or missing 246 | first_ip = '::ffff:127.0.0.1' 247 | first_port = 11111 248 | second_ip = '::ffff:127.0.0.2' 249 | second_port = 22222 250 | # in some cases, on some systemcalls, there is sockaddr at the end 251 | # yet I do not have strace example of such 252 | return [first_ip, first_port, second_ip, second_port] 253 | 254 | def sorted_tcpip_v6_params(self, syscall, net_info): 255 | """ parse tcpip and put in right order src/dst for pcap """ 256 | parsed_tcpip = self.parse_tcpip_v6(net_info) 257 | if not parsed_tcpip: 258 | return False 259 | (first_ip, first_port, second_ip, second_port) = parsed_tcpip 260 | if syscall in self.syscalls_out: 261 | return [first_ip, first_port, second_ip, second_port] 262 | return [second_ip, second_port, first_ip, first_port] 263 | 264 | def bytes_code_payload(self, line_payload): 265 | r"""Convert payload to bytes, ignoring malformed hex escapes. 266 | 267 | strace with the ``-xx`` flag emits payload bytes as ``\xHH`` 268 | sequences. Occasionally, we encounter truncated escapes like 269 | ``\x`` without any hex digits (for example when the output is 270 | cut short). The previous implementation tried to split the 271 | string manually which resulted in ``int('0x', 16)`` raising a 272 | ``ValueError`` when such a malformed escape was present. We 273 | now use a regular expression to extract only well-formed 274 | ``\xHH`` sequences and ignore the malformed ones so that the 275 | parser can continue processing the trace. 276 | """ 277 | 278 | hex_bytes = re.findall(r"\\x([0-9a-fA-F]{2})", line_payload) 279 | return bytes(int(byte, 16) for byte in hex_bytes) 280 | 281 | def _extract_result(self, unified_line): 282 | """Return the syscall result (retval) or None.""" 283 | if ' = ' not in unified_line: 284 | return None 285 | tail = unified_line.rsplit(' = ', 1)[1] 286 | value = tail.split(' ', 1)[0] 287 | try: 288 | return int(value) 289 | except ValueError: 290 | return None 291 | 292 | def _extract_inode(self, annotation): 293 | """Extract inode value from fd annotation if present.""" 294 | if '[' not in annotation or ']' not in annotation: 295 | return None 296 | candidate = annotation.split('[')[1].split(']')[0] 297 | return int(candidate) if candidate.isdigit() else None 298 | 299 | def parse_strace_line(self, strace_line): 300 | """ decode strace line to a structure, or return False """ 301 | if not strace_line: 302 | return False 303 | unified_line = self.filter_and_reconstruct_line(strace_line) 304 | if not unified_line: 305 | return False 306 | parsed = {} 307 | args = unified_line.split(' ') 308 | 309 | if len(args[2].split('<')) > 1: 310 | fd_annotation = args[2].split('<')[1].split('>')[0] 311 | parsed['protocol'] = fd_annotation.split(':')[0] 312 | else: 313 | return False 314 | 315 | parsed['pid'] = int(args[0]) 316 | parsed['syscall'] = args[2].split('(')[0] 317 | 318 | parsed['result'] = self._extract_result(unified_line) 319 | 320 | if parsed['syscall'] in self.syscalls_out: 321 | parsed['direction_out'] = True 322 | else: 323 | parsed['direction_out'] = False 324 | 325 | parsed['fd'] = int(args[2].split('(')[1].split('<')[0]) 326 | try: 327 | parsed['time'] = float(args[1]) 328 | except ValueError: 329 | parsed['time'] = 0.0 330 | 331 | # start tracking first occurrence of pid-fd pair 332 | track_key = f"{parsed['pid']}-{parsed['fd']}" 333 | self.fd_track.start_track(track_key) 334 | 335 | # if syscall is close, fd is closed, incrase fd_track for pid-fd key 336 | if parsed['syscall'] in self.syscalls_format['state']: 337 | self.fd_track.increase(track_key) 338 | if parsed['protocol'] in self.protocols_unix: 339 | parsed['session'] = self.fd_track.get(track_key) 340 | parsed['payload'] = b'' 341 | parsed['inode'] = self._extract_inode(fd_annotation) 342 | return parsed 343 | return False 344 | 345 | # TCP session unique number, ie count of different connections with same pair pid-fd 346 | parsed['session'] = self.fd_track.get(track_key) 347 | 348 | payload = self.get_payload_chunk(parsed['syscall'], args) 349 | 350 | full_payload = self.bytes_code_payload(payload) 351 | if isinstance(parsed['result'], int): 352 | emit_len = parsed['result'] 353 | if emit_len < 0: 354 | emit_len = 0 355 | if emit_len < len(full_payload): 356 | full_payload = full_payload[:emit_len] 357 | if len(full_payload) > self.scapy_max_payload: 358 | parsed['payload'] = full_payload[:self.scapy_max_payload] 359 | self.split_cache_packet = dict(parsed) 360 | self.split_cache_packet['payload'] = full_payload[self.scapy_max_payload:] 361 | else: 362 | parsed['payload'] = full_payload 363 | if parsed['protocol'] in self.protocols_unix: 364 | parsed['inode'] = self._extract_inode(fd_annotation) 365 | return parsed 366 | 367 | # parase ip address encoded in strace fd argument 368 | net_info = ']'.join('['.join(args[2].split('[')[1:]).split(']')[0:-1]) 369 | 370 | net_parse = False 371 | if parsed['protocol'] in self.protocols_ipv4: 372 | net_parse = self.sorted_tcpip_v4_params(parsed['syscall'], net_info, args) 373 | if parsed['protocol'] in self.protocols_ipv6: 374 | net_parse = self.sorted_tcpip_v6_params(parsed['syscall'], net_info) 375 | 376 | if not net_parse: 377 | return False 378 | 379 | (parsed['source_ip'], parsed['source_port'], parsed['destination_ip'], 380 | parsed['destination_port']) = net_parse 381 | 382 | return parsed 383 | 384 | def process(self, pline): 385 | """ call to reserved process method, used by higher level generator """ 386 | return self.parse_strace_line(pline) 387 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | --------------------------------------------------------------------------------