GL.iNet X-3000 Spitz AX Opsætning
Overview
This guide walks through a clean setup for a GL.iNet X-3000 Spitz AX as a 4G/5G router with secure remote access.
Prerequisites
Active SIM with data plan
APN details from your carrier
Ethernet cable for initial setup
Laptop connected to the router LAN
Initial Access
Connect via LAN to the router.
Open the admin UI at http://192.168.8.1.
Set a strong admin password and store it.
Update Firmware
Go to System -> Upgrade.
Check for the latest firmware and apply it.
Reboot and confirm version.
SIM and Mobile Settings
Insert the SIM.
Go to Network -> Modem.
Set the APN provided by your carrier.
Save and wait for the modem to register.
Confirm “Connected” and verify public IP.
WAN/LAN Setup
If using it as primary internet: - Leave WAN as DHCP or static as needed.
If using it behind another router: - Enable “Repeater” or bridge mode (if required).
Set LAN IP range (e.g., 192.168.10.1/24).
Wi-Fi Setup
Go to Wireless.
Rename SSID (2.4 GHz and 5 GHz).
Set WPA2/WPA3 password.
Remote Access (VPN)
WireGuard (recommended)
Go to VPN -> WireGuard.
Create server configuration.
Download client config for each device.
Test remote access while off-site.
Backups
Go to System -> Backup/Restore.
Download a backup after setup.
Troubleshooting
No mobile data: - Re-check APN, SIM activation, and signal strength.
Unstable connection: - Lock modem bands or set preferred network mode.
Can’t reach admin UI: - Factory reset using the reset pin (hold 10 seconds).
Change Log
0.1.0 Initial draft
test test test
MQTT
SIM mangement
Install pakages
$ opkg install luci-app-statistics collectd-mod-mqtt mosquitto-client-ssl collectd-mod-thermal collectd-mod-uptime collectd-mod-dhcpleases collectd-mod-ping
Create config directory to store MQTT config for collectd
$ mkdir -p /etc/collectd/conf.d/
Define variables and replace values of variables according to yours. Leave MQTT_TLS_PROTOCOL and MQTT_CA_CERT blank if not used
MQTT_BROKER_HOST="192.168.8.200"
MQTT_PORT="1883"
MQTT_USERNAME="openwrt"
MQTT_PASSWORD="MitHemmeligePassword!"
MQTT_PREFIX="collectd"
MQTT_TLS_PROTOCOL=""
MQTT_CA_CERT=""
Create shared config to store the variables
cat >/etc/collectd/mqtt_broker.conf <<EOF
MQTT_BROKER_HOST="$MQTT_BROKER_HOST"
MQTT_PORT="$MQTT_PORT"
MQTT_USERNAME="$MQTT_USERNAME"
MQTT_PASSWORD="$MQTT_PASSWORD"
MQTT_PREFIX="$MQTT_PREFIX"
MQTT_TLS_PROTOCOL="$MQTT_TLS_PROTOCOL"
MQTT_CA_CERT="$MQTT_CA_CERT"
EOF
Modify file permissions
$ chmod 600 /etc/collectd/mqtt_broker.conf
Generate collectd config to store in /etc/collectd/conf.d
. /etc/collectd/mqtt_broker.conf && \
cat >/etc/collectd/conf.d/mqtt.conf <<EOF
LoadPlugin mqtt
<Plugin "mqtt">
<Publish "OpenWRT">
Host "$MQTT_BROKER_HOST"
Port "${MQTT_PORT:-1883}"
User "$MQTT_USERNAME"
Password "$MQTT_PASSWORD"
ClientId "$(uci -q get system.@system[0].hostname)"
Prefix "${MQTT_PREFIX:-collectd}"
Retain true
</Publish>
</Plugin>
EOF
Restart collecd service
$ /etc/init.d/collectd restart
cat >/etc/hotplug.d/iface/01-ha-mqtt-wan-and-cell-status <<'SH'
#!/bin/sh
# Publish cellular WAN + signal + carrier + SIM info to MQTT
# Load shared MQTT settings
[ -r /etc/collectd/mqtt_broker.conf ] && . /etc/collectd/mqtt_broker.conf
# ---- USER CONFIG ----
WAN_IFACE="modem_0001_4"
MODEM_SIGNAL_TIME=0
CLEAR_DNS_SLOTS=5
CLEAR_IP6_SLOTS=5
# ---------------------
log() { logger -t ha-mqtt-wan "$*"; }
need() { command -v "$1" >/dev/null 2>&1; }
[ -n "$ACTION" ] || exit 0
[ "$INTERFACE" = "$WAN_IFACE" ] || exit 0
need mosquitto_pub || { log "missing mosquitto_pub"; exit 0; }
need jsonfilter || { log "missing jsonfilter"; exit 0; }
need ifstatus || { log "missing ifstatus"; exit 0; }
need ubus || { log "missing ubus"; exit 0; }
# Ensure required MQTT vars exist (fail fast in logs)
[ -n "$MQTT_BROKER_HOST" ] || { log "MQTT_BROKER_HOST not set"; exit 0; }
[ -n "$MQTT_PORT" ] || MQTT_PORT="1883"
[ -n "$MQTT_USERNAME" ] || MQTT_USERNAME=""
[ -n "$MQTT_PASSWORD" ] || MQTT_PASSWORD=""
[ -n "$MQTT_PREFIX" ] || MQTT_PREFIX="collectd"
HOSTNAME="$(uci -q get system.@system[0].hostname)"
[ -n "$HOSTNAME" ] || HOSTNAME="$(hostname 2>/dev/null)"
BASE="$MQTT_PREFIX/$HOSTNAME/wan-info/$WAN_IFACE"
auth_args="-h $MQTT_BROKER_HOST -p $MQTT_PORT"
[ -n "$MQTT_USERNAME" ] && auth_args="$auth_args -u $MQTT_USERNAME"
[ -n "$MQTT_PASSWORD" ] && auth_args="$auth_args -P $MQTT_PASSWORD"
[ -n "$MQTT_TLS_PROTOCOL" ] && auth_args="$auth_args --tls-version $MQTT_TLS_PROTOCOL"
[ -n "$MQTT_CA_CERT" ] && auth_args="$auth_args --cafile $MQTT_CA_CERT"
pub_abs() {
mosquitto_pub $auth_args -t "$1" -r -m "$2" >/dev/null 2>&1 \
|| log "MQTT publish failed: $1"
}
pub() { pub_abs "$BASE-$1" "$2"; }
clear_slots() {
suffix="$1"; cnt="$2"
i=0
while [ $i -lt "$cnt" ]; do
pub "$suffix-$i" ""
i=$((i + 1))
done
}
publish_ifstatus() {
IFJSON="$(ifstatus "$WAN_IFACE" 2>/dev/null)"
[ -n "$IFJSON" ] || return
jget() { echo "$IFJSON" | jsonfilter -e "$1" 2>/dev/null; }
pub "up" "$(jget '@.up')"
pub "pending" "$(jget '@.pending')"
pub "available" "$(jget '@.available')"
pub "autostart" "$(jget '@.autostart')"
pub "dynamic" "$(jget '@.dynamic')"
pub "uptime" "$(jget '@.uptime')"
pub "l3_device" "$(jget '@.l3_device')"
pub "proto" "$(jget '@.proto')"
pub "device" "$(jget '@.device')"
pub "metric" "$(jget '@.metric')"
pub "dns_metric" "$(jget '@.dns_metric')"
pub "delegation" "$(jget '@.delegation')"
ip4="$(jget '@["ipv4-address"][0].address')"
m4="$(jget '@["ipv4-address"][0].mask')"
gw4="$(jget '@.route[0].nexthop')"
pub "ip4" "$ip4"
pub "ip4_mask" "$m4"
pub "gw4" "$gw4"
lease="$(jget '@.data.leasetime')"
dhcp_host="$(jget '@.data.hostname')"
pub "lease" "$lease"
pub "dhcp_hostname" "$dhcp_host"
clear_slots "dns" "$CLEAR_DNS_SLOTS"
dns_list="$(echo "$IFJSON" | jsonfilter -e '@["dns-server"][@]' 2>/dev/null)"
i=0
for d in $dns_list; do
pub "dns-$i" "$d"
i=$((i + 1))
done
clear_slots "ip6" "$CLEAR_IP6_SLOTS"
ip6_list="$(echo "$IFJSON" | jsonfilter -e '@["ipv6-address"][@].address' 2>/dev/null)"
i=0
for a in $ip6_list; do
pub "ip6-$i" "$a"
i=$((i + 1))
done
}
publish_modem_signal() {
MSJSON="$(ubus call modem.signal get_signals "{\"time\":$MODEM_SIGNAL_TIME}" 2>/dev/null)"
[ -n "$MSJSON" ] || return
network_type="$(echo "$MSJSON" | jsonfilter -e '@.signals[0].network_type' 2>/dev/null)"
rssi="$(echo "$MSJSON" | jsonfilter -e '@.signals[0].rssi' 2>/dev/null)"
rsrp="$(echo "$MSJSON" | jsonfilter -e '@.signals[0].rsrp' 2>/dev/null)"
rsrq="$(echo "$MSJSON" | jsonfilter -e '@.signals[0].rsrq' 2>/dev/null)"
sinr="$(echo "$MSJSON" | jsonfilter -e '@.signals[0].sinr' 2>/dev/null)"
strength="$(echo "$MSJSON" | jsonfilter -e '@.signals[0].strength' 2>/dev/null)"
mode="$(echo "$MSJSON" | jsonfilter -e '@.signals[0].mode' 2>/dev/null)"
ts="$(echo "$MSJSON" | jsonfilter -e '@.signals[0].timestamp' 2>/dev/null)"
slot="$(echo "$MSJSON" | jsonfilter -e '@.signals[0].slot' 2>/dev/null)"
pub "rat" "$network_type"
pub "signal-rssi" "$rssi"
pub "signal-rsrp" "$rsrp"
pub "signal-rsrq" "$rsrq"
pub "signal-sinr" "$sinr"
pub "signal-bars" "$strength"
pub "signal-mode" "$mode"
pub "signal-slot" "$slot"
pub "signal-timestamp" "$ts"
}
publish_carrier() {
ubus call AT get_result '{"cmd":"AT+COPS=3,0","timeout":3}' >/dev/null 2>&1
AJ="$(ubus call AT get_result '{"cmd":"AT+COPS?","timeout":3}' 2>/dev/null)"
[ -n "$AJ" ] || return
raw="$(echo "$AJ" | jsonfilter -e '@.data' 2>/dev/null | tr -d '\r')"
carrier="$(echo "$raw" | sed -n 's/.*+COPS: [0-9],[0-9],"\([^"]*\)".*/\1/p' | head -n1)"
[ -n "$carrier" ] && pub "carrier" "$carrier"
act="$(echo "$raw" | sed -n 's/.*+COPS: [0-9],[0-9],".*",\([0-9]\+\).*/\1/p' | head -n1)"
[ -n "$act" ] && pub "carrier-act" "$act"
}
publish_sim_info() {
AJ="$(ubus call AT get_result '{"cmd":"AT+QUIMSLOT?","timeout":3}' 2>/dev/null)"
s="$(echo "$AJ" | jsonfilter -e '@.data' 2>/dev/null | tr -d '\r' \
| sed -n 's/.*+QUIMSLOT: \([0-9]\+\).*/\1/p' | head -n1)"
[ -n "$s" ] && pub "sim-slot" "$s"
AJ2="$(ubus call AT get_result '{"cmd":"AT+CCID","timeout":3}' 2>/dev/null)"
iccid="$(echo "$AJ2" | jsonfilter -e '@.data' 2>/dev/null | tr -d '\r' \
| sed -n 's/.*+CCID: \([0-9]\+\).*/\1/p' | head -n1)"
if [ -z "$iccid" ]; then
AJ3="$(ubus call AT get_result '{"cmd":"AT+QCCID","timeout":3}' 2>/dev/null)"
iccid="$(echo "$AJ3" | jsonfilter -e '@.data' 2>/dev/null | tr -d '\r' \
| sed -n 's/.*+QCCID: \([0-9]\+\).*/\1/p' | head -n1)"
fi
[ -n "$iccid" ] && pub "sim-iccid" "$iccid"
}
clear_all() {
pub_abs "$BASE-status" "OFF"
pub "ip4" ""; pub "ip4_mask" ""; pub "gw4" ""
pub "uptime" ""; pub "l3_device" ""; pub "proto" ""; pub "device" ""
pub "metric" ""; pub "dns_metric" ""; pub "delegation" ""
pub "lease" ""; pub "dhcp_hostname" ""
pub "rat" ""; pub "signal-rssi" ""; pub "signal-rsrp" ""; pub "signal-rsrq" ""
pub "signal-sinr" ""; pub "signal-bars" ""; pub "signal-mode" ""
pub "signal-slot" ""; pub "signal-timestamp" ""
pub "carrier" ""; pub "carrier-act" ""
pub "sim-slot" ""; pub "sim-iccid" ""
clear_slots "dns" "$CLEAR_DNS_SLOTS"
clear_slots "ip6" "$CLEAR_IP6_SLOTS"
}
case "$ACTION" in
ifdown)
clear_all
log "ifdown $WAN_IFACE"
;;
ifup|ifupdate)
pub_abs "$BASE-status" "ON"
publish_ifstatus
publish_modem_signal
publish_carrier
publish_sim_info
log "$ACTION $WAN_IFACE published"
;;
esac
exit 0
SH
Make file executable
$ chmod +x /etc/hotplug.d/iface/01-ha-mqtt-wan-and-cell-status
cat >/usr/bin/mqtt_sim_switcher.sh <<'SH'
#!/bin/sh
# MQTT SIM switcher for GL-X3000:
# - Receives SIM slot commands via MQTT
# - Uses GL's /usr/bin/switch_sim_slot (safe + updates GL state + redials)
# - Publishes SIM state (and carrier) to MQTT
# - Watches for SIM changes made in GL UI and republishes state
#
# Shared MQTT config is loaded from: /etc/collectd/mqtt_broker.conf
# Load shared MQTT settings
[ -r /etc/collectd/mqtt_broker.conf ] && . /etc/collectd/mqtt_broker.conf
# ---- LOCAL CONFIG ----
WAN_IFACE="modem_0001_4"
POLL_SECONDS=10 # how often to detect SIM changes from GL UI
# ----------------------
need() { command -v "$1" >/dev/null 2>&1; }
log() { logger -t mqtt-sim-switcher "$*"; }
need mosquitto_sub || exit 1
need mosquitto_pub || exit 1
need ubus || exit 1
need jsonfilter || exit 1
[ -x /usr/bin/switch_sim_slot ] || exit 1
# Validate MQTT config (fail fast)
[ -n "$MQTT_BROKER_HOST" ] || { log "MQTT_BROKER_HOST not set (check /etc/collectd/mqtt_broker.conf)"; exit 1; }
[ -n "$MQTT_PORT" ] || MQTT_PORT="1883"
[ -n "$MQTT_PREFIX" ] || MQTT_PREFIX="collectd"
HOSTNAME="$(uci -q get system.@system[0].hostname)"
[ -n "$HOSTNAME" ] || HOSTNAME="$(hostname 2>/dev/null)"
TOPIC_SET="$MQTT_PREFIX/$HOSTNAME/modem/sim/set"
TOPIC_STATE="$MQTT_PREFIX/$HOSTNAME/modem/sim/state"
TOPIC_CARRIER="$MQTT_PREFIX/$HOSTNAME/modem/sim/carrier"
TOPIC_RESULT="$MQTT_PREFIX/$HOSTNAME/modem/sim/result"
AUTH_ARGS="-h $MQTT_BROKER_HOST -p $MQTT_PORT"
[ -n "$MQTT_USERNAME" ] && AUTH_ARGS="$AUTH_ARGS -u $MQTT_USERNAME"
[ -n "$MQTT_PASSWORD" ] && AUTH_ARGS="$AUTH_ARGS -P $MQTT_PASSWORD"
[ -n "$MQTT_TLS_PROTOCOL" ] && AUTH_ARGS="$AUTH_ARGS --tls-version $MQTT_TLS_PROTOCOL"
[ -n "$MQTT_CA_CERT" ] && AUTH_ARGS="$AUTH_ARGS --cafile $MQTT_CA_CERT"
pub_result() {
mosquitto_pub $AUTH_ARGS -t "$TOPIC_RESULT" -r -m "$1" >/dev/null 2>&1 \
|| log "MQTT publish failed: $TOPIC_RESULT"
}
get_sim_slot() {
# Prefer GL state file first
f="/tmp/run/dual_sim/$WAN_IFACE/current_sim"
slot=""
if [ -r "$f" ]; then
slot="$(cat "$f" 2>/dev/null | tr -cd '0-9' | head -c1)"
fi
# Fallback to AT
if [ -z "$slot" ]; then
AJ="$(ubus call AT get_result '{"cmd":"AT+QUIMSLOT?","timeout":3}' 2>/dev/null)"
slot="$(echo "$AJ" | jsonfilter -e '@.data' 2>/dev/null \
| tr -d '\r' \
| sed -n 's/.*+QUIMSLOT: \([0-9]\+\).*/\1/p' \
| head -n1)"
fi
printf '%s' "$slot"
}
get_carrier() {
AJ="$(ubus call AT get_result '{"cmd":"AT+COPS?","timeout":3}' 2>/dev/null)"
raw="$(echo "$AJ" | jsonfilter -e '@.data' 2>/dev/null | tr -d '\r')"
carrier="$(echo "$raw" | sed -n 's/.*+COPS: [0-9],[0-9],"\([^"]*\)".*/\1/p' | head -n1)"
printf '%s' "$carrier"
}
publish_state() {
slot="$(get_sim_slot)"
if [ -n "$slot" ]; then
mosquitto_pub $AUTH_ARGS -t "$TOPIC_STATE" -r -m "$slot" >/dev/null 2>&1 \
|| log "MQTT publish failed: $TOPIC_STATE"
fi
carrier="$(get_carrier)"
if [ -n "$carrier" ]; then
mosquitto_pub $AUTH_ARGS -t "$TOPIC_CARRIER" -r -m "$carrier" >/dev/null 2>&1 \
|| log "MQTT publish failed: $TOPIC_CARRIER"
fi
}
normalize_payload() {
p="$(printf '%s' "$1" | tr -d '\r\n' | sed 's/^ *//; s/ *$//' | tr -d '"')"
[ "$p" = "(null)" ] && p=""
p="$(printf '%s' "$p" | tr 'A-Z' 'a-z')"
printf '%s' "$p"
}
switch_sim() {
sim="$(normalize_payload "$1")"
# Ignore empties
[ -z "$sim" ] && { log "RX empty payload ignored"; return; }
# Accept 1,2,main and a few aliases
case "$sim" in
1|2|main) ;;
sim1|sim_1|sim-1|slot1|slot_1|slot-1|sim\ 1|slot\ 1) sim="1" ;;
sim2|sim_2|sim-2|slot2|slot_2|slot-2|sim\ 2|slot\ 2) sim="2" ;;
*)
pub_result "ERR invalid payload (use 1, 2, or main)"
log "RX invalid payload=[$sim]"
return
;;
esac
log "Switch request: $sim"
pub_result "OK switching to $sim ..."
if [ "$sim" = "main" ]; then
/usr/bin/switch_sim_slot auto sw main >/dev/null 2>&1
rc=$?
else
/usr/bin/switch_sim_slot auto sw "$sim" >/dev/null 2>&1
rc=$?
fi
if [ $rc -ne 0 ]; then
pub_result "ERR switch_sim_slot failed (rc=$rc)"
log "switch_sim_slot failed rc=$rc"
return
fi
publish_state
pub_result "OK switched to $sim"
}
watch_sim_changes() {
last=""
while true; do
slot="$(get_sim_slot)"
if [ -n "$slot" ] && [ "$slot" != "$last" ]; then
last="$slot"
publish_state
log "SIM change detected -> $slot (published)"
fi
sleep "$POLL_SECONDS"
done
}
# Startup publish
publish_state
pub_result "OK listener started"
# Background watcher (captures GL UI switches)
watch_sim_changes &
# Subscribe forever
mosquitto_sub $AUTH_ARGS -v -t "$TOPIC_SET" 2>/dev/null | while read -r topic payload; do
log "RX topic=$topic payload_raw=[$payload]"
switch_sim "$payload"
done
SH
$ chmod +x /etc/init.d/mqtt-sim-switcher
$ /etc/init.d/mqtt-sim-switcher enable
$ /etc/init.d/mqtt-sim-switcher start