From 0d3c77bbe22ee04536e9c89cbf207486be145cd0 Mon Sep 17 00:00:00 2001 From: Marc Sune Date: Mon, 26 Aug 2024 17:03:28 +0200 Subject: [PATCH] Tc --- src/.gitignore | 1 + src/Makefile | 2 + src/common.h | 56 +++++++++++++++++++++++++ src/rules.default | 4 ++ tools/gen.py | 102 ++++++++++++++++++++++++++++++---------------- tools/rules | 7 +++- 6 files changed, 135 insertions(+), 37 deletions(-) create mode 100644 src/.gitignore create mode 100644 src/rules.default diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..71501e8 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1 @@ +rules.h diff --git a/src/Makefile b/src/Makefile index 92d36dc..36fb0a5 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,8 +1,10 @@ all: compile compile: + python3 ../tools/gen.py rules.default > rules.h clang -O2 -Wall -Werror -g -target bpf -c funnel.c -o tc_funnel.o clang -O2 -Wall -Werror -g -target bpf -c unfunnel.c -o tc_unfunnel.o clean: rm -rf *.o || true + rm -rf rules.h || true diff --git a/src/common.h b/src/common.h index 35b6d7c..2a57f79 100644 --- a/src/common.h +++ b/src/common.h @@ -1,6 +1,8 @@ #ifndef FUNNEL_COMMON_H #define FUNNEL_COMMON_H +#include + #if 0 #define PRINTK(...) bpf_printk( __VA_ARGS__ ) #else @@ -45,6 +47,58 @@ static __always_inline __be16 csum_fold(__s64 csum) char ____license[] __attribute__((section("license"), used)) = NAME #endif + +//Rules +typedef struct sfunnel_ip4_addr_match { + __be32 addr; + __u8 mask; + bool negate; +} sfunnel_ip4_addr_match_t; + +typedef struct sfunnel_l4_port_match { + __be16 port; + bool negate; +} sfunnel_l4_port_match_t; + +typedef struct sfunnel_action_params { + bool execute; + union { + struct { + __u8 funn_proto; //Funneling proto (TCP or UDP) + __be16 sport; + __be16 dport; + __u8 tcp_flags; + } funnel; + + struct { + __u8 proto; //Inner pkt proto (TCP or UDP) + } unfunnel; + + struct { + __be32 daddr; + //TODO: add more for LB. + } dnat; + }p; +}sfunnel_action_params_t; + +typedef struct sfunnel_ip4_rule { + struct { + sfunnel_ip4_addr_match_t saddr; + sfunnel_ip4_addr_match_t daddr; + __u8 proto; //0 don't match + sfunnel_l4_port_match_t sport; + sfunnel_l4_port_match_t dport; + } matches; + struct { + sfunnel_action_params_t funnel; + sfunnel_action_params_t unfunnel; + sfunnel_action_params_t dnat; + sfunnel_action_params_t accept; + sfunnel_action_params_t drop; + } actions; +}sfunnel_ip4_rule_t; + + //XXX: to be removed by dynamic config #define TCP_FUNNEL_DST_PORT 179 #define TCP_FUNNEL_SRC_PORT 540 //E.g. UUCP @@ -58,4 +112,6 @@ static __always_inline __be16 csum_fold(__s64 csum) //XXX: end to be removed +#include "rules.h" + #endif //FUNNEL_COMMON_H diff --git a/src/rules.default b/src/rules.default new file mode 100644 index 0000000..bee200d --- /dev/null +++ b/src/rules.default @@ -0,0 +1,4 @@ +ip udp dport 2055 actions funnel tcp dport 179 sport 540 +ip udp dport 4739 actions funnel tcp dport 179 sport 540 +ip udp dport 6343 actions funnel tcp dport 179 sport 540 +ip tcp dport 4739 actions funnel tcp dport 179 sport 541 diff --git a/tools/gen.py b/tools/gen.py index 09389bc..8cd84ed 100644 --- a/tools/gen.py +++ b/tools/gen.py @@ -13,7 +13,8 @@ # Filter support (*): # # TODO -# +# Action support: +# TODO INDENT=" " param_re="(\S+)" @@ -31,15 +32,12 @@ f"(dport)\s*{neg_re}\s*{param_re}" ] -# Action support: - -# dnat
: DNAT to an IPv4/IPv6 address -# drop : drop packet -action_keys = [ - "funnel", # funnel sport dport - "unfunnel",# unfunnel : unfunnels traffic, TCP or UDP header - "dnat", # DNAT to an IPv4/IPv6 address - "drop" # Drop packet +action_patterns = [ + f"(funnel)\s*{param_re}\s*(sport|dport)\s*{param_re}\s*(sport|dport)\s*{param_re}", + f"(unfunnel)\s*{param_re}", + f"(dnat)\s*{param_re}", + f"(accept)", + f"(drop)" ] #Parsing routines @@ -73,19 +71,42 @@ def extract_matches(string: str): return matches def extract_actions(string: str): - key_pattern = '|'.join(re.escape(key) for key in action_keys) - pattern = rf'\s*({key_pattern}|\w+)\s*([\w|\.|/]+)?\s*' - matches = re.findall(pattern, string) - result_dict = {} - for key, value in matches: - key = key.strip() - if key not in action_keys: - raise ValueError(f"ERROR: unknown action '{key}'!") - - result_dict[key.strip()] = { - "value": value.strip() - } - return result_dict + s = string + actions={} + for a_it in action_patterns: + pattern = rf'\s*{a_it}\s*' + re_a = re.search(pattern, s) + if not re_a: + continue + grp_len = len(re_a.groups()) + if grp_len not in [1,2,6]: + raise Exception(f"ERROR: parsing '{a_it}' in '{string}'") + a = {} + if grp_len == 2: + #Unfunnel/DNAT + if re_a.groups()[0] == "unfunnel" and re_a.groups()[1] not in ["tcp", "udp"]: + raise ValueError(f"ERROR: protocol following unfunnel action must be 'tcp' or 'udp' in '{string}'") + a["value"] = re_a.groups()[1] + elif grp_len == 6: + #Funnel + if re_a.groups()[1] not in ["tcp", "udp"]: + raise ValueError(f"ERROR: protocol following funnel action must be 'tcp' or 'udp' in '{string}'") + a["fun_proto"] = re_a.groups()[1] + if re_a.groups()[2] not in ["sport", "dport"]: + raise ValueError(f"ERROR: unknown heade field '{re_a.groups()[3]}' in '{string}'") + a[re_a.groups()[2]] = re_a.groups()[3] + if re_a.groups()[4] not in ["sport", "dport"]: + raise ValueError(f"ERROR: unknown heade field '{re_a.groups()[3]}' in '{string}'") + a[re_a.groups()[4]] = re_a.groups()[5] + + actions[re_a.groups()[0]] = a + s = re.sub(pattern, "", s) + + if s != "": + raise ValueError(f"ERROR: unable to parse '{s}' from '{string}'") + + return actions + def extract_tuples(string): r = string.split("actions") if len(r) != 2: @@ -105,7 +126,7 @@ def gen_ip_match(field: str, m: dict, indent: str): s += f" .negate = {neg}," s += f" .addr = 0x{ip:08x}," s += f" .mask = 0x{mask:08x}" - s += f"}},\n" + s += f" }},\n" return s def gen_port_match(field: str, m: dict, indent: str, end:str =","): @@ -115,9 +136,12 @@ def gen_port_match(field: str, m: dict, indent: str, end:str =","): s = f"{indent}.{field} = {{" s += f" .negate = {neg}," s += f" .port = 0x{port_nbo:04x}" - s += f"}}{end}\n" + s += f" }}{end}\n" return s +def __gen_ipproto(proto: str): + return "IPPROTO_" + proto.strip().rstrip().upper() + def gen_matches(matches: str, indent: str): s = "" @@ -133,7 +157,7 @@ def gen_matches(matches: str, indent: str): if "tcp" not in matches and "udp" not in matches: raise ValueError(f"ERROR: tcp nor udp specified. One must be selected") proto = "tcp" if "tcp" in matches else "udp" - s += f"{indent}.proto = IPPROTO" + proto.upper() + ",\n" + s += f"{indent}.proto = {__gen_ipproto(proto)},\n" #L4 ports if "sport" not in matches: @@ -147,9 +171,9 @@ def gen_matches(matches: str, indent: str): return s def gen_no_param_action(action: str, actions: dict, indent: str, end:str = ","): - s = f"{indent}.{action} {{" - s += f' .execute = { "1" if action in actions else "0"},' - s += f" .p = {{{{0}}}}" + s = f"{indent}.{action} = {{" + s += f' .execute = { "1" if action in actions else "0" },' + s += f" .p = {{{{0}}}} " s += f"}}{end}\n" return s @@ -162,16 +186,22 @@ def gen_actions(actions: dict, indent: str): s += gen_no_param_action("accept", actions, indent) if "funnel" in actions: + proto = __gen_ipproto(actions["funnel"]["fun_proto"]) + f = actions['funnel'] + s += f"{indent}.funnel = {{ .execute = 1, .p = {{ .funnel = {{ .funn_proto = {proto}, .sport = {f['sport']}, .dport = {f['dport']} }} }} }},\n" pass else: s += gen_no_param_action("funnel", actions, indent) if "unfunnel" in actions: - pass + proto = f"IPPROTO" + actions["unfunnel"]["value"].upper().strip() + s += f"{indent}.unfunnel = {{ .execute = 1, .p = {{ .unfunnel = {{ .proto = {proto} }} }} }},\n" else: s += gen_no_param_action("unfunnel", actions, indent) if "dnat" in actions: - pass + network = ipaddress.IPv4Network(actions["dnat"]["value"], strict=False) + addr_nbo = socket.htonl(int(network.network_address)) + s += f"{indent}.dnat = {{ .execute = 1, .p = {{ .dnat = {{ .daddr = 0x{addr_nbo:08x} }} }} }}\n" else: s += gen_no_param_action("dnat", actions, indent, end="") @@ -193,9 +223,10 @@ def gen_header(rules_str: str, rules: list): s += f"{rules_str}" s += f"*/\n\n" - s += "struct ip4_rule ip4_rules[] = {\n" + s += "struct sfunnel_ip4_rule ip4_rules[] = {\n" for index, rule in enumerate(rules): s += f"{INDENT}{{\n" + s += f'{2*INDENT}//{rule["__line__"]}\n' s += f'{2*INDENT}.matches = {{\n' s += gen_matches(rule["matches"], f"{3*INDENT}") s += f'{2*INDENT}}},\n' @@ -203,7 +234,7 @@ def gen_header(rules_str: str, rules: list): s += gen_actions(rule["actions"], f"{3*INDENT}") s += f'{INDENT}{INDENT}}}\n' s += f"{INDENT}}}"+ ("," if index != (len(rules) -1) else "") + "\n" - s += "} //ip4_rules\n" + s += "}; //ip4_rules\n" #End guard return s + "#endif //{guard}" @@ -227,9 +258,10 @@ def usage(): matches, actions = extract_tuples(line) rule["matches"] = matches rule["actions"] = actions + rule["__line__"] = line rules.append(rule) - print(f"Parsed { len(rules) } rules:") - print(f"{json.dumps(rules, indent=2)}") + #print(f"Parsed { len(rules) } rules:") + #print(f"{json.dumps(rules, indent=2)}") print(gen_header(file_content, rules)) diff --git a/tools/rules b/tools/rules index 5b7e084..1a46a96 100644 --- a/tools/rules +++ b/tools/rules @@ -1,2 +1,5 @@ -ip saddr 192.168.0.2/32 daddr != 10.0.0.2 tcp dport != 179 actions dnat -ip saddr 192.168.0.2/24 daddr 10.0.0.2 udp actions funnel test +ip saddr 192.168.0.2/32 daddr != 10.0.0.2 tcp dport != 179 actions dnat 172.16.0.1 +ip saddr 192.168.0.2/24 daddr 10.0.0.2 udp actions funnel tcp dport 80 sport 70 +ip saddr 192.168.0.2/24 daddr 10.0.0.2 udp actions funnel udp sport 80 dport 70 +ip saddr 192.168.0.2/24 daddr 10.0.0.2 udp actions drop +ip saddr 192.168.1.2/24 daddr 10.0.0.2 udp actions unfunnel tcp