feat: update I2P interface support with new documentation, localization, and default settings for connectable peers

This commit is contained in:
Ivan 2026-08-13 09:40:19 -05:00
parent dafd52a72e
commit 6d7806980a
No known key found for this signature in database
16 changed files with 209 additions and 12 deletions

View file

@ -74,6 +74,12 @@ LXMF, LXST, and Nomad features become reachable
3. Watch the path table (**Tools → RNPath**) if connectivity fails.
4. Enable **auto-announce** so your services are visible.
## I2P
I2P uses the local router's SAM API (usually `127.0.0.1:7656`). Enable SAM in the router. Do not run Java I2P and i2pd at the same time.
MeshChatX allows one I2P interface, last in the list, with Transport Mode on. New interfaces default `connectable` off. Turn it on only if this node should accept inbound I2P peers. That makes the node an I2P transport. At least one `b32.i2p` peer is required.
## Bundled documentation hints
The Interfaces UI links into the Reticulum manual sections on interface options. Open **Documentation → Reticulum** and search for `interfaces` if you need field-by-field reference.

Binary file not shown.

View file

@ -1066,7 +1066,7 @@ def register_interfaces_routes(routes, app):
if interface_type == "I2PInterface":
connectable_value = data.get("connectable")
if connectable_value is None or connectable_value == "":
interface_details["connectable"] = "True"
interface_details["connectable"] = "False"
else:
interface_details["connectable"] = (
"True"

View file

@ -640,8 +640,7 @@
<div
class="bg-blue-50/50 dark:bg-blue-900/10 p-3 rounded-2xl border border-blue-100 dark:border-blue-900/20 text-xs text-blue-800 dark:text-blue-300"
>
To use the I2P interface, you must have an I2P router running on your
system with SAM enabled.
{{ $t("interfaces.i2p_sam_required") }}
</div>
<div
v-if="!transportEnabled"
@ -657,12 +656,23 @@
</div>
<div class="flex items-center gap-2">
<Toggle id="i2p-connectable" v-model="newInterfaceConnectable" />
<FormLabel for="i2p-connectable" class="cursor-pointer mb-0! text-sm"
>Allow incoming peers (connectable)</FormLabel
>
<FormLabel for="i2p-connectable" class="cursor-pointer mb-0! text-sm">{{
$t("interfaces.i2p_connectable_label")
}}</FormLabel>
</div>
<p class="text-xs text-gray-600 dark:text-zinc-400">
{{ $t("interfaces.i2p_connectable_hint") }}
</p>
<div
v-if="newInterfaceConnectable"
class="bg-amber-50/80 dark:bg-amber-900/20 p-3 rounded-2xl border border-amber-200 dark:border-amber-800/40 text-xs text-amber-900 dark:text-amber-200"
>
{{ $t("interfaces.i2p_connectable_warning") }}
</div>
<div>
<FormLabel class="glass-label">Initial Peers (Optional)</FormLabel>
<FormLabel class="glass-label">{{
$t("interfaces.i2p_peers_label")
}}</FormLabel>
<div class="space-y-2">
<div
v-for="(peer, index) in I2PSettings.newInterfacePeers"
@ -672,7 +682,7 @@
<input
v-model="I2PSettings.newInterfacePeers[index]"
type="text"
placeholder="b32.i2p address"
:placeholder="$t('interfaces.i2p_peers_placeholder')"
class="input-field"
/>
<button
@ -691,7 +701,8 @@
class="secondary-chip py-1! px-3! text-[10px]!"
@click="addI2PPeer('')"
>
<MaterialDesignIcon icon-name="plus" class="size-3" /> Add Peer
<MaterialDesignIcon icon-name="plus" class="size-3" />
{{ $t("interfaces.i2p_peers_add") }}
</button>
</div>
</div>
@ -2203,7 +2214,7 @@ export default {
newInterfaceFixedMTU: null,
newInterfaceBootstrapOnly: true,
newInterfaceConfiguredBitrate: null,
newInterfaceConnectable: true,
newInterfaceConnectable: false,
newInterfaceBackboneListenMode: false,
newInterfaceBackboneListenPort: null,
newInterfaceBackboneListenIp: null,
@ -2805,7 +2816,7 @@ export default {
this.newInterfaceMaxReconnectTries = iface.max_reconnect_tries ?? null;
this.newInterfaceFixedMTU = iface.fixed_mtu ?? null;
this.newInterfaceConnectable =
iface.connectable === undefined ? true : this.parseBool(iface.connectable);
iface.connectable === undefined ? false : this.parseBool(iface.connectable);
if (iface.type === "BackboneInterface") {
this.newInterfaceBackboneListenMode = iface.listen_port != null && iface.listen_port !== "";
@ -3307,7 +3318,7 @@ export default {
config.type === "I2PInterface"
? config.connectable !== undefined && config.connectable !== null && config.connectable !== ""
? this.parseBool(config.connectable)
: true
: false
: null,
group_id: config.group_id || null,
multicast_address_type: config.multicast_address_type || null,
@ -3444,6 +3455,16 @@ export default {
return;
}
if (this.newInterfaceType === "I2PInterface") {
const peers = (this.I2PSettings.newInterfacePeers || [])
.map((p) => String(p).trim())
.filter(Boolean);
if (!peers.length) {
ToastUtils.error(this.$t("interfaces.i2p_peers_required"));
return;
}
}
if (this.newInterfaceType === "RNodeInterface" && this.newInterfaceRNodeUseBle) {
const raw = (this.newInterfaceRNodeBlePeer || "").trim();
const inner = raw.toLowerCase().startsWith("ble://") ? raw.slice(6).trim() : raw;

View file

@ -1434,6 +1434,14 @@
"i2p_transport_required": "Aktivieren Sie den Transportmodus in den Einstellungen, bevor Sie eine I2P-Schnittstelle hinzufügen.",
"i2p_already_exists": "Es existiert bereits eine I2P-Schnittstelle. Entfernen Sie sie, bevor Sie eine weitere hinzufügen.",
"i2p_import_forbidden": "I2P-Schnittstellen können nicht aus einer Datei importiert werden. Fügen Sie I2P nur über Schnittstelle hinzufügen hinzu.",
"i2p_sam_required": "Ein lokaler I2P-Router muss SAM aktiviert haben, üblicherweise auf 127.0.0.1:7656. Java-I2P und i2pd nicht gleichzeitig betreiben.",
"i2p_connectable_label": "Eingehende Peers zulassen (connectable)",
"i2p_connectable_hint": "Aus entspricht Reticulum. Ein veröffentlicht ein eingehendes I2P-Ziel, damit andere RNS-Knoten zu Ihnen verbinden können. Lassen Sie es aus, außer Sie wollen ein I2P-Transport sein.",
"i2p_connectable_warning": "Connectable plus Transportmodus öffnet ein Server-SAM-Ziel. Stirbt diese Sitzung, reißt RNS die I2P-Schnittstelle herunter, bis Sie neu starten.",
"i2p_peers_label": "Anfängliche Peers (erforderlich)",
"i2p_peers_add": "Peer hinzufügen",
"i2p_peers_placeholder": "b32.i2p-Adresse",
"i2p_peers_required": "Fügen Sie mindestens einen b32.i2p-Peer hinzu. Mit ausgeschaltetem connectable hat I2P kein Ziel.",
"mode_default_full": "Standard (Full)",
"mode_full": "Full",
"mode_gateway": "Gateway",

View file

@ -1406,6 +1406,14 @@
"i2p_transport_required": "Enable Transport Mode in Settings before adding an I2P interface.",
"i2p_already_exists": "An I2P interface already exists. Remove it before adding another.",
"i2p_import_forbidden": "I2P interfaces cannot be imported from a file. Add I2P only through Add Interface.",
"i2p_sam_required": "A local I2P router must have SAM enabled, usually on 127.0.0.1:7656. Do not run Java I2P and i2pd at the same time.",
"i2p_connectable_label": "Allow incoming peers (connectable)",
"i2p_connectable_hint": "Off matches Reticulum. On publishes an inbound I2P destination so other RNS nodes can connect to you. Leave it off unless you intend to be an I2P transport.",
"i2p_connectable_warning": "Connectable plus Transport Mode opens a server SAM destination. If that session dies, RNS tears the I2P interface down until you restart.",
"i2p_peers_label": "Initial peers (required)",
"i2p_peers_add": "Add peer",
"i2p_peers_placeholder": "b32.i2p address",
"i2p_peers_required": "Add at least one b32.i2p peer. With connectable off, I2P has nowhere to connect.",
"find_more_nodes": "Find more nodes",
"quick_import": "Quick Import",
"quick_import_paste_hint": "Paste raw config",

View file

@ -1381,6 +1381,14 @@
"i2p_transport_required": "Active el modo de transporte en Ajustes antes de añadir una interfaz I2P.",
"i2p_already_exists": "Ya existe una interfaz I2P. Elimínela antes de añadir otra.",
"i2p_import_forbidden": "Las interfaces I2P no se pueden importar desde un archivo. Añada I2P solo desde Añadir interfaz.",
"i2p_sam_required": "Un enrutador I2P local debe tener SAM activado, normalmente en 127.0.0.1:7656. No ejecute Java I2P e i2pd a la vez.",
"i2p_connectable_label": "Permitir pares entrantes (connectable)",
"i2p_connectable_hint": "Desactivado coincide con Reticulum. Activado publica un destino I2P de entrada para que otros nodos RNS puedan conectar. Déjelo desactivado salvo que quiera ser transporte I2P.",
"i2p_connectable_warning": "Connectable más modo transporte abre un destino SAM de servidor. Si esa sesión muere, RNS derriba la interfaz I2P hasta que reinicie.",
"i2p_peers_label": "Pares iniciales (obligatorio)",
"i2p_peers_add": "Añadir par",
"i2p_peers_placeholder": "dirección b32.i2p",
"i2p_peers_required": "Añada al menos un par b32.i2p. Con connectable desactivado, I2P no tiene a quién conectar.",
"mode_default_full": "Predeterminado (Full)",
"mode_full": "Full",
"mode_gateway": "Gateway",

View file

@ -1381,6 +1381,14 @@
"i2p_transport_required": "Ota välitystila käyttöön asetuksissa ennen I2P-liittymän lisäämistä.",
"i2p_already_exists": "I2P-liittymä on jo olemassa. Poista se ennen uuden lisäämistä.",
"i2p_import_forbidden": "I2P-liittymiä ei voi tuoda tiedostosta. Lisää I2P vain Lisää liittymä -sivulta.",
"i2p_sam_required": "Paikallisessa I2P-reitittimessä SAM on oltava päällä, yleensä osoitteessa 127.0.0.1:7656. Älä aja Java I2P:tä ja i2pd:tä yhtä aikaa.",
"i2p_connectable_label": "Salli saapuvat vertaiset (connectable)",
"i2p_connectable_hint": "Pois vastaa Reticulumia. Päällä julkaisee saapuvan I2P-kohteen, jotta muut RNS-solmut voivat yhdistää. Jätä pois, ellet aio olla I2P-kuljetus.",
"i2p_connectable_warning": "Connectable plus kuljetustila avaa palvelin-SAM-kohteen. Jos istunto kuolee, RNS kaataa I2P-liittymän kunnes käynnistät uudelleen.",
"i2p_peers_label": "Alkuperäiset vertaiset (pakollinen)",
"i2p_peers_add": "Lisää vertainen",
"i2p_peers_placeholder": "b32.i2p-osoite",
"i2p_peers_required": "Lisää vähintään yksi b32.i2p-vertainen. Connectable pois päältä I2P:llä ei ole kohdetta.",
"mode_default_full": "Oletus (Full)",
"mode_full": "Full",
"mode_gateway": "Gateway",

View file

@ -1381,6 +1381,14 @@
"i2p_transport_required": "Activez le mode transport dans les paramètres avant d'ajouter une interface I2P.",
"i2p_already_exists": "Une interface I2P existe déjà. Supprimez-la avant d'en ajouter une autre.",
"i2p_import_forbidden": "Les interfaces I2P ne peuvent pas être importées depuis un fichier. Ajoutez I2P uniquement via Ajouter une interface.",
"i2p_sam_required": "Un routeur I2P local doit avoir SAM activé, en général sur 127.0.0.1:7656. Ne lancez pas Java I2P et i2pd en même temps.",
"i2p_connectable_label": "Autoriser les pairs entrants (connectable)",
"i2p_connectable_hint": "Désactivé correspond à Reticulum. Activé publie une destination I2P entrante pour que d'autres nœuds RNS puissent se connecter. Laissez désactivé sauf si vous voulez être un transport I2P.",
"i2p_connectable_warning": "Connectable plus mode transport ouvre une destination SAM serveur. Si cette session meurt, RNS démonte l'interface I2P jusqu'au redémarrage.",
"i2p_peers_label": "Pairs initiaux (obligatoire)",
"i2p_peers_add": "Ajouter un pair",
"i2p_peers_placeholder": "adresse b32.i2p",
"i2p_peers_required": "Ajoutez au moins un pair b32.i2p. Avec connectable désactivé, I2P n'a nulle part où se connecter.",
"mode_default_full": "Par défaut (Full)",
"mode_full": "Full",
"mode_gateway": "Gateway",

View file

@ -1434,6 +1434,14 @@
"i2p_transport_required": "Abilita la modalità trasporto nelle Impostazioni prima di aggiungere un'interfaccia I2P.",
"i2p_already_exists": "Esiste già un'interfaccia I2P. Rimuovila prima di aggiungerne un'altra.",
"i2p_import_forbidden": "Le interfacce I2P non possono essere importate da un file. Aggiungi I2P solo tramite Aggiungi interfaccia.",
"i2p_sam_required": "Un router I2P locale deve avere SAM abilitato, di solito su 127.0.0.1:7656. Non eseguire Java I2P e i2pd insieme.",
"i2p_connectable_label": "Consenti peer in ingresso (connectable)",
"i2p_connectable_hint": "Disattivo corrisponde a Reticulum. Attivo pubblica una destinazione I2P in ingresso così altri nodi RNS possono connettersi. Lascia disattivo a meno che tu non voglia essere un trasporto I2P.",
"i2p_connectable_warning": "Connectable più modalità trasporto apre una destinazione SAM server. Se quella sessione muore, RNS smonta l'interfaccia I2P fino al riavvio.",
"i2p_peers_label": "Peer iniziali (obbligatorio)",
"i2p_peers_add": "Aggiungi peer",
"i2p_peers_placeholder": "indirizzo b32.i2p",
"i2p_peers_required": "Aggiungi almeno un peer b32.i2p. Con connectable disattivo, I2P non ha dove connettersi.",
"mode_default_full": "Predefinito (Full)",
"mode_full": "Full",
"mode_gateway": "Gateway",

View file

@ -1381,6 +1381,14 @@
"i2p_transport_required": "Schakel transportmodus in bij Instellingen voordat je een I2P-interface toevoegt.",
"i2p_already_exists": "Er bestaat al een I2P-interface. Verwijder die voordat je er nog een toevoegt.",
"i2p_import_forbidden": "I2P-interfaces kunnen niet uit een bestand worden geïmporteerd. Voeg I2P alleen toe via Interface toevoegen.",
"i2p_sam_required": "Een lokale I2P-router moet SAM aan hebben, meestal op 127.0.0.1:7656. Draai Java I2P en i2pd niet tegelijk.",
"i2p_connectable_label": "Inkomende peers toestaan (connectable)",
"i2p_connectable_hint": "Uit komt overeen met Reticulum. Aan publiceert een inkomende I2P-bestemming zodat andere RNS-nodes kunnen verbinden. Laat uit tenzij je een I2P-transport wilt zijn.",
"i2p_connectable_warning": "Connectable plus transportmodus opent een server-SAM-bestemming. Als die sessie sterft, haalt RNS de I2P-interface naar beneden tot je herstart.",
"i2p_peers_label": "Eerste peers (verplicht)",
"i2p_peers_add": "Peer toevoegen",
"i2p_peers_placeholder": "b32.i2p-adres",
"i2p_peers_required": "Voeg minstens één b32.i2p-peer toe. Met connectable uit heeft I2P nergens heen te verbinden.",
"mode_default_full": "Standaard (Full)",
"mode_full": "Full",
"mode_gateway": "Gateway",

View file

@ -1434,6 +1434,14 @@
"i2p_transport_required": "Включите режим транспорта в настройках перед добавлением интерфейса I2P.",
"i2p_already_exists": "Интерфейс I2P уже существует. Удалите его перед добавлением другого.",
"i2p_import_forbidden": "Интерфейсы I2P нельзя импортировать из файла. Добавляйте I2P только через страницу добавления интерфейса.",
"i2p_sam_required": "У локального I2P-роутера должен быть включён SAM, обычно на 127.0.0.1:7656. Не запускайте Java I2P и i2pd одновременно.",
"i2p_connectable_label": "Разрешить входящих пиров (connectable)",
"i2p_connectable_hint": "Выкл совпадает с Reticulum. Вкл публикует входящий I2P-адрес, чтобы другие узлы RNS могли подключаться. Оставьте выкл, если вы не хотите быть I2P-транспортом.",
"i2p_connectable_warning": "Connectable вместе с режимом транспорта открывает серверный SAM-адрес. Если сессия умрёт, RNS снимет I2P-интерфейс до перезапуска.",
"i2p_peers_label": "Начальные пиры (обязательно)",
"i2p_peers_add": "Добавить пира",
"i2p_peers_placeholder": "адрес b32.i2p",
"i2p_peers_required": "Добавьте хотя бы один пир b32.i2p. При выключенном connectable I2P некуда подключаться.",
"mode_default_full": "По умолчанию (Full)",
"mode_full": "Full",
"mode_gateway": "Gateway",

View file

@ -1381,6 +1381,14 @@
"i2p_transport_required": "添加 I2P 接口前请先在设置中启用传输模式。",
"i2p_already_exists": "已存在 I2P 接口。请先删除后再添加另一个。",
"i2p_import_forbidden": "不能从文件导入 I2P 接口。请仅通过“添加接口”页面添加 I2P。",
"i2p_sam_required": "本地 I2P 路由必须启用 SAM通常在 127.0.0.1:7656。不要同时运行 Java I2P 和 i2pd。",
"i2p_connectable_label": "允许入站对等节点 (connectable)",
"i2p_connectable_hint": "关闭与 Reticulum 默认一致。打开会发布入站 I2P 目的地,供其他 RNS 节点连入。除非你要当 I2P 传输节点,否则保持关闭。",
"i2p_connectable_warning": "Connectable 加上传输模式会打开服务器 SAM 目的地。该会话一旦断开RNS 会拆掉 I2P 接口,直到你重启。",
"i2p_peers_label": "初始对等节点(必填)",
"i2p_peers_add": "添加对等节点",
"i2p_peers_placeholder": "b32.i2p 地址",
"i2p_peers_required": "至少添加一个 b32.i2p 对等节点。connectable 关闭时I2P 没有可连接的目标。",
"mode_default_full": "默认 (Full)",
"mode_full": "Full",
"mode_gateway": "Gateway",

View file

@ -74,6 +74,12 @@ LXMF, LXST, and Nomad features become reachable
3. Watch the path table (**Tools → RNPath**) if connectivity fails.
4. Enable **auto-announce** so your services are visible.
## I2P
I2P uses the local router's SAM API (usually `127.0.0.1:7656`). Enable SAM in the router. Do not run Java I2P and i2pd at the same time.
MeshChatX allows one I2P interface, last in the list, with Transport Mode on. New interfaces default `connectable` off. Turn it on only if this node should accept inbound I2P peers. That makes the node an I2P transport. At least one `b32.i2p` peer is required.
## Bundled documentation hints
The Interfaces UI links into the Reticulum manual sections on interface options. Open **Documentation → Reticulum** and search for `interfaces` if you need field-by-field reference.

View file

@ -739,6 +739,51 @@ async def test_i2p_connectable_can_be_disabled(temp_dir):
assert saved["peers"] == ["abcdef.b32.i2p"]
@pytest.mark.asyncio
async def test_i2p_connectable_defaults_to_false_when_omitted(temp_dir):
config = ConfigDict(
{
"reticulum": {"enable_transport": "True"},
"interfaces": {},
},
)
async with make_app(temp_dir, config) as handler:
payload = {
"name": "I2POut",
"type": "I2PInterface",
"peers": ["abcdef.b32.i2p"],
}
response = await handler(make_request(payload))
body = json.loads(response.body)
assert response.status == 200, body
saved = config["interfaces"]["I2POut"]
assert saved["connectable"] == "False"
@pytest.mark.asyncio
async def test_i2p_connectable_true_still_persists(temp_dir):
config = ConfigDict(
{
"reticulum": {"enable_transport": "True"},
"interfaces": {},
},
)
async with make_app(temp_dir, config) as handler:
payload = {
"name": "I2PIn",
"type": "I2PInterface",
"peers": ["abcdef.b32.i2p"],
"connectable": True,
}
response = await handler(make_request(payload))
body = json.loads(response.body)
assert response.status == 200, body
saved = config["interfaces"]["I2PIn"]
assert saved["connectable"] == "True"
@pytest.mark.asyncio
async def test_internal_mode_and_rns_bool_options_persist(temp_dir):
config = ConfigDict({"reticulum": {}, "interfaces": {}})

View file

@ -279,6 +279,53 @@ describe("AddInterfacePage.vue interface options", () => {
);
});
it("defaults I2P connectable off and requires a peer", async () => {
const wrapper = mountPage();
const ToastUtils = (await import("../../meshchatx/src/frontend/js/ToastUtils")).default;
expect(wrapper.vm.newInterfaceConnectable).toBe(false);
wrapper.vm.newInterfaceName = "I2P";
wrapper.vm.newInterfaceType = "I2PInterface";
wrapper.vm.I2PSettings.newInterfacePeers = [" "];
wrapper.vm.config = { ...(wrapper.vm.config || {}), is_transport_enabled: true };
wrapper.vm.reticulumInstance = {
...(wrapper.vm.reticulumInstance || {}),
enable_transport: true,
};
wrapper.vm.existingInterfaces = {};
await wrapper.vm.saveInterface();
expect(ToastUtils.error).toHaveBeenCalledWith("interfaces.i2p_peers_required");
expect(mockAxios.post).not.toHaveBeenCalled();
wrapper.vm.I2PSettings.newInterfacePeers = ["abcdef.b32.i2p"];
await wrapper.vm.saveInterface();
expect(mockAxios.post).toHaveBeenCalledWith(
"/api/v1/reticulum/interfaces/add",
expect.objectContaining({
type: "I2PInterface",
peers: ["abcdef.b32.i2p"],
connectable: false,
})
);
});
it("shows I2P SAM copy and a connectable warning when inbound is on", async () => {
const wrapper = mountPage();
wrapper.vm.newInterfaceType = "I2PInterface";
await wrapper.vm.$nextTick();
expect(wrapper.text()).toContain("interfaces.i2p_sam_required");
expect(wrapper.text()).toContain("interfaces.i2p_connectable_hint");
expect(wrapper.text()).not.toContain("interfaces.i2p_connectable_warning");
wrapper.vm.newInterfaceConnectable = true;
await wrapper.vm.$nextTick();
expect(wrapper.text()).toContain("interfaces.i2p_connectable_warning");
expect(wrapper.text()).toContain("interfaces.i2p_peers_add");
});
it("sends I2P connectable=false when peer disables it", async () => {
const wrapper = mountPage();