diff --git a/net/owsync/Makefile b/net/owsync/Makefile new file mode 100644 index 00000000000000..e475195aec7946 --- /dev/null +++ b/net/owsync/Makefile @@ -0,0 +1,82 @@ +# SPDX-License-Identifier: MIT + +include $(TOPDIR)/rules.mk + +PKG_NAME:=owsync +PKG_VERSION:=1.2.0 +PKG_RELEASE:=1 + +PKG_SOURCE_PROTO:=git +PKG_SOURCE_URL:=https://github.com/pgaufillet/owsync.git +PKG_SOURCE_VERSION:=v1.2.0 +PKG_MIRROR_HASH:=97f67ac2d762686c051f3e8c70b685212a9b37862934d40586919d3353330507 + +PKG_MAINTAINER:=Pierre Gaufillet +PKG_LICENSE:=MIT +PKG_LICENSE_FILES:=LICENSE + +PKG_BUILD_PARALLEL:=1 + +include $(INCLUDE_DIR)/package.mk + +define Package/owsync + SECTION:=net + CATEGORY:=Network + SUBMENU:=File Transfer + TITLE:=Lightweight bidirectional file synchronization daemon + URL:=https://github.com/pgaufillet/owsync + DEPENDS:=+libjson-c +OWSYNC_ENABLE_ENCRYPTION:libopenssl +rpcd +endef + +define Package/owsync/description + owsync is a lightweight, secure, and robust high-availability file + synchronization tool designed specifically for OpenWrt routers. + It provides bi-directional, multi-master synchronization of + configuration files between redundant nodes. +endef + +define Package/owsync/config + config OWSYNC_ENABLE_ENCRYPTION + bool "Enable encryption support (AES-256-GCM)" + default y + help + Build owsync with encryption support using OpenSSL. + If disabled, owsync can only be used in plain mode over + secure VPN connections (WireGuard/IPsec). +endef + +MAKE_FLAGS += \ + PKGCONFIG="$(PKG_CONFIG)" \ + ENABLE_ENCRYPTION=$(if $(CONFIG_OWSYNC_ENABLE_ENCRYPTION),1,0) + +define Package/owsync/install + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) $(PKG_BUILD_DIR)/bin/owsync $(1)/usr/bin/ + + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/owsync.config $(1)/etc/config/owsync + + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/owsync.init $(1)/etc/init.d/owsync + + $(INSTALL_DIR) $(1)/etc/owsync + + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./files/owsync.rpcd $(1)/usr/libexec/rpcd/owsync +endef + +define Package/owsync/conffiles +/etc/config/owsync +/etc/owsync +endef + +define Package/owsync/prerm +#!/bin/sh +if [ -z "$${IPKG_INSTROOT}" ]; then + /etc/init.d/owsync stop >/dev/null 2>&1 + /etc/init.d/owsync disable >/dev/null 2>&1 +fi +exit 0 +endef + +$(eval $(call BuildPackage,owsync)) diff --git a/net/owsync/files/owsync.config b/net/owsync/files/owsync.config new file mode 100644 index 00000000000000..3ff4912be16cee --- /dev/null +++ b/net/owsync/files/owsync.config @@ -0,0 +1,26 @@ +config owsync 'general' + option enabled '0' + option host '::' + option port '4321' + option dir '/etc/config' + option db '/etc/owsync/owsync.db' + option key 'GENERATE_WITH_OWSYNC_GENKEY' + option poll_interval '60' + + # Plain mode (no encryption) - only use over secure VPN + # option plain_mode '1' + + # Target Peers for automatic sync + # List peer hostnames/IPs (all use the same port) + # list peer '192.168.1.2' + # list peer 'fd00::2' + + # Whitelist specific files (required - if empty, nothing is synced) + # Use 'list include *' to sync all files + # list include 'firewall' + # list include 'dhcp' + + # Blacklist specific files (takes precedence over includes) + list exclude 'network' + list exclude 'system' + list exclude 'owsync' diff --git a/net/owsync/files/owsync.init b/net/owsync/files/owsync.init new file mode 100644 index 00000000000000..8957ab110d5b80 --- /dev/null +++ b/net/owsync/files/owsync.init @@ -0,0 +1,143 @@ +#!/bin/sh /etc/rc.common +# SPDX-License-Identifier: MIT +# Copyright (c) 2025-2026 Pierre Gaufillet + +START=99 +STOP=10 +USE_PROCD=1 + +# Dependencies: network must be up before owsync can bind/connect +DEPEND="network" + +CONF_FILE="/tmp/owsync.conf" + +# Collect list items into shell variables +PEERS="" +INCLUDES="" +EXCLUDES="" + +append_peer() { + PEERS="$PEERS $1" +} + +append_include() { + INCLUDES="$INCLUDES $1" +} + +append_exclude() { + EXCLUDES="$EXCLUDES $1" +} + +generate_config() { + local host="$1" + local port="$2" + local key="$3" + local dir="$4" + local db="$5" + local poll_interval="$6" + local plain_mode="$7" + + # Create config file with restricted permissions (key inside) + rm -f "$CONF_FILE" + touch "$CONF_FILE" + chmod 600 "$CONF_FILE" + + cat > "$CONF_FILE" <> "$CONF_FILE" + else + echo "encryption_key=$key" >> "$CONF_FILE" + fi + + # Add peers + for peer in $PEERS; do + echo "peer=$peer" >> "$CONF_FILE" + done + + # Add includes + for inc in $INCLUDES; do + echo "include=$inc" >> "$CONF_FILE" + done + + # Add excludes + for exc in $EXCLUDES; do + echo "exclude=$exc" >> "$CONF_FILE" + done +} + +start_service() { + config_load owsync + + local enabled + config_get_bool enabled general enabled 0 + [ "$enabled" -eq 1 ] || return 0 + + local host port key dir db poll_interval plain_mode + config_get host general host "::" + config_get port general port "4321" + config_get key general key + config_get dir general dir "/etc/config" + config_get db general db "/etc/owsync/owsync.db" + config_get poll_interval general poll_interval "60" + config_get_bool plain_mode general plain_mode 0 + + # Collect list items + PEERS="" + INCLUDES="" + EXCLUDES="" + config_list_foreach "general" "peer" append_peer + config_list_foreach "general" "include" append_include + config_list_foreach "general" "exclude" append_exclude + + # Validate key (unless plain mode) + if [ "$plain_mode" != "1" ]; then + if [ -z "$key" ] || [ "$key" = "GENERATE_WITH_OWSYNC_GENKEY" ]; then + logger -t owsync -p daemon.err "Error: owsync key not configured" + return 1 + fi + fi + + # Validate peers for daemon mode + if [ -z "$PEERS" ]; then + logger -t owsync -p daemon.err "Error: owsync requires at least one peer for daemon mode" + return 1 + fi + + # Ensure DB directory exists + local db_dir + db_dir=$(dirname "$db") + [ -d "$db_dir" ] || mkdir -p "$db_dir" + + # Generate config file (key stays in file, not command line) + generate_config "$host" "$port" "$key" "$dir" "$db" "$poll_interval" "$plain_mode" + + # Start daemon with config file + procd_open_instance "owsync" + procd_set_param command /usr/bin/owsync daemon -c "$CONF_FILE" + procd_set_param respawn + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param file /etc/config/owsync + procd_close_instance +} + +stop_service() { + # Clean up generated config file + rm -f "$CONF_FILE" +} + +service_triggers() { + procd_add_reload_trigger "owsync" +} diff --git a/net/owsync/files/owsync.rpcd b/net/owsync/files/owsync.rpcd new file mode 100644 index 00000000000000..196492ef528bb9 --- /dev/null +++ b/net/owsync/files/owsync.rpcd @@ -0,0 +1,133 @@ +#!/bin/sh +# SPDX-License-Identifier: MIT +# Copyright (c) 2025-2026 Pierre Gaufillet +# +# owsync rpcd handler - provides ubus status interface +# Usage: ubus call owsync status + +. /usr/share/libubox/jshn.sh + +case "$1" in + list) + # List available methods and their parameters + json_init + json_add_object "status" + json_close_object + json_dump + ;; + call) + case "$2" in + status) + # Get owsync service status + + # Initialize variables + status="STOPPED" + pid=0 + enabled=0 + uptime=0 + encryption_enabled=0 + peer_count=0 + sync_dir="" + db_path="" + db_last_modified=0 + + # Check if owsync is enabled via standalone config or ha-cluster + if [ -f /etc/config/owsync ]; then + owsync_enabled=$(uci -q get owsync.config.enabled) + if [ "$owsync_enabled" = "1" ]; then + enabled=1 + fi + fi + + # Also check ha-cluster config + if [ -f /etc/config/ha-cluster ]; then + ha_enabled=$(uci -q get ha-cluster.config.enabled) + sync_method=$(uci -q get ha-cluster.config.sync_method) + if [ "$ha_enabled" = "1" ] && [ "$sync_method" = "owsync" ]; then + enabled=1 + fi + fi + + # Check if running + svc_pid=$(pgrep -o "owsync" 2>/dev/null) + if [ -n "$svc_pid" ]; then + status="RUNNING" + pid=$svc_pid + + # Calculate uptime from /proc/PID/stat + start_time=$(awk '{print $22}' /proc/$svc_pid/stat 2>/dev/null) + if [ -n "$start_time" ]; then + uptime_jiffies=$(awk '{print $1 * 100}' /proc/uptime 2>/dev/null) + uptime=$(awk "BEGIN{print int(($uptime_jiffies - $start_time) / 100)}") + fi + fi + + # Get config from generated runtime config or UCI + # Runtime config is at /tmp/owsync.conf (when using ha-cluster) + if [ -f /tmp/owsync.conf ]; then + config_file="/tmp/owsync.conf" + elif [ -f /etc/owsync/owsync.conf ]; then + config_file="/etc/owsync/owsync.conf" + else + config_file="" + fi + + if [ -n "$config_file" ]; then + # Parse config file for status info + sync_dir=$(grep "^sync_dir=" "$config_file" 2>/dev/null | cut -d= -f2) + db_path=$(grep "^database=" "$config_file" 2>/dev/null | cut -d= -f2) + + # Check encryption mode + plain_mode=$(grep "^plain_mode=" "$config_file" 2>/dev/null | cut -d= -f2) + if [ "$plain_mode" != "1" ] && [ "$plain_mode" != "true" ]; then + encryption_enabled=1 + fi + + # Count peers + peer_count=$(grep -c "^peer=" "$config_file" 2>/dev/null || echo 0) + fi + + # Fallback to UCI if config not found + if [ -z "$sync_dir" ] && [ -f /etc/config/owsync ]; then + sync_dir=$(uci -q get owsync.config.sync_dir) + fi + + # Default database path + db_path="${db_path:-/var/lib/owsync/owsync.db}" + + # Get database last modified time + if [ -f "$db_path" ]; then + db_last_modified=$(date -r "$db_path" +%s 2>/dev/null || echo 0) + fi + + # Build JSON response + json_init + json_add_string status "$status" + json_add_int pid "$pid" + json_add_boolean enabled "$enabled" + json_add_int uptime "$uptime" + + json_add_object database + json_add_string path "$db_path" + json_add_int last_modified "$db_last_modified" + json_close_object + + json_add_object config + json_add_boolean encryption_enabled "$encryption_enabled" + json_add_int peer_count "$peer_count" + json_add_string sync_dir "$sync_dir" + json_close_object + + json_dump + ;; + + *) + json_init + json_add_boolean success 0 + json_add_string error "Unknown method: $2" + json_dump + exit 1 + ;; + esac + ;; +esac