From 6f46b600a4af6ff0e48c096a85ac4c1abe94be57 Mon Sep 17 00:00:00 2001 From: Mike Wang Date: Sun, 12 Oct 2025 18:00:10 +0800 Subject: [PATCH 1/6] chore: Update version string --- natter-check/natter-check.py | 2 +- natter.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/natter-check/natter-check.py b/natter-check/natter-check.py index 9688133..d501310 100755 --- a/natter-check/natter-check.py +++ b/natter-check/natter-check.py @@ -25,7 +25,7 @@ import struct import codecs -__version__ = "2.2.0" +__version__ = "2.2.1-dev" def fix_codecs(codec_list = ["utf-8", "idna"]): diff --git a/natter.py b/natter.py index c45f90c..cff33d5 100755 --- a/natter.py +++ b/natter.py @@ -34,7 +34,7 @@ import threading import subprocess -__version__ = "2.2.0" +__version__ = "2.2.1-dev" class Logger(object): From 8697d267c9ec1679d69008855fb5344202a81f9c Mon Sep 17 00:00:00 2001 From: Mike Wang Date: Sun, 12 Oct 2025 18:12:18 +0800 Subject: [PATCH 2/6] Fix: Invalid priority expression value with `-m nftables` #154 --- natter.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/natter.py b/natter.py index cff33d5..e430dd6 100755 --- a/natter.py +++ b/natter.py @@ -686,25 +686,29 @@ def _nftables_init(self): return except subprocess.CalledProcessError: pass + + # Priority values: + # dstnat (-100) - 5: -105 + # srcnat ( 100) - 5: 95 initial_rules = ( ''' table ip natter { chain natter_dnat { } chain natter_snat { } chain prerouting { - type nat hook prerouting priority dstnat-5; policy accept; + type nat hook prerouting priority -105; policy accept; jump natter_dnat; } chain output { - type nat hook output priority dstnat-5; policy accept; + type nat hook output priority -105; policy accept; jump natter_dnat; } chain postrouting { - type nat hook postrouting priority srcnat-5; policy accept; + type nat hook postrouting priority 95; policy accept; jump natter_snat; } chain input { - type nat hook input priority srcnat-5; policy accept; + type nat hook input priority 95; policy accept; jump natter_snat; } } From 10330f701250ae82dbec4b00841b880cbc57ad1e Mon Sep 17 00:00:00 2001 From: Mike Wang Date: Sun, 12 Oct 2025 18:14:44 +0800 Subject: [PATCH 3/6] Fix: MicroPython 'list' object has no attribute '__copy__' on startup --- micropython/pymodule/argparse.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/micropython/pymodule/argparse.py b/micropython/pymodule/argparse.py index 03cfe21..a82a8c2 100644 --- a/micropython/pymodule/argparse.py +++ b/micropython/pymodule/argparse.py @@ -982,7 +982,7 @@ def __init__(self, metavar=metavar) def __call__(self, parser, namespace, values, option_string=None): - items = _ensure_value(namespace, self.dest, []).__copy__() + items = _ensure_value(namespace, self.dest, [])[:] items.append(values) setattr(namespace, self.dest, items) @@ -1008,7 +1008,7 @@ def __init__(self, metavar=metavar) def __call__(self, parser, namespace, values, option_string=None): - items = _ensure_value(namespace, self.dest, []).__copy__() + items = _ensure_value(namespace, self.dest, [])[:] items.append(self.const) setattr(namespace, self.dest, items) From ed8cc59b42935d48f2c08e984e50a9d3a5002a23 Mon Sep 17 00:00:00 2001 From: Mike Wang Date: Sun, 12 Oct 2025 18:19:34 +0800 Subject: [PATCH 4/6] Feat: enable SO_REUSEPORT for port in use by other process Co-authored-by: hev --- .../cmodule/natterutils/micropython.mk | 3 + .../cmodule/natterutils/modnatterutils.c | 210 ++++++++++++++++++ natter.py | 15 ++ 3 files changed, 228 insertions(+) create mode 100644 micropython/cmodule/natterutils/micropython.mk create mode 100644 micropython/cmodule/natterutils/modnatterutils.c diff --git a/micropython/cmodule/natterutils/micropython.mk b/micropython/cmodule/natterutils/micropython.mk new file mode 100644 index 0000000..b7ac4bd --- /dev/null +++ b/micropython/cmodule/natterutils/micropython.mk @@ -0,0 +1,3 @@ +NATTERUTILS_MOD_DIR := $(USERMOD_DIR) +SRC_USERMOD += $(NATTERUTILS_MOD_DIR)/modnatterutils.c +CFLAGS_USERMOD += -I$(NATTERUTILS_MOD_DIR) diff --git a/micropython/cmodule/natterutils/modnatterutils.c b/micropython/cmodule/natterutils/modnatterutils.c new file mode 100644 index 0000000..d921bef --- /dev/null +++ b/micropython/cmodule/natterutils/modnatterutils.c @@ -0,0 +1,210 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) +#include +#include +#include +#endif + +#include "py/runtime.h" +#include "py/smallint.h" + +#if defined(__linux__) && defined(SYS_pidfd_open) && defined(SYS_pidfd_getfd) +#define NATTERUTILS_HAVE_REUSE_PORT +#endif + +#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0])) + +#ifdef NATTERUTILS_HAVE_REUSE_PORT +static unsigned long get_inode(int port, int family) { + const char *paths[] = { + "/proc/net/tcp", + "/proc/net/tcp6", + }; + char *line = NULL; + size_t len = 0; + ssize_t nread; + FILE *fp; + int i; + + i = (family == AF_INET6) ? 1 : 0; + + fp = fopen(paths[i], "r"); + if (!fp) { + return 0; + } + + nread = getline(&line, &len, fp); + if (nread < 0) { + fclose(fp); + return 0; + } + + while ((nread = getline(&line, &len, fp)) != -1) { + int res, local_port, rem_port, d, state, uid, timer_run, timeout; + unsigned long rxq, txq, time_len, retr, inode; + char rem_addr[128], local_addr[128]; + + res = sscanf(line, + "%d: %64[0-9A-Fa-f]:%X %64[0-9A-Fa-f]:%X " + "%X %lX:%lX %X:%lX %lX %d %d %lu %*s\n", + &d, local_addr, &local_port, rem_addr, &rem_port, &state, + &txq, &rxq, &timer_run, &time_len, &retr, &uid, &timeout, + &inode); + if ((res >= 14) && (state == 10) && (local_port == port)) { + fclose(fp); + return inode; + } + } + + free(line); + fclose(fp); + + return 0; +} + +static int get_pid_fd(unsigned long inode, pid_t *pid, int *fd) { + struct dirent *dpe; + char match[256]; + DIR *dp; + + dp = opendir("/proc"); + if (!dp) { + return -1; + } + + snprintf(match, sizeof(match) - 1, "socket:[%lu]", inode); + + while ((dpe = readdir(dp))) { + char path[1024]; + struct dirent *dfe; + DIR *df; + + if (dpe->d_type != DT_DIR) { + continue; + } + + snprintf(path, sizeof(path) - 1, "/proc/%s/fd", dpe->d_name); + df = opendir(path); + if (!df) { + continue; + } + + while ((dfe = readdir(df))) { + char name[256]; + int len; + + if (dfe->d_type != DT_LNK) { + continue; + } + + snprintf(path, sizeof(path) - 1, "/proc/%s/fd/%s", dpe->d_name, + dfe->d_name); + len = readlink(path, name, sizeof(name) - 1); + if (len < 0) { + continue; + } + + name[len] = '\0'; + if (strcmp(name, match) == 0) { + *fd = strtoul(dfe->d_name, NULL, 10); + *pid = strtoul(dpe->d_name, NULL, 10); + closedir(df); + closedir(dp); + return 0; + } + } + + closedir(df); + } + + closedir(dp); + + return -1; +} + +static int set_reuse_port(pid_t pid, int fd) { + const int reuse = 1; + int pfd; + int sfd; + + pfd = syscall(SYS_pidfd_open, pid, 0); + if (pfd < 0) { + return -1; + } + + sfd = syscall(SYS_pidfd_getfd, pfd, fd, 0); + if (sfd < 0) { + close(pfd); + return -1; + } + + setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int)); + setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, &reuse, sizeof(int)); + + close(sfd); + close(pfd); + return 0; +} + +static mp_obj_t natterutils_reuse_port(mp_obj_t port_obj) { + int types[] = {AF_INET, AF_INET6}; + int result = 0; + int p; + size_t i; + + p = mp_obj_get_int(port_obj); + + for (i = 0; i < ARRAY_SIZE(types); i++) { + unsigned long inode; + + inode = get_inode(p, types[i]); + if (inode > 0) { + pid_t pid; + int res; + int sfd; + + res = get_pid_fd(inode, &pid, &sfd); + if (res == 0) { + result |= set_reuse_port(pid, sfd); + } + } + } + + if (result) { + mp_raise_OSError(EINVAL); + } + + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(natterutils_reuse_port_obj, + natterutils_reuse_port); +#endif /* NATTERUTILS_HAVE_REUSE_PORT */ + + +static const mp_rom_map_elem_t natterutils_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_posix) }, + +#ifdef NATTERUTILS_HAVE_REUSE_PORT + { MP_ROM_QSTR(MP_QSTR_reuse_port), + MP_ROM_PTR(&natterutils_reuse_port_obj) }, +#endif /* NATTERUTILS_HAVE_REUSE_PORT */ +}; +static MP_DEFINE_CONST_DICT(natterutils_module_globals, + natterutils_module_globals_table); + + +const mp_obj_module_t natterutils_user_cmodule = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *) &natterutils_module_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_natterutils, natterutils_user_cmodule); diff --git a/natter.py b/natter.py index e430dd6..e18b7ea 100755 --- a/natter.py +++ b/natter.py @@ -235,6 +235,8 @@ def __init__(self, stun_server_list, source_host="0.0.0.0", source_port=0, def get_mapping(self): first = self.stun_server_list[0] while True: + if self.source_port: + set_reuse_port(self.source_port) try: return self._get_mapping() except StunClient.ServerUnavailable as ex: @@ -1545,6 +1547,19 @@ def ip_normalize(ipaddr): return socket.inet_ntoa(socket.inet_aton(ipaddr)) +def set_reuse_port(port): + try: + from natterutils import reuse_port + except ImportError: + Logger.debug("reuse-port: Not implemented on this platform. Ignored.") + return + try: + reuse_port(port) + except OSError: + Logger.debug("reuse-port: Failed to reuse port. Ignored.") + return + + def natter_main(show_title = True): argp = argparse.ArgumentParser( description="Expose your port behind full-cone NAT to the Internet.", add_help=False From cdce450a3270c58c167af479b374477256cf4738 Mon Sep 17 00:00:00 2001 From: Mike Wang Date: Mon, 13 Oct 2025 04:07:07 +0800 Subject: [PATCH 5/6] feat: Embed natter-check into natter executable --- micropython/patch/tools_mpy_tool_py.patch | 12 ++++++++++++ micropython/pymodule/_manifest.py | 1 + micropython/pymodule/socket.py | 13 +++++++++++++ natter.py | 19 +++++++++++++++++++ 4 files changed, 45 insertions(+) create mode 100644 micropython/patch/tools_mpy_tool_py.patch diff --git a/micropython/patch/tools_mpy_tool_py.patch b/micropython/patch/tools_mpy_tool_py.patch new file mode 100644 index 0000000..5bb9d3a --- /dev/null +++ b/micropython/patch/tools_mpy_tool_py.patch @@ -0,0 +1,12 @@ +diff --git a/tools/mpy-tool.py b/tools/mpy-tool.py +index 69d3a6c..2240ff3 100755 +--- a/tools/mpy-tool.py ++++ b/tools/mpy-tool.py +@@ -1386,6 +1386,7 @@ def read_mpy(filename): + + # Compute the compiled-module escaped name. + cm_escaped_name = qstr_table[0].str.replace("/", "_")[:-3] ++ cm_escaped_name = cm_escaped_name.replace('-', '_hyphen_') + + # Read the outer raw code, which will in turn read all its children. + raw_code_file_offset = reader.tell() diff --git a/micropython/pymodule/_manifest.py b/micropython/pymodule/_manifest.py index 3e42363..ca58e77 100644 --- a/micropython/pymodule/_manifest.py +++ b/micropython/pymodule/_manifest.py @@ -8,6 +8,7 @@ def __manifest_add_modules(): module('natter.py', base_path='../..') +module('natter-check.py', base_path='../../natter-check') __manifest_add_modules() del __manifest_add_modules diff --git a/micropython/pymodule/socket.py b/micropython/pymodule/socket.py index df78707..5524b97 100644 --- a/micropython/pymodule/socket.py +++ b/micropython/pymodule/socket.py @@ -185,6 +185,19 @@ def inet_ntoa(packed_ip): return _usocket.inet_ntop(_posix.AF_INET, packed_ip) +def gethostbyname_ex(hostname): + res = _getaddrinfo( + hostname, "0", _posix.AF_INET, _posix.SOCK_DGRAM, 0, + _posix.AI_NUMERICSERV + ) + ipstrs = [ + _getnameinfo( + rec[4], _posix.NI_NUMERICHOST | _posix.NI_NUMERICSERV + )[0] for rec in res + ] + return (hostname, [], ipstrs) + + def _getaddrinfo(*args, **kwargs): try: return _posix.getaddrinfo(*args, **kwargs) diff --git a/natter.py b/natter.py index e18b7ea..ee08567 100755 --- a/natter.py +++ b/natter.py @@ -1439,6 +1439,18 @@ def search_codec(name): codecs.register(search_codec) +def run_natter_check(): + try: + modcheck = __import__('natter-check', None, None, ['natter-check']) + except ImportError: + raise RuntimeError("natter-check.py is missing") from None + + if hasattr(modcheck, 'natter-check'): + modcheck = modcheck.__dict__['natter-check'] + + modcheck.main() + + def check_docker_network(): if not sys.platform.startswith("linux"): return @@ -1572,6 +1584,9 @@ def natter_main(show_title = True): group.add_argument( "--help", action="help", help="show this help message and exit" ) + group.add_argument( + "--check", action="store_true", help="run natter-check and exit" + ) group.add_argument( "-v", action="store_true", help="verbose mode, printing debug messages" ) @@ -1649,6 +1664,10 @@ def natter_main(show_title = True): else: sys.tracebacklimit = 0 + if args.check: + run_natter_check() + sys.exit(0) + validate_positive(interval) if stun_list: for stun_srv in stun_list: From ce1427650178167d2330c490d2e5cd979223e9a4 Mon Sep 17 00:00:00 2001 From: Mike Wang Date: Mon, 13 Oct 2025 04:09:19 +0800 Subject: [PATCH 6/6] release: Natter v2.2.1 --- natter-check/natter-check.py | 2 +- natter.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/natter-check/natter-check.py b/natter-check/natter-check.py index d501310..f8f51a9 100755 --- a/natter-check/natter-check.py +++ b/natter-check/natter-check.py @@ -25,7 +25,7 @@ import struct import codecs -__version__ = "2.2.1-dev" +__version__ = "2.2.1" def fix_codecs(codec_list = ["utf-8", "idna"]): diff --git a/natter.py b/natter.py index ee08567..e04901f 100755 --- a/natter.py +++ b/natter.py @@ -34,7 +34,7 @@ import threading import subprocess -__version__ = "2.2.1-dev" +__version__ = "2.2.1" class Logger(object):