├── .github
└── workflows
│ └── c-cpp.yml
├── .gitignore
├── Dockerfile
├── Makefile
├── README.md
├── UNLICENSE
├── alertik.c
├── env_events.c
├── env_events.h
├── events.c
├── events.h
├── log.c
├── log.h
├── media
└── demo.mp4
├── notifiers.c
├── notifiers.h
├── str.c
├── str.h
├── syslog.c
├── syslog.h
├── toolchain
├── armv6.mk
└── toolchain.sh
└── tools
├── Makefile
├── index.html
└── regext.c
/.github/workflows/c-cpp.yml:
--------------------------------------------------------------------------------
1 | #
2 | # Alertik: a tiny 'syslog' server & notification tool for Mikrotik routers.
3 | # This is free and unencumbered software released into the public domain.
4 | #
5 |
6 | name: CI
7 |
8 | on:
9 | push:
10 | branches: [ "master" ]
11 | pull_request:
12 | branches: [ "master" ]
13 |
14 | jobs:
15 | linux:
16 | name: Build for ARMv6 (compatible with v7 and Aarch64), and push to Dockerhub
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - name: Checkout
21 | uses: actions/checkout@v4
22 | - name: Download & extract GCC v11.2/musl (b76f37f) for armv6
23 | run: toolchain/toolchain.sh "download_musl_armv6"
24 | - name: Download & build BearSSL for armv6
25 | run: toolchain/toolchain.sh "download_build_bearssl"
26 | - name: Download & build libcurl for armv6
27 | run: toolchain/toolchain.sh "download_build_libcurl"
28 | - name: Build Alertik for armv6
29 | run: toolchain/toolchain.sh "build_alertik_armv6"
30 |
31 | - name: Set up Docker Buildx
32 | uses: docker/setup-buildx-action@v3
33 | - name: Login to Docker Hub
34 | if: github.event_name == 'push'
35 | uses: docker/login-action@v3
36 | with:
37 | username: ${{ secrets.DOCKERHUB_USERNAME }}
38 | password: ${{ secrets.DOCKERHUB_TOKEN }}
39 |
40 | - name: Build and push Docker image for armv6/v7 and aarch64
41 | uses: docker/build-push-action@v5
42 | with:
43 | context: .
44 | push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
45 | platforms: linux/arm/v6, linux/arm/v7, linux/arm64
46 | tags: ${{ secrets.DOCKERHUB_USERNAME }}/alertik:latest
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | alertik
2 | *.o
3 | toolchain/armv6-linux-musleabi-cross
4 | toolchain/BearSSL
5 | toolchain/curl-8.8.0
6 | toolchain/armv6-musl.tgz
7 | toolchain/curl.tar.xz
8 | log/
9 | log.txt
10 | *~
11 | *.swp
12 | *.swo
13 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM scratch
2 | COPY alertik /alertik
3 | EXPOSE 5140/udp
4 | CMD ["/alertik"]
5 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | #
2 | # Alertik: a tiny 'syslog' server & notification tool for Mikrotik routers.
3 | # This is free and unencumbered software released into the public domain.
4 | #
5 |
6 | CC ?= cc
7 | CFLAGS += -Wall -Wextra -O2
8 | LDLIBS += -pthread -lcurl
9 | STRIP = strip
10 | VERSION = v0.1
11 | OBJS = alertik.o events.o env_events.o notifiers.o log.o syslog.o str.o
12 |
13 | ifeq ($(LOG_FILE),yes)
14 | CFLAGS += -DUSE_FILE_AS_LOG
15 | endif
16 |
17 | # We're cross-compiling?
18 | ifneq ($(CROSS),)
19 | CC = $(CROSS)-linux-musleabi-gcc
20 | STRIP = $(CROSS)-linux-musleabi-strip
21 | ifeq ($(LOG_FILE),)
22 | CFLAGS += -DUSE_FILE_AS_LOG # We don't have stdout...
23 | endif
24 | LDLIBS += -lbearssl
25 | LDFLAGS += -no-pie --static
26 | endif
27 |
28 | GIT_HASH=$(shell git rev-parse --short HEAD 2>/dev/null || echo '$(VERSION)')
29 | CFLAGS += -DGIT_HASH=\"$(GIT_HASH)\"
30 |
31 | .PHONY: all clean
32 |
33 | all: alertik Makefile
34 | $(STRIP) --strip-all alertik
35 |
36 | alertik: $(OBJS)
37 |
38 | clean:
39 | rm -f $(OBJS) alertik
40 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # alertik
2 | [](https://opensource.org/license/unlicense)
3 | [](https://github.com/Theldus/alertik/actions/workflows/c-cpp.yml)
4 |
5 | Alertik is a tiny syslog server and event notifier designed for MikroTik routers.
6 |
7 | https://github.com/Theldus/alertik/assets/8294550/7963be36-268a-458e-9b79-83a466aa85be
8 |
9 | Receiving Telegram notification due to failed WiFi login attempt
10 |
11 |
12 | ## The Problem
13 | MikroTik routers are impressive devices, capable of performing numerous tasks on small, compact, and affordable hardware. However, there is always room for improvement, and certain missing features can leave users feeling restricted. One such limitation is related to logging: RouterOS logs *a lot* of useful information, but it provides *no* means to trigger events based on these logs, which can be quite limiting for users.
14 |
15 | To work around this, many users attempt to write scripts that poll the router's logs, parse the messages, and trigger events or configurations. However, this method proves to be highly unreliable, as evidenced by discussions in:
16 | - [\[PROPOSAL\] Event driven scripting](https://forum.mikrotik.com/viewtopic.php?t=198490)
17 | - [Executed a script on log event](https://forum.mikrotik.com/viewtopic.php?t=184330)
18 | - [Blacklist for failed login to IPSec VPN](https://forum.mikrotik.com/viewtopic.php?t=148397)
19 |
20 | and many others. There are two main issues with this approach:
21 |
22 | 1) **Polling:** How frequently should polling occur for a given event? Will events be missed between polling intervals?
23 | 2) **Timestamp:** Due to polling, scripts need to handle the event timestamps carefully. The script must check the dates of all events that occurred since the last check, which is not trivial because of the RouterOS log's date format, as highlighted in the RouterOS wiki:
24 |
25 | > All messages stored in routers local memory can be printed from /log menu. Each entry contains time and date when event occurred, topics that this message belongs to and message itself. [...] If logs are printed at the same date when log entry was added, **then only time will be shown**. In example above you can see that second message was added on sep/15 current year (year is not added) and the last message was added today so only the time is displayed.
26 |
27 | This makes it incredibly difficult to accurately analyze the logs and trigger events accordingly.
28 |
29 | ## The Solution
30 | Instead of trying to parse RouterOS logs in an elaborate way, a smarter approach to receiving and handling logs is to use a syslog server:
31 | - RouterOS sends messages immediately to the server, eliminating the need for any polling mechanism.
32 | - By using a syslog server, the complete and parseable timestamp of the message can be configured by the server, if date usage is necessary.
33 |
34 | Initially, my idea was to create a Docker image based on Alpine, using rsyslogd, bash scripts, and cURL for receiving logs, parsing them, and sending notifications. However, I noticed something interesting: the 'syslog' from RouterOS is simply UDP packets with raw strings, without any headers, protocols, or anything else—just the log string sent in a UDP packet to the configured syslog server.
35 |
36 | From this point, it seemed excessive to use Alpine, rsyslog, cURL, and shell scripts. So, I decided to write my own C program in the simplest way possible. The result is *Alertik, a single-file static binary, Docker image of just **395 kB**. It even fits in the ridiculous free space of my hAP ac^2 (1 MiB free)!* (Though I recommend using tmpfs.)
37 |
38 | ## How Does It Work?
39 | The operation is quite simple: Alertik listens on the UDP port of your choice (5140 by default) and queues messages in a circular buffer. A second thread then retrieves one message at a time and checks if its substring (or regex) matches a predefined list of handlers. If a match is found, the handler is invoked with the message and the event timestamp. From this point, the user can send notifications to some services (like Telegram, Slack, Discord, and etc) based on these logs.
40 |
41 | All of this is packed into a single 395kB binary, thanks to libcurl, BearSSL, and Musl.
42 |
43 | ## Notifiers
44 | In Alertik, notifiers are the services used to send the notifications. Each notifier can be configured to handle one or more events, and the system is designed to be extensible, allowing for the addition of more notifiers if needed.
45 |
46 | Currently, Alertik supports the following notifiers:
47 |
48 | - **Telegram Bot**
49 | - **Slack WebHook**
50 | - **Microsoft Teams WebHook**
51 | - **Discord WebHook**
52 | - **Generic WebHooks** (4 slots available)
53 |
54 | Each notifier is configured via environment variables. Below is the list of environment variables required for configuring each notifier:
55 |
56 | | Notifier | Environment Variable Name | Description |
57 | |---------------------------|-----------------------------------|------------------------------------------------|
58 | | **Telegram** | `TELEGRAM_BOT_TOKEN` | Token for the Telegram bot. |
59 | | | `TELEGRAM_CHAT_ID` | Chat ID where messages will be sent. |
60 | | **Slack** | `SLACK_WEBHOOK_URL` | WebHook URL for Slack notifications. |
61 | | **Microsoft Teams** | `TEAMS_WEBHOOK_URL` | WebHook URL for Microsoft Teams notifications. |
62 | | **Discord** | `DISCORD_WEBHOOK_URL` | WebHook URL for Discord notifications. |
63 | | **Generic WebHook 1** | `GENERIC1_WEBHOOK_URL` | URL for the first generic webhook. |
64 | | **Generic WebHook 2** | `GENERIC2_WEBHOOK_URL` | URL for the second generic webhook. |
65 | | **Generic WebHook 3** | `GENERIC3_WEBHOOK_URL` | URL for the third generic webhook. |
66 | | **Generic WebHook 4** | `GENERIC4_WEBHOOK_URL` | URL for the fourth generic webhook. |
67 |
68 | For Generic WebHooks, Alertik sends a POST request with the following JSON content to the configured URL:
69 | ```json
70 | {"text": ""}
71 | ```
72 |
73 | ## Environment Events
74 | **Environment Events** offer the simplest way to configure event triggers. By setting a few environment variables, you can easily define how events should work, whether using substring matches or regex patterns. This approach provides a straightforward method for setting up events, and this section will guide you through configuring them with examples for both substring and regex matching.
75 |
76 | ### Configuration Format
77 | The environment variables for configuring events follow this format:
78 |
79 | ```bash
80 | export ENV_EVENTS="2" # Maximum of 16 events (starting from 0)
81 | export EVENT0_NOTIFIER= # Options: Telegram, Slack, Discord, Teams, Generic1 ... Generic4
82 | export EVENT0_MATCH_TYPE="substr" # or "regex"
83 | export EVENT0_MATCH_STR="substring or regex pattern"
84 | export EVENT0_MASK_MSG="message to be sent in case of match"
85 | ...
86 | ```
87 |
88 | In `EVENT0_MASK_MSG`, you can use match groups (up to 32 groups, starting from 1) for custom messages. Use the `@` character to refer to these groups. For example, with a regex pattern:
89 |
90 | ```regex
91 | ether2 link up \(speed (.+), full duplex\)
92 | ```
93 |
94 | You can use the match group in `MASK_MSG` like this:
95 |
96 | ```bash
97 | EVENT0_MASK_MSG="Your link ether2 is up at @1 speed"
98 | ```
99 |
100 | To include an actual `@` character in the message, escape it by typing `@@`. For example:
101 |
102 | ```bash
103 | EVENT0_MASK_MSG="User @1 and @2 were reported to user @@John"
104 | ```
105 |
106 | ### Examples: Substring Matching
107 | #### `1)` **Identify Login Failures**
108 |
109 | **Log Message:**
110 | ```
111 | login failure for user admin
112 | ```
113 |
114 | **Configuration:**
115 | ```bash
116 | export EVENT0_NOTIFIER="Slack"
117 | export EVENT0_MATCH_TYPE="substr"
118 | export EVENT0_MATCH_STR="login failure for user admin"
119 | export EVENT0_MASK_MSG="There is a failed login attempt for user admin"
120 | ```
121 |
122 | #### `2)` **Identify WiFi Login Failures**
123 |
124 | **Log Message:**
125 | ```
126 | 36:7F:7F:07:C4:B0@honeypot: disconnected, unicast key exchange timeout, signal strength -85
127 | ```
128 |
129 | **Configuration:**
130 | ```bash
131 | export EVENT0_NOTIFIER="Telegram"
132 | export EVENT0_MATCH_TYPE="substr"
133 | export EVENT0_MATCH_STR="honeypot: disconnected, unicast key exchange timeout"
134 | export EVENT0_MASK_MSG="There is an attempt to login into your HoneyPot network!"
135 | ```
136 |
137 | ### Examples: Regex Matching
138 | #### `1)` **Identify SSH Login Failures with User and IP Extraction**
139 |
140 | **Log Message:**
141 | ```
142 | login failure for user john_doe from 192.168.1.10 via ssh
143 | ```
144 |
145 | **Configuration:**
146 | ```bash
147 | export EVENT0_NOTIFIER="Discord"
148 | export EVENT0_MATCH_TYPE="regex"
149 | export EVENT0_MATCH_STR="login failure for user ([A-Za-z]+) from (\d{1,3}.*) via ssh"
150 | export EVENT0_MASK_MSG="Alert: failed user attempt to login as @1 from @2"
151 | ```
152 |
153 | #### `2)` **Identify Link Up with Speed Less Than 1Gbps**
154 |
155 | **Log Message:**
156 | ```
157 | eth0 link up (speed 100Mbps, full duplex)
158 | ```
159 |
160 | **Configuration:**
161 | ```bash
162 | export EVENT0_NOTIFIER="Teams"
163 | export EVENT0_MATCH_TYPE="regex"
164 | export EVENT0_MATCH_STR="([a-zA-Z0-9]+) link up \(speed (\d+Mbps), full duplex\)"
165 | export EVENT0_MASK_MSG="Your interface @1 is running at @2"
166 | ```
167 |
168 | #### `3)` **Log Connection Attempts**
169 | To monitor and log incoming connection attempts to your network's PCs, you can configure Alertik to detect such events using a custom firewall rule and a regex pattern. Here’s a step-by-step guide on how to achieve this:
170 |
171 | **1. Configure the Firewall Rule:**
172 | First, set up a firewall rule on your router to log each new incoming connection to any of your machines. This rule also ensures that each source IP is added to an 'ignore' list to prevent duplicate logging for one week. Here's how you can add the rule:
173 |
174 | _a) Detect incoming connections to your router:_
175 | ```bash
176 | /ip/firewall/filter
177 | add action=add-src-to-address-list address-list=ignore_ip_log \
178 | address-list-timeout=1w chain=input comment=\
179 | "Log new incoming connections to any of my machines" \
180 | connection-nat-state="" connection-state=new in-interface=WANinterface \
181 | log=yes src-address-list=!ignore_ip_log
182 | ```
183 | The interesting thing about this rule is that it will detect all connection attempts to your router, regardless of whether those ports are open or not!
184 |
185 | _b) Detects connections that have NAT configured for your machines_
186 | ```bash
187 | /ip/firewall/filter
188 | add action=add-src-to-address-list address-list=ign_ip_log address-list-timeout=\
189 | 1w chain=forward comment=\
190 | "Log new incomming connections to any of my NATed machines" \
191 | connection-nat-state=dstnat connection-state=new in-interface=WANinterface \
192 | log=yes src-address-list=!ign_ip_log
193 | ```
194 |
195 | This rule is especially important as it detects potential attempts to connect to your machines.
196 |
197 | **2. Define the Regex Pattern**
198 | Use the following regex pattern to match log entries for incoming connection attempts. This regex pattern extracts details from the log message, including the source and destination IP addresses and ports:
199 | ```
200 | input: in:.*src-mac [0-9a-f:]+, proto [^,]+, ((\d{1,3}\.?)+):(\d{1,5})->((\d{1,3}\.?)+):(\d{1,5})
201 | ```
202 | and
203 | ```
204 | forward: in:.*src-mac [0-9a-f:]+, proto [^,]+, ((\d{1,3}\.?)+):(\d{1,5})->((\d{1,3}\.?)+):(\d{1,5})
205 | ```
206 |
207 | **Log Messages Examples:**
208 | ```
209 | input: in:WANinterface out:(unknown 0), connection-state:new src-mac 18:3d:5e:79:42:a5, proto TCP (SYN), 192.0.2.1:45624->198.51.100.2:80, len 60
210 | forward: in:WANinterface out:mybridge, connection-state:new,dnat src-mac 18:3d:5e:79:42:a5, proto TCP (SYN), 34.148.164.203:47202->10.0.0.4:22, NAT 34.148.164.203:47202->(192.168.100.30:30292->10.0.0.4:22), len 60
211 | ```
212 |
213 | **Final Configuration:**
214 | ```bash
215 | export EVENT0_NOTIFIER="Telegram"
216 | export EVENT0_MATCH_TYPE="regex"
217 | export EVENT0_MATCH_STR="input: in:.*src-mac [0-9a-f:]+, proto [^,]+, ((\d{1,3}\.?)+):(\d{1,5})->((\d{1,3}\.?)+):(\d{1,5})"
218 | export EVENT0_MASK_MSG="The IP @1:@3 is trying to connect to your router @4:@6, please do something"
219 |
220 | export EVENT1_NOTIFIER="Telegram"
221 | export EVENT1_MATCH_TYPE="regex"
222 | export EVENT1_MATCH_STR="forward: in:.*src-mac [0-9a-f:]+, proto [^,]+, ((\d{1,3}\.?)+):(\d{1,5})->((\d{1,3}\.?)+):(\d{1,5})"
223 | export EVENT1_MASK_MSG="The IP @1:@3 is trying to connect to one of your machines @4:@6, please do something"
224 | ```
225 |
226 | > [!NOTE]
227 | > The regex used in Alertik follows the POSIX Regex Extended syntax. This syntax may vary slightly from patterns used in PCRE2/Perl and other regex implementations. For validation of patterns specifically for Alertik, you can use the regex validator at [https://theldus.github.io/alertik](https://theldus.github.io/alertik). Regex patterns that match in this tool are guaranteed to work correctly in Alertik.
228 |
229 | ## Static Events
230 | **Static Events** offer a more complex event handling mechanism compared to Environment Events. These events are predefined in the source code of Alertik and can support advanced functionalities, such as tracking a certain number of similar events within a specified time window or handling events with specific values.
231 |
232 | Similar to Environment Events, Static Events are configured through environment variables. However, their configuration options are more limited since their core logic is already implemented in the source code.
233 |
234 | ### Configuration
235 | To enable and configure Static Events, use the following environment variables:
236 |
237 | ```bash
238 | export STATIC_EVENTS_ENABLED="0,3,5..."
239 | ```
240 | Each number in the list corresponds to a static event that will be enabled.
241 |
242 | For each enabled event, specify the notifier to be used:
243 |
244 | ```bash
245 | export STATIC_EVENT0_NOTIFIER=Telegram
246 | export STATIC_EVENT3_NOTIFIER=Telegram
247 | export STATIC_EVENT5_NOTIFIER=Slack
248 | ...
249 | ```
250 |
251 | ### Available Static Events
252 | Currently, there is only one static event available:
253 |
254 | - **Event 0: `handle_wifi_login_attempts`**
255 | This event monitors logs for failed login attempts to any Wi-Fi network. When such attempts are detected, the event sends a report containing the Wi-Fi network name and the MAC address of the device.
256 |
257 | Future versions of Alertik may include additional static events, and users have the option to add custom events directly in the source code.
258 |
259 | ### Adding New Static Events
260 | Adding Static Events can be done in three simple steps. For example, if you want to detect login events and send notifications for them:
261 |
262 | ```bash
263 | system,info,account user admin logged in from 10.0.0.245 via winbox
264 | ```
265 |
266 | 1. Increment the number of events in `events.h`, as shown below:
267 | ```diff
268 | diff --git a/events.h b/events.h
269 | index 49b4826..ab5f079 100644
270 | --- a/events.h
271 | +++ b/events.h
272 | @@ -10,7 +10,7 @@
273 | #include
274 |
275 | #define MSG_MAX 2048
276 | - #define NUM_EVENTS 1
277 | + #define NUM_EVENTS 2
278 | ```
279 |
280 | 2. Add your event handler to the list of handlers, along with the substring to be searched:
281 | ```diff
282 | diff --git a/events.c b/events.c
283 | index ce20e38..c289dc3 100644
284 | --- a/events.c
285 | +++ b/events.c
286 | @@ -26,6 +26,7 @@ static regmatch_t pmatch[MAX_MATCHES];
287 |
288 | /* Handlers. */
289 | static void handle_wifi_login_attempts(struct log_event *, int);
290 | +static void handle_admin_login(struct log_event *, int);
291 | struct static_event static_events[NUM_EVENTS] = {
292 | /* Failed login attempts. */
293 | {
294 | @@ -36,6 +37,11 @@ struct static_event static_events[NUM_EVENTS] = {
295 | .ev_notifier_idx = NOTIFY_IDX_TELE
296 | },
297 | /* Add new handlers here. */
298 | + {
299 | + .ev_match_str = "user admin logged in from",
300 | + .hnd = handle_admin_login,
301 | + .ev_match_type = EVNT_SUBSTR
302 | + }
303 | };
304 | ```
305 |
306 | 3. Add your handler (since Alertik uses libcurl, you can also easily adapt the code to send GET/POST requests to any other similar service):
307 | ```c
308 | static void handle_admin_login(struct log_event *ev, int idx_env)
309 | {
310 | struct notifier *self;
311 | int notif_idx;
312 |
313 | log_msg("Event message: %s\n", ev->msg);
314 | log_msg("Event timestamp: %d\n", ev->timestamp);
315 |
316 | notif_idx = static_events[idx_env].ev_notifier_idx;
317 | self = ¬ifiers[notif_idx];
318 |
319 | if (self->send_notification(self, ev->msg) < 0) {
320 | log_msg("unable to send the notification!\n");
321 | return;
322 | }
323 | }
324 | ```
325 |
326 | ## Forward Mode
327 | **Forward Mode** is designed for scenarios where an existing syslog server is already in use with RouterOS. This feature allows Alertik to forward received log messages without any modifications to a specified syslog server. This is particularly useful for integrating Alertik into an existing logging infrastructure while still benefiting from its event-triggering capabilities.
328 |
329 | To enable Forward Mode, configure the following environment variables:
330 |
331 | ```bash
332 | export FORWARD_HOST=
333 | export FORWARD_PORT=
334 | ```
335 |
336 | - **`FORWARD_HOST`**: Specify the IP address (IPv4 or IPv6) or domain name of the syslog server to which messages should be forwarded.
337 | - **`FORWARD_PORT`**: Define the port number on which the syslog server is listening for incoming messages.
338 |
339 | ## Setup in RouterOS
340 | Using Alertik is straightforward: simply configure your RouterOS to download the latest Docker image from [theldus/alertik:latest](https://hub.docker.com/repository/docker/theldus/alertik/tags) and set/export the environment variables related to the Notifiers and Environment/Static Events you want to configure.
341 |
342 | The general procedure is similar for any Docker image (click to expand):
343 |
344 | - Create a virtual interface for the Docker container (e.g., veth1-something)
345 | - Create (or use an existing) bridge for the newly created interface.
346 | - Create a small tmpfs, 5M is more than sufficient (remember: tmpfs only uses memory when needed, you won't actually use 5M of memory...)
347 | - Configure the IP for the syslog server.
348 | - Select the topics to be sent to the syslog server.
349 | - Configure a mount point for the Alertik logs: /tmpfs/log -> /log
350 | - Set the environment variables for Notifiers and Environment/Static Events.
351 | - Configure the Docker registry to: `https://registry-1.docker.io`
352 | - Finally, add the Docker image, pointing to: `theldus/alertik:latest`.
353 |
354 | Below is the complete configuration for my environment, for reference:
355 | ```bash
356 | # Virtual interface creation
357 | /interface veth add address=/24 gateway= gateway6="" name=veth1-docker
358 | # Bridge configuration
359 | /interface bridge port add bridge= interface=veth1-docker
360 | # Create a small tmpfs
361 | /disk add slot=tmpfs tmpfs-max-size=5000000 type=tmpfs
362 | # Syslog server IP configuration
363 | /system logging action add name=rsyslog remote= remote-port=5140 target=remote
364 | # Topics to send to the syslog server
365 | /system logging add action=rsyslog topics=info
366 | # Configure rsyslog server
367 | /system logging action add name=rsyslog remote= remote-port=5140 target=remote
368 | # Mountpoint configuration
369 | /container mounts add dst=/log name=logmount src=/tmpfs/log
370 |
371 | # Docker environment variables configuration for Telegram/Slack/Discord/Teams and/or Generic events
372 | /container envs
373 | add key=TELEGRAM_BOT_TOKEN name=alertik value=
374 | add key=TELEGRAM_CHAT_ID name=alertik value=
375 | ...
376 |
377 | # Add some event, such as identifying login failures via SSH
378 | /container envs
379 | add key=EVENT0_NOTIFIER name=alertik value="Telegram"
380 | add key EVENT0_MATCH_TYPE name=alertik value="substr"
381 | add key EVENT0_MATCH_STR name=alertik value=="login failure for user admin"
382 | add key EVENT0_MASK_MSG name=alertik value="There is a failed login attempt for user admin"
383 |
384 | # Docker Hub registry configuration
385 | /container config set registry-url=https://registry-1.docker.io tmpdir=tmpfs
386 | ```
387 |
388 | and finally:
389 | ```bash
390 | # Add Docker image
391 | /container
392 | add remote-image=theldus/alertik:latest envlist=alertik interface=veth1-docker mounts=logmount root-dir=tmpfs/alertik workdir=/
393 | # Monitor the status
394 | /container print
395 | # Run the image
396 | /container start 0
397 | ```
398 |
399 |
400 | This might seem overwhelming, but trust me: **it is simple**.
401 |
402 | Every step described above is the same process for any Docker image to be used on MikroTik. Therefore, I recommend getting familiar with Docker/container configurations before working with alertik. For this, there are at least three excellent videos from MikroTik on the subject:
403 | - [Impossible, docker containers on Mikrotik? Part 1](https://www.youtube.com/watch?v=8u1PVouAGnk)
404 | - [Docker containers on Mikrotik? Part 2: PiHole](https://www.youtube.com/watch?v=UMcJs4oyHDk)
405 | - [Temporary container in the RAM (tmpfs) - a lifehack for low-cost MikroTik routers](https://www.youtube.com/watch?v=KO9wbarVPOk)
406 |
407 | ### Logging
408 | Logging is the primary method for debugging Alertik. The system logs all operations, and any issues are likely to be reflected in the log file. To retrieve the log file, copy from the RouterOS to your local computer:
409 |
410 | ```bash
411 | $ scp admin@:/tmpfs/log/log.txt .
412 | ```
413 |
414 | (Detailed instructions on creating a mount-point with `tmpfs` were provided earlier.)
415 |
416 | Although not main purpose, Alertik logs can also serve as a replacement for the default RouterOS logs, bypassing limitations such as the default message count restriction.
417 |
418 | ## Build Instructions
419 | The easiest and recommended way to build Alertik is via the Docker image available at: [theldus/alertik:latest], compatible with armv6, armv7, and aarch64. However, if you prefer to build it manually, the process is straightforward since the toolchain setup is already fully scripted:
420 | ```bash
421 | $ git clone https://github.com/Theldus/alertik.git
422 | $ cd alertik/
423 | $ toolchain/toolchain.sh "download_musl_armv6"
424 | $ toolchain/toolchain.sh "download_build_bearssl"
425 | $ toolchain/toolchain.sh "download_build_libcurl"
426 |
427 | # Export the toolchain to your PATH
428 | $ export PATH=$PATH:$PWD/toolchain/armv6-linux-musleabi-cross/bin
429 |
430 | # Build
431 | $ make CROSS=armv6
432 | armv6-linux-musleabi-gcc -Wall -Wextra -O2 -DUSE_FILE_AS_LOG -DGIT_HASH=\"4e82617\" -c -o alertik.o alertik.c
433 | armv6-linux-musleabi-gcc -Wall -Wextra -O2 -DUSE_FILE_AS_LOG -DGIT_HASH=\"4e82617\" -c -o events.o events.c
434 | armv6-linux-musleabi-gcc -Wall -Wextra -O2 -DUSE_FILE_AS_LOG -DGIT_HASH=\"4e82617\" -c -o env_events.o env_events.c
435 | armv6-linux-musleabi-gcc -Wall -Wextra -O2 -DUSE_FILE_AS_LOG -DGIT_HASH=\"4e82617\" -c -o notifiers.o notifiers.c
436 | armv6-linux-musleabi-gcc -Wall -Wextra -O2 -DUSE_FILE_AS_LOG -DGIT_HASH=\"4e82617\" -c -o log.o log.c
437 | armv6-linux-musleabi-gcc -Wall -Wextra -O2 -DUSE_FILE_AS_LOG -DGIT_HASH=\"4e82617\" -c -o syslog.o syslog.c
438 | armv6-linux-musleabi-gcc -Wall -Wextra -O2 -DUSE_FILE_AS_LOG -DGIT_HASH=\"4e82617\" -c -o str.o str.c
439 | armv6-linux-musleabi-gcc -no-pie --static alertik.o events.o env_events.o notifiers.o log.o syslog.o str.o -pthread -lcurl -lbearssl -o alertik
440 | armv6-linux-musleabi-strip --strip-all alertik
441 |
442 | $ ls -lah alertik
443 | -rwxr-xr-x 1 david users 395K Aug 5 22:39 alertik
444 | ```
445 |
446 | To generate the Docker image, ensure you have the [buildx] extension installed:
447 | ```bash
448 | $ docker buildx build --platform linux/arm/v6,linux/arm/v7,linux/arm64 . --tag theldus/alertik:latest
449 | [+] Building 6.9s (5/5) FINISHED docker-container:multi-platform-builder
450 | => [internal] booting buildkit 3.4s
451 | => => starting container buildx_buildkit_multi-platform-builder0 3.4s
452 | => [internal] load build definition from Dockerfile 0.2s
453 | => => transferring dockerfile: 105B 0.0s
454 | => [internal] load .dockerignore 0.2s
455 | => => transferring context: 2B 0.0s
456 | => [internal] load build context 0.4s
457 | => => transferring context: 362.79kB 0.0s
458 | => [linux/arm/v6 1/1] COPY alertik /alertik 0.7s
459 | WARNING: No output specified with docker-container driver. Build result will only remain in the build cache. To push result image into registry use --push or to load image into docker use --load
460 | ```
461 |
462 | ## Security Notice
463 | Running a Docker image on your router can be a cause for concern. It is not advisable to blindly trust readily available Docker images, especially when it comes to sensitive devices like routers. With this in mind, all Docker images provided in this repository are exclusively pushed to Dockerhub via Github Actions. This means you can audit the entire process from start to finish, ensuring that the downloaded Docker images are exactly as they claim to be.
464 |
465 | [Incidents like the one involving libxz](https://tukaani.org/xz-backdoor/) must not be repeated. Trust should not be placed in manual uploads, whether of binaries or source code, when there are available alternatives.
466 |
467 | ## Contributing
468 | Alertik is always open to the community and willing to accept contributions, whether with issues, documentation, testing, new features, bugfixes, typos, and etc. Welcome aboard.
469 |
470 | ## License
471 | Alertik is a public domain project licensed under Unlicense.
472 |
473 | [theldus/alertik:latest]: https://hub.docker.com/repository/docker/theldus/alertik/tags
474 | [buildx]: https://github.com/docker/buildx
475 |
--------------------------------------------------------------------------------
/UNLICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
25 |
--------------------------------------------------------------------------------
/alertik.c:
--------------------------------------------------------------------------------
1 | /*
2 | * Alertik: a tiny 'syslog' server & notification tool for Mikrotik routers.
3 | * This is free and unencumbered software released into the public domain.
4 | */
5 |
6 | #define _POSIX_C_SOURCE 200809L
7 | #include
8 | #include
9 |
10 | #include "events.h"
11 | #include "env_events.h"
12 | #include "log.h"
13 | #include "notifiers.h"
14 | #include "syslog.h"
15 |
16 | /*
17 | * Alertik
18 | */
19 |
20 | static void *handle_messages(void *p)
21 | {
22 | ((void)p);
23 |
24 | int handled = 0;
25 | struct log_event ev = {0};
26 |
27 | while (syslog_pop_msg_from_fifo(&ev) >= 0) {
28 | print_log_event(&ev);
29 |
30 | if (!is_within_notify_threshold()) {
31 | log_msg("ignoring, reason: too many notifications!\n");
32 | continue;
33 | }
34 |
35 | handled = process_static_event(&ev);
36 | handled += process_environment_event(&ev);
37 |
38 | if (handled)
39 | update_notify_last_sent();
40 | else
41 | log_msg("> Not handled!\n");
42 | }
43 | return NULL;
44 | }
45 |
46 | int main(void)
47 | {
48 | pthread_t handler;
49 | int ret;
50 | int fd;
51 |
52 | log_init();
53 |
54 | log_msg(
55 | "Alertik (" GIT_HASH ") (built at " __DATE__ " " __TIME__ ")\n");
56 | log_msg(" (https://github.com/Theldus/alertik)\n");
57 | log_msg("-------------------------------------------------\n");
58 |
59 | ret = init_static_events();
60 | ret += init_environment_events();
61 | if (!ret)
62 | panic("No event was configured, please configure at least one\n"
63 | "before proceeding!\n");
64 |
65 | syslog_init_forward();
66 |
67 | fd = syslog_create_udp_socket();
68 | if (pthread_create(&handler, NULL, handle_messages, NULL))
69 | panic_errno("Unable to create hanler thread!");
70 |
71 | log_msg("Waiting for messages at :%d (UDP)...\n", SYSLOG_PORT);
72 |
73 | while (syslog_enqueue_new_upd_msg(fd) >= 0);
74 | return EXIT_SUCCESS;
75 | }
76 |
--------------------------------------------------------------------------------
/env_events.c:
--------------------------------------------------------------------------------
1 | /*
2 | * Alertik: a tiny 'syslog' server & notification tool for Mikrotik routers.
3 | * This is free and unencumbered software released into the public domain.
4 | */
5 |
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 |
13 | #include "log.h"
14 | #include "events.h"
15 | #include "env_events.h"
16 | #include "notifiers.h"
17 | #include "str.h"
18 |
19 | /*
20 | * Environment events
21 | */
22 |
23 | /* Regex params. */
24 | #define MAX_MATCHES 32
25 |
26 | /* Event match types. */
27 | #define MATCH_TYPES_LEN 2
28 | static const char *const match_types[] = {"substr", "regex"};
29 |
30 | /* Environment events list. */
31 | static int num_env_events;
32 | struct env_event env_events[MAX_ENV_EVENTS] = {0};
33 |
34 | /**
35 | * Safe string-to-int routine that takes into account:
36 | * - Overflow and Underflow
37 | * - No undefined behavior
38 | *
39 | * Taken from https://stackoverflow.com/a/12923949/3594716
40 | * and slightly adapted: no error classification, because
41 | * I don't need to know, error is error.
42 | *
43 | * @param out Pointer to integer.
44 | * @param s String to be converted.
45 | *
46 | * @return Returns 0 if success and a negative number otherwise.
47 | */
48 | static int str2int(int *out, const char *s)
49 | {
50 | char *end;
51 | if (s[0] == '\0' || isspace(s[0]))
52 | return -1;
53 | errno = 0;
54 |
55 | long l = strtol(s, &end, 10);
56 |
57 | /* Both checks are needed because INT_MAX == LONG_MAX is possible. */
58 | if (l > INT_MAX || (errno == ERANGE && l == LONG_MAX))
59 | return -1;
60 | if (l < INT_MIN || (errno == ERANGE && l == LONG_MIN))
61 | return -1;
62 | if (*end != '\0')
63 | return -1;
64 |
65 | *out = l;
66 | return 0;
67 | }
68 |
69 | /**
70 | * @brief Retrieves the event string from the environment variables.
71 | *
72 | * @param ev_num Event number.
73 | * @param str String identifier.
74 | *
75 | * @return Returns the event string.
76 | */
77 | static char *get_event_str(int ev_num, char *str)
78 | {
79 | char *env;
80 | char ev[64] = {0};
81 | snprintf(ev, sizeof ev - 1, "EVENT%d_%s", ev_num, str);
82 | if (!(env = getenv(ev)))
83 | panic("Unable to find event for %s\n", ev);
84 | return env;
85 | }
86 |
87 | /**
88 | * @brief Retrieves the index of the event from the environment variables.
89 | *
90 | * @param ev_num Event number.
91 | * @param str String identifier.
92 | * @param str_list List of strings to match against.
93 | * @param size Size of the string list.
94 | *
95 | * @return Returns the index of the matching event.
96 | */
97 | static int
98 | get_event_idx(int ev_num, char *str, const char *const *str_list, int size)
99 | {
100 | char *env = get_event_str(ev_num, str);
101 | for (int i = 0; i < size; i++) {
102 | if (!strcmp(env, str_list[i]))
103 | return i;
104 | }
105 | panic("String parameter (%s) invalid for %s\n", env, str);
106 | }
107 |
108 | /**
109 | * @brief Handles match replacement in the event mask message.
110 | *
111 | * @param notif_message Pointer to the append buffer.
112 | * @param c_msk Pointer to the current position in the mask message.
113 | * @param e_msk End of the mask message.
114 | * @param pmatch Array of regex matches.
115 | * @param env Pointer to the environment event.
116 | * @param log_ev Pointer to the log event.
117 | *
118 | * @return Returns 1 if the replacement was handled, 0 otherwise.
119 | */
120 | static int handle_match_replacement(
121 | struct str_ab *notif_message,
122 | const char **c_msk, const char *e_msk,
123 | regmatch_t *pmatch,
124 | struct env_event *env,
125 | struct log_event *log_ev)
126 | {
127 | const char *c = *c_msk;
128 | const char *e = e_msk;
129 | size_t match = *c - '0';
130 | regoff_t off;
131 | regoff_t len;
132 |
133 | /* Check if there is a second digit. */
134 | if (c < e) {
135 | if (c[1] >= '0' && c[1] <= '9') {
136 | match = (match * 10) + (c[1] - '0');
137 | c++;
138 | }
139 | }
140 |
141 | /* Validate if read number is within the match range
142 | * i.e., between 1-nsub.
143 | */
144 | if (!match || match > env->regex.re_nsub)
145 | return 0;
146 |
147 | *c_msk = c;
148 |
149 | /* Append c_msk into our dst, according to the informed
150 | * match.
151 | */
152 | off = pmatch[match].rm_so;
153 | len = pmatch[match].rm_eo - off;
154 |
155 | if (ab_append_str(notif_message, log_ev->msg + off, len) < 0)
156 | return 0;
157 |
158 | return 1;
159 | }
160 |
161 | /**
162 | * @brief Creates a masked message based on the base mask string
163 | * and the matches found.
164 | *
165 | * @param env Pointer to the environment event.
166 | * @param pmatch Array of regex matches.
167 | * @param log_ev Pointer to the log event.
168 | * @param buf Buffer to store the masked message.
169 | * @param buf_size Size of the buffer.
170 | *
171 | * @return Returns the pointer to the end of the masked message.
172 | */
173 | static int
174 | create_masked_message(struct env_event *env, regmatch_t *pmatch,
175 | struct log_event *log_ev, struct str_ab *notif_message)
176 | {
177 | const char *c_msk, *e_msk;
178 |
179 | c_msk = env->ev_mask_msg;
180 | e_msk = c_msk + strlen(c_msk);
181 |
182 | for (; *c_msk != '\0'; c_msk++)
183 | {
184 | if (*c_msk != '@') {
185 | if (ab_append_chr(notif_message, *c_msk) < 0)
186 | break;
187 | continue;
188 | }
189 |
190 | /* Abort if there is no next char to look ahead. */
191 | else if (c_msk + 1 >= e_msk)
192 | break;
193 |
194 | /* Look next char, in order to escape it if needed.
195 | * If next is also '@',escape it.
196 | */
197 | if (c_msk[1] == '@') {
198 | if (ab_append_chr(notif_message, *c_msk) < 0)
199 | break;
200 | else {
201 | c_msk++;
202 | continue; /* skip next char (since we already read it). */
203 | }
204 | }
205 |
206 | /* If not a number, abort. */
207 | else if (!(c_msk[1] >= '0' && c_msk[1] <= '9')) {
208 | log_msg("Warning: expected number at input, but found (%c), "
209 | "the resulting message will be incomplete!\n",
210 | c_msk[1]);
211 | break;
212 | }
213 |
214 | /* Its a number, proceed the replacement. */
215 | else
216 | {
217 | c_msk++;
218 | if (!handle_match_replacement(notif_message, &c_msk, e_msk,
219 | pmatch, env, log_ev))
220 | {
221 | break;
222 | }
223 | }
224 | }
225 |
226 | /* If we could parse the entire mask. */
227 | return (*c_msk == '\0');
228 | }
229 |
230 | /**
231 | * @brief Handles a log event with a regex match.
232 | *
233 | * @param ev Pointer to the log event.
234 | * @param idx_env Index of the environment event.
235 | *
236 | * @return Returns 1 if the event was handled, 0 otherwise.
237 | */
238 | static int handle_regex(struct log_event *ev, int idx_env)
239 | {
240 | char time_str[32] = {0};
241 | regmatch_t pmatch[MAX_MATCHES] = {0};
242 | struct str_ab notif_message;
243 | struct notifier *self;
244 |
245 | int ret;
246 | int notif_idx;
247 | struct env_event *env_ev;
248 |
249 | env_ev = &env_events[idx_env];
250 | notif_idx = env_ev->ev_notifier_idx;
251 | self = ¬ifiers[notif_idx];
252 |
253 | if (regexec(&env_ev->regex, ev->msg, MAX_MATCHES, pmatch, 0) == REG_NOMATCH)
254 | return 0;
255 |
256 | log_msg("> Environment event detected!\n");
257 | log_msg("> type : regex\n");
258 | log_msg("> expr : %s\n", env_ev->ev_match_str);
259 | log_msg("> amnt sub expr: %zu\n", env_ev->regex.re_nsub);
260 | log_msg("> notifier : %s\n", notifiers_str[notif_idx]);
261 |
262 | ab_init(¬if_message);
263 |
264 | /* Check if there are any subexpressions, if not, just format
265 | * the message.
266 | */
267 | if (env_ev->regex.re_nsub) {
268 | if (!create_masked_message(env_ev, pmatch, ev, ¬if_message)) {
269 | log_msg("Unable to create masked message!\n");
270 | return 0;
271 | }
272 |
273 | if (ab_append_fmt(¬if_message, ", at: %s",
274 | get_formatted_time(ev->timestamp, time_str)))
275 | {
276 | return 0;
277 | }
278 | }
279 |
280 | else {
281 | ret = ab_append_fmt(¬if_message,
282 | "%s, at: %s",
283 | env_ev->ev_mask_msg,
284 | get_formatted_time(ev->timestamp, time_str));
285 |
286 | if (ret)
287 | return 0;
288 | }
289 |
290 | if (self->send_notification(self, notif_message.buff) < 0) {
291 | log_msg("unable to send the notification through %s\n",
292 | notifiers_str[notif_idx]);
293 | }
294 |
295 | return 1;
296 | }
297 |
298 | /**
299 | * @brief Handles a log event with a substring match.
300 | *
301 | * @param ev Pointer to the log event.
302 | * @param idx_env Index of the environment event.
303 | *
304 | * @return Returns 1 if the event was handled, 0 otherwise.
305 | */
306 | static int handle_substr(struct log_event *ev, int idx_env)
307 | {
308 | int ret;
309 | int notif_idx;
310 | char time_str[32] = {0};
311 |
312 | struct notifier *self;
313 | struct env_event *env_ev;
314 | struct str_ab notif_message;
315 |
316 | env_ev = &env_events[idx_env];
317 | notif_idx = env_ev->ev_notifier_idx;
318 | self = ¬ifiers[notif_idx];
319 |
320 | if (!strstr(ev->msg, env_ev->ev_match_str))
321 | return 0;
322 |
323 | log_msg("> Environment event detected!\n");
324 | log_msg("> type: substr, match: (%s), notifier: %s\n",
325 | env_ev->ev_match_str, notifiers_str[notif_idx]);
326 |
327 | ab_init(¬if_message);
328 |
329 | /* Format the message. */
330 | ret = ab_append_fmt(¬if_message,
331 | "%s, at: %s",
332 | env_ev->ev_mask_msg,
333 | get_formatted_time(ev->timestamp, time_str)
334 | );
335 |
336 | if (ret)
337 | return 0;
338 |
339 | if (self->send_notification(self, notif_message.buff) < 0) {
340 | log_msg("unable to send the notification through %s\n",
341 | notifiers_str[notif_idx]);
342 | }
343 |
344 | return 1;
345 | }
346 |
347 | /**
348 | * @brief Given an environment-variable event, checks if it
349 | * belongs to one of the registered events and then, handle
350 | * it.
351 | *
352 | * @param ev Event to be processed.
353 | *
354 | * @return Returns the amount of matches, 0 if none (not handled).
355 | */
356 | int process_environment_event(struct log_event *ev)
357 | {
358 | int i;
359 | int handled;
360 |
361 | for (i = 0, handled = 0; i < num_env_events; i++) {
362 | if (env_events[i].ev_match_type == EVNT_SUBSTR)
363 | handled += handle_substr(ev, i);
364 | else
365 | handled += handle_regex(ev, i);
366 | }
367 | return handled;
368 | }
369 |
370 | /**
371 | * @brief Initialize environment variables events.
372 | *
373 | * @return Returns 0 if there is no environment event,
374 | * 1 if there is at least one _and_ is successfully
375 | * configured.
376 | */
377 | int init_environment_events(void)
378 | {
379 | struct notifier *self;
380 | char *tmp;
381 | tmp = getenv("ENV_EVENTS");
382 |
383 | if (!tmp || (str2int(&num_env_events, tmp) < 0) || num_env_events <= 0) {
384 | log_msg("Environment events not detected, disabling...\n");
385 | return (0);
386 | }
387 |
388 | if (num_env_events >= MAX_ENV_EVENTS)
389 | panic("Environment ENV_EVENTS exceeds the maximum supported (%d/%d)\n",
390 | num_env_events, MAX_ENV_EVENTS);
391 |
392 | log_msg("%d environment event(s) found, registering...\n", num_env_events);
393 | for (int i = 0; i < num_env_events; i++) {
394 | /* EVENTn_MATCH_TYPE. */
395 | env_events[i].ev_match_type = get_event_idx(i, "MATCH_TYPE",
396 | match_types, MATCH_TYPES_LEN);
397 | /* EVENTn_NOTIFIER. */
398 | env_events[i].ev_notifier_idx = get_event_idx(i, "NOTIFIER",
399 | notifiers_str, NUM_NOTIFIERS);
400 | /* EVENTn_MATCH_STR. */
401 | env_events[i].ev_match_str = get_event_str(i, "MATCH_STR");
402 | /* EVENTn_MASK_MSG. */
403 | env_events[i].ev_mask_msg = get_event_str(i, "MASK_MSG");
404 | }
405 |
406 | log_msg("Environment events summary:\n");
407 | for (int i = 0; i < num_env_events; i++)
408 | {
409 | log_msg("EVENT%d_MATCH_TYPE: %s\n", i,
410 | match_types[env_events[i].ev_match_type]);
411 | log_msg("EVENT%d_MATCH_STR: %s\n", i, env_events[i].ev_match_str);
412 | log_msg("EVENT%d_NOTIFIER: %s\n", i,
413 | notifiers_str[env_events[i].ev_notifier_idx]);
414 | log_msg("EVENT%d_MASK_MSG: %s\n\n", i, env_events[i].ev_mask_msg);
415 |
416 | /* Try to setup notifier if not yet. */
417 | self = ¬ifiers[env_events[i].ev_notifier_idx];
418 | self->setup(self);
419 |
420 | /* If regex, compile it first. */
421 | if (env_events[i].ev_match_type == EVNT_REGEX) {
422 | if (regcomp(
423 | &env_events[i].regex,
424 | env_events[i].ev_match_str,
425 | REG_EXTENDED))
426 | {
427 | panic("Unable to compile regex (%s) for EVENT%d!!!",
428 | env_events[i].ev_match_str, i);
429 | }
430 | }
431 | }
432 | return 1;
433 | }
434 |
--------------------------------------------------------------------------------
/env_events.h:
--------------------------------------------------------------------------------
1 | /*
2 | * Alertik: a tiny 'syslog' server & notification tool for Mikrotik routers.
3 | * This is free and unencumbered software released into the public domain.
4 | */
5 |
6 | #ifndef ENV_EVENTS_H
7 | #define ENV_EVENTS_H
8 |
9 | #include
10 |
11 | #define MAX_ENV_EVENTS 16
12 | struct log_event;
13 |
14 | struct env_event {
15 | int ev_match_type; /* whether regex or str. */
16 | int ev_notifier_idx; /* Telegram, Discord... */
17 | const char *ev_match_str; /* regex str or substr here. */
18 | const char *ev_mask_msg; /* Mask message to be sent. */
19 | regex_t regex; /* Compiled regex. */
20 | };
21 |
22 | extern int init_environment_events(void);
23 | extern int process_environment_event(struct log_event *ev);
24 |
25 | #endif /* ENV_EVENTS_H */
26 |
--------------------------------------------------------------------------------
/events.c:
--------------------------------------------------------------------------------
1 | /*
2 | * Alertik: a tiny 'syslog' server & notification tool for Mikrotik routers.
3 | * This is free and unencumbered software released into the public domain.
4 | */
5 |
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include "events.h"
13 | #include "notifiers.h"
14 | #include "log.h"
15 | #include "str.h"
16 |
17 | /*
18 | * Static events
19 | */
20 |
21 | #define MIN(a,b) (((a)<(b))?(a):(b))
22 |
23 | /* Misc. */
24 | #define MAX_MATCHES 32
25 | static regmatch_t pmatch[MAX_MATCHES];
26 |
27 | /* Handlers. */
28 | static void handle_wifi_login_attempts(struct log_event *, int);
29 | struct static_event static_events[NUM_EVENTS] = {
30 | /* Failed login attempts. */
31 | {
32 | .ev_match_str = "unicast key exchange timeout",
33 | .hnd = handle_wifi_login_attempts,
34 | .ev_match_type = EVNT_SUBSTR,
35 | .enabled = 0,
36 | .ev_notifier_idx = NOTIFY_IDX_TELE
37 | },
38 | /* Add new handlers here. */
39 | };
40 |
41 | /**
42 | * @brief Retrieves the event string from the environment variables.
43 | *
44 | * @param ev_num Event number.
45 | * @param str String identifier.
46 | *
47 | * @return Returns the event string.
48 | */
49 | static char *get_event_str(long ev_num, char *str)
50 | {
51 | char *env;
52 | char ev[64] = {0};
53 | snprintf(ev, sizeof ev - 1, "STATIC_EVENT%ld_%s", ev_num, str);
54 | if (!(env = getenv(ev)))
55 | panic("Unable to find event for %s\n", ev);
56 | return env;
57 | }
58 |
59 | /**
60 | * @brief Retrieves the index of the event from the environment variables.
61 | *
62 | * @param ev_num Event number.
63 | * @param str String identifier.
64 | * @param str_list List of strings to match against.
65 | * @param size Size of the string list.
66 | *
67 | * @return Returns the index of the matching event.
68 | */
69 | static int
70 | get_event_idx(long ev_num, char *str, const char *const *str_list, int size)
71 | {
72 | char *env = get_event_str(ev_num, str);
73 | for (int i = 0; i < size; i++) {
74 | if (!strcmp(env, str_list[i]))
75 | return i;
76 | }
77 | panic("String parameter (%s) invalid for %s\n", env, str);
78 | }
79 |
80 | /**
81 | * @brief Given an event, checks if it belongs to one of the
82 | * registered events and then, handle it.
83 | *
84 | * @param ev Event to be processed.
85 | *
86 | * @return Returns the amount of matches, 0 if none (not handled).
87 | */
88 | int process_static_event(struct log_event *ev)
89 | {
90 | int i;
91 | int handled;
92 | struct static_event *sta_ev;
93 |
94 | for (i = 0, handled = 0; i < NUM_EVENTS; i++) {
95 | /* Skip not enabled events. */
96 | if (!static_events[i].enabled)
97 | continue;
98 |
99 | sta_ev = &static_events[i];
100 |
101 | if (static_events[i].ev_match_type == EVNT_SUBSTR) {
102 | if (strstr(ev->msg, static_events[i].ev_match_str)) {
103 | static_events[i].hnd(ev, i);
104 | handled += 1;
105 | }
106 | }
107 |
108 | else {
109 | if (regexec(&sta_ev->regex, ev->msg, MAX_MATCHES, pmatch, 0)) {
110 | static_events[i].hnd(ev, i);
111 | handled += 1;
112 | }
113 | }
114 | }
115 | return handled;
116 | }
117 |
118 | /**
119 | * @brief Initialize static events.
120 | *
121 | * @return Returns 0 if there is no static event, and 1 if
122 | * there is at least one _and_ is successfully configured.
123 | */
124 | int init_static_events(void)
125 | {
126 | struct notifier *self;
127 | char *ptr, *end;
128 | long ev;
129 |
130 | /* Check for: STATIC_EVENTS_ENABLED=0,3,5,2... */
131 | ptr = getenv("STATIC_EVENTS_ENABLED");
132 | if (!ptr || ptr[0] == '\0') {
133 | log_msg("Static events not detected, disabling...\n");
134 | return (0);
135 | }
136 |
137 | end = ptr;
138 | errno = 0;
139 |
140 | do
141 | {
142 | ev = strtol(end, &end, 10);
143 | if (errno != 0 || ((ptr == end) && ev == 0))
144 | panic("Unable to parse STATIC_EVENTS_ENABLED, aborting...\n");
145 |
146 | /* Skip whitespaces. */
147 | while (*end != '\0' && isspace(*end))
148 | end++;
149 |
150 | /* Check if ev number is sane. */
151 | if (ev < 0 || ev >= NUM_EVENTS)
152 | panic("Event (%ld) is not valid!, should be between 0-%d\n",
153 | ev, NUM_EVENTS - 1);
154 |
155 | /* Try to retrieve & initialize notifier for the event. */
156 | static_events[ev].ev_notifier_idx =
157 | get_event_idx(ev, "NOTIFIER", notifiers_str, NUM_NOTIFIERS);
158 | static_events[ev].enabled = 1;
159 |
160 | if (*end != ',' && *end != '\0')
161 | panic("Wrong event number in STATIC_EVENTS_ENABLED, aborting...\n");
162 |
163 | } while (*end++ != '\0');
164 |
165 |
166 | log_msg("Static events summary:\n");
167 | for (int i = 0; i < NUM_EVENTS; i++) {
168 | if (!static_events[i].enabled)
169 | continue;
170 |
171 | log_msg("STATIC_EVENT%d : enabled\n", i);
172 | log_msg("STATIC_EVENT%d_NOTIFIER: %s\n\n",
173 | i, notifiers_str[static_events[i].ev_notifier_idx]);
174 |
175 | /* Try to setup notifier if not yet. */
176 | self = ¬ifiers[static_events[i].ev_notifier_idx];
177 | self->setup(self);
178 |
179 | /* If regex, compile it first. */
180 | if (static_events[i].ev_match_type == EVNT_REGEX) {
181 | if (regcomp(
182 | &static_events[i].regex,
183 | static_events[i].ev_match_str,
184 | REG_EXTENDED))
185 | {
186 | panic("Unable to compile regex (%s) for EVENT%d!!!",
187 | static_events[i].ev_match_str, i);
188 | }
189 | }
190 | }
191 | return 1;
192 | }
193 |
194 |
195 | ///////////////////////////////////////////////////////////////////////////////
196 | ///////////////////////////// FAILED LOGIN ATTEMPTS ///////////////////////////
197 | ///////////////////////////////////////////////////////////////////////////////
198 |
199 | /**
200 | * @brief Parses the message pointed by @p msg and saves the
201 | * read mac-address and interface in @p mac_addr and @wifi_iface.
202 | *
203 | * @param msg Buffer to be read and parsed.
204 | * @param wifi_iface Output buffer that will contain the parsed
205 | * device interface.
206 | * @param mac_addr Output buffer that will contain the parsed
207 | * mac address.
208 | *
209 | * @return Returns 0 if success, -1 otherwise.
210 | */
211 | static int
212 | parse_login_attempt_msg(const char *msg, char *wifi_iface, char *mac_addr)
213 | {
214 | size_t len = strlen(msg);
215 | size_t tmp = 0;
216 | size_t at = 0;
217 |
218 | /* Find '@' and the last ' '. */
219 | for (at = 0; at < len && msg[at] != '@'; at++) {
220 | if (msg[at] == ' ')
221 | tmp = at;
222 | }
223 |
224 | if (at == len || !tmp) {
225 | log_msg("unable to parse additional data, ignoring...\n");
226 | return -1;
227 | }
228 |
229 | memcpy(mac_addr, msg + tmp + 1, MIN(at - tmp - 1, 32));
230 |
231 | /*
232 | * Find network name.
233 | * Assuming that the interface name does not have ':'...
234 | */
235 | for (tmp = at + 1; tmp < len && msg[tmp] != ':'; tmp++);
236 | if (tmp == len) {
237 | log_msg("unable to find interface name!, ignoring..\n");
238 | return -1;
239 | }
240 |
241 | memcpy(wifi_iface, msg + at + 1, MIN(tmp - at - 1, 32));
242 | return (0);
243 | }
244 |
245 | /**
246 | * @brief For a given log event @p ev and offset index @p idx_env,
247 | * handle the event and send a notification message to the
248 | * configured notifier.
249 | *
250 | * @param ev Log event structure.
251 | * @param idx_env Event index.
252 | */
253 | static void handle_wifi_login_attempts(struct log_event *ev, int idx_env)
254 | {
255 | char time_str[32] = {0};
256 | char mac_addr[32] = {0};
257 | char wifi_iface[32] = {0};
258 | struct str_ab notif_message;
259 | struct notifier *self;
260 | int notif_idx;
261 | int ret;
262 |
263 | log_msg("> Login attempt detected!\n");
264 |
265 | if (parse_login_attempt_msg(ev->msg, wifi_iface, mac_addr) < 0)
266 | return;
267 |
268 | ab_init(¬if_message);
269 |
270 | /* Send our notification. */
271 | ret = ab_append_fmt(¬if_message,
272 | "There is someone trying to connect "
273 | "to your WiFi: %s, with the mac-address: %s, at:%s",
274 | wifi_iface,
275 | mac_addr,
276 | get_formatted_time(ev->timestamp, time_str)
277 | );
278 |
279 | if (ret)
280 | return;
281 |
282 | log_msg("> Retrieved info, MAC: (%s), Interface: (%s)\n", mac_addr, wifi_iface);
283 |
284 | notif_idx = static_events[idx_env].ev_notifier_idx;
285 | self = ¬ifiers[notif_idx];
286 |
287 | if (self->send_notification(self, notif_message.buff) < 0) {
288 | log_msg("unable to send the notification!\n");
289 | return;
290 | }
291 | }
292 |
293 | ////////////////////////////// YOUR HANDLER HERE //////////////////////////////
294 |
--------------------------------------------------------------------------------
/events.h:
--------------------------------------------------------------------------------
1 | /*
2 | * Alertik: a tiny 'syslog' server & notification tool for Mikrotik routers.
3 | * This is free and unencumbered software released into the public domain.
4 | */
5 |
6 | #ifndef EVENTS_H
7 | #define EVENTS_H
8 |
9 | #include
10 | #include
11 |
12 | #define MSG_MAX 2048
13 | #define NUM_EVENTS 1
14 |
15 | #define EVNT_SUBSTR 0
16 | #define EVNT_REGEX 1
17 |
18 | /* Log event. */
19 | struct log_event {
20 | char msg[MSG_MAX];
21 | time_t timestamp;
22 | };
23 |
24 | struct static_event {
25 | void(*hnd)(struct log_event *, int); /* Event handler. */
26 | const char *ev_match_str; /* Substr or regex to match. */
27 | int ev_match_type; /* Whether substr or regex. */
28 | int ev_notifier_idx; /* Telegram, Discord... */
29 | int enabled; /* Whether if handler enabled or not. */
30 | regex_t regex; /* Compiled regex. */
31 | };
32 |
33 | extern int process_static_event(struct log_event *ev);
34 | extern int init_static_events(void);
35 |
36 | #endif /* EVENTS_H */
37 |
--------------------------------------------------------------------------------
/log.c:
--------------------------------------------------------------------------------
1 | /*
2 | * Alertik: a tiny 'syslog' server & notification tool for Mikrotik routers.
3 | * This is free and unencumbered software released into the public domain.
4 | */
5 |
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 | #include
14 | #include
15 |
16 | #include "events.h"
17 | #include "log.h"
18 |
19 | /*
20 | * Alertik's log routines
21 | */
22 |
23 | static pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER;
24 | static int curr_file;
25 |
26 | /* There should *always* be a corresponding close_log_file() call. */
27 | static inline void open_log_file(void)
28 | {
29 | struct stat sb;
30 | pthread_mutex_lock(&log_mutex);
31 | if (curr_file == STDOUT_FILENO)
32 | return;
33 |
34 | if (stat("log", &sb) < 0) {
35 | if (mkdir("log", 0755) < 0)
36 | return;
37 | }
38 | curr_file = openat(AT_FDCWD, LOG_FILE,
39 | O_WRONLY|O_CREAT|O_APPEND, 0666);
40 |
41 | if (curr_file < 0)
42 | curr_file = STDOUT_FILENO; /* fallback to stdout if can't open. */
43 | }
44 |
45 | /* This should *always* be called *after* a call to open_log_file(). */
46 | static void close_log_file(void)
47 | {
48 | if (curr_file || curr_file == STDOUT_FILENO)
49 | goto out;
50 | fsync(curr_file);
51 | close(curr_file);
52 | out:
53 | pthread_mutex_unlock(&log_mutex);
54 | }
55 |
56 | /**
57 | * @brief Format the current time passed as @p time
58 | * into the buffer @p time_str.
59 | *
60 | * @param time Time to be formated as string.
61 | * @param time_str Output buffer.
62 | *
63 | * @return Returns the output buffer.
64 | */
65 | char *get_formatted_time(time_t time, char *time_str)
66 | {
67 | strftime(
68 | time_str,
69 | 32,
70 | "%Y-%m-%d %H:%M:%S",
71 | localtime(&time)
72 | );
73 | return time_str;
74 | }
75 |
76 | /**
77 | * @brief Receives a formated string and outputs to stdout
78 | * with the current timestamp.
79 | *
80 | * @param fmt String format.
81 | */
82 | void log_msg(const char *fmt, ...)
83 | {
84 | char time_str[32] = {0};
85 | va_list ap;
86 |
87 | open_log_file();
88 | dprintf(curr_file, "[%s] ", get_formatted_time(time(NULL), time_str));
89 | va_start(ap, fmt);
90 | vdprintf(curr_file, fmt, ap);
91 | va_end(ap);
92 | close_log_file();
93 | }
94 |
95 | /**
96 | * @brief For a given log event @p ev, print the log event
97 | * into the log file (wheter stdout or an actual file).
98 | *
99 | * @param ev Log event to be printed.
100 | */
101 | void print_log_event(struct log_event *ev)
102 | {
103 | char time_str[32] = {0};
104 | open_log_file();
105 | dprintf(curr_file, "\n[%s] %s\n",
106 | get_formatted_time(ev->timestamp, time_str), ev->msg);
107 | close_log_file();
108 | }
109 |
110 | /**
111 | * @brief Initializes the logging routines.
112 | */
113 | void log_init(void) {
114 | atexit(close_log_file);
115 | #ifndef USE_FILE_AS_LOG
116 | curr_file = STDOUT_FILENO;
117 | #endif
118 | }
119 |
--------------------------------------------------------------------------------
/log.h:
--------------------------------------------------------------------------------
1 | /*
2 | * Alertik: a tiny 'syslog' server & notification tool for Mikrotik routers.
3 | * This is free and unencumbered software released into the public domain.
4 | */
5 |
6 | #ifndef LOG_H
7 | #define LOG_H
8 |
9 | #include
10 | #include
11 | #include
12 | #include
13 | struct log_event;
14 |
15 | /* Uncomment/comment to enable/disable the following settings. */
16 | // #define USE_FILE_AS_LOG /* stdout if commented. */
17 |
18 | #define panic_errno(s) \
19 | do {\
20 | log_msg("%s: %s", (s), strerror(errno)); \
21 | exit(EXIT_FAILURE); \
22 | } while(0);
23 |
24 | #define panic(...) \
25 | do {\
26 | log_msg(__VA_ARGS__); \
27 | exit(EXIT_FAILURE); \
28 | } while(0);
29 |
30 | #define log_errno(s) log_msg("%s: %s", (s), strerror(errno))
31 |
32 | #define LOG_FILE "log/log.txt"
33 |
34 | extern char *get_formatted_time(time_t time, char *time_str);
35 | extern void print_log_event(struct log_event *ev);
36 | extern void log_msg(const char *fmt, ...);
37 | extern void log_init(void);
38 |
39 | #endif /* LOG_H */
40 |
--------------------------------------------------------------------------------
/media/demo.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Theldus/alertik/7167963abeabf0ef52a589a5810e0f82dcff3b28/media/demo.mp4
--------------------------------------------------------------------------------
/notifiers.c:
--------------------------------------------------------------------------------
1 | /*
2 | * Alertik: a tiny 'syslog' server & notification tool for Mikrotik routers.
3 | * This is free and unencumbered software released into the public domain.
4 | */
5 |
6 | #include
7 | #include
8 | #include
9 | #include
10 |
11 | #include "log.h"
12 | #include "notifiers.h"
13 | #include "str.h"
14 |
15 | /*
16 | * Notification handling/notifiers
17 | */
18 |
19 | struct webhook_data {
20 | char *webhook_url;
21 | const char *env_var;
22 | };
23 |
24 | /* EPOCH in secs of last sent notification. */
25 | static time_t time_last_sent_notify;
26 |
27 | /* Just to omit the print to stdout. */
28 | size_t libcurl_noop_cb(void *ptr, size_t size, size_t nmemb, void *data) {
29 | ((void)ptr);
30 | ((void)data);
31 | return size * nmemb;
32 | }
33 |
34 | /**
35 | * @brief Just updates the time (Epoch) of the last sent
36 | * notify.
37 | */
38 | void update_notify_last_sent(void) {
39 | time_last_sent_notify = time(NULL);
40 | }
41 |
42 | /**
43 | * @brief Checks if the current time is within or not
44 | * the minimal threshold to send a nofication.
45 | *
46 | * @return Returns 1 if within the range (can send nofications),
47 | * 0 otherwise.
48 | */
49 | int is_within_notify_threshold(void) {
50 | return (time(NULL) - time_last_sent_notify) > LAST_SENT_THRESHOLD_SECS;
51 | }
52 |
53 | /**
54 | * @brief Initializes and configures the CURL handle for sending a request.
55 | *
56 | * @param hnd CURL handle.
57 | * @param url Request URL.
58 | * @return Returns 0.
59 | */
60 | static int setopts_get_curl(CURL *hnd, const char *url)
61 | {
62 | curl_easy_setopt(hnd, CURLOPT_URL, url);
63 | curl_easy_setopt(hnd, CURLOPT_NOPROGRESS, 1L);
64 | curl_easy_setopt(hnd, CURLOPT_USERAGENT, CURL_USER_AGENT);
65 | curl_easy_setopt(hnd, CURLOPT_MAXREDIRS, 3L);
66 | curl_easy_setopt(hnd, CURLOPT_WRITEFUNCTION, libcurl_noop_cb);
67 | #ifdef CURL_VERBOSE
68 | curl_easy_setopt(hnd, CURLOPT_VERBOSE, 1L);
69 | #endif
70 | #ifndef VALIDATE_CERTS
71 | curl_easy_setopt(hnd, CURLOPT_SSL_VERIFYPEER, 0L);
72 | #endif
73 | return 0;
74 | }
75 |
76 | /**
77 | * @brief Cleanup all resources used by libcurl, including the handler,
78 | * curl_slist and escape'd chars.
79 | *
80 | * @param hnd curl handler
81 | * @param escape Escape string if any (leave NULL if there's none).
82 | * @param slist String list if any (leave NULL if there's none).
83 | */
84 | static void do_curl_cleanup(CURL *hnd, char *escape, struct curl_slist *slist)
85 | {
86 | curl_free(escape);
87 | curl_slist_free_all(slist);
88 | if (hnd)
89 | curl_easy_cleanup(hnd);
90 | }
91 |
92 | /**
93 | * @brief Finally sends a curl request, check its return code
94 | * and then cleanup the resources allocated.
95 | *
96 | * @param hnd curl handler
97 | * @param escape Escape string if any (leave NULL if there's none).
98 | * @param slist String list if any (leave NULL if there's none).
99 | *
100 | * @return Returns CURLE_OK if success, !CURLE_OK if error.
101 | */
102 | static CURLcode do_curl(CURL *hnd, char *escape, struct curl_slist *slist)
103 | {
104 | long response_code = 0;
105 | CURLcode ret_curl = !CURLE_OK;
106 |
107 | #ifndef DISABLE_NOTIFICATIONS
108 | ret_curl = curl_easy_perform(hnd);
109 | if (ret_curl != CURLE_OK) {
110 | log_msg("> Unable to send request!\n");
111 | goto error;
112 | }
113 | else {
114 | curl_easy_getinfo(hnd, CURLINFO_RESPONSE_CODE, &response_code);
115 | log_msg("> Done!\n", response_code);
116 | if (response_code != 200) {
117 | log_msg("(Info: Response code != 200 (%ld), your message might "
118 | "not be correctly sent!)\n", response_code);
119 | }
120 | }
121 | #endif
122 |
123 | ret_curl = CURLE_OK;
124 | error:
125 | do_curl_cleanup(hnd, escape, slist);
126 | return ret_curl;
127 | }
128 |
129 | /**
130 | * @brief Initializes and configures the CURL handle for sending a POST
131 | * request with a JSON payload.
132 | *
133 | * @param hnd CURL handle.
134 | * @param url Request URL.
135 | * @param json_payload Payload data in JSON format.
136 | *
137 | * @return Returns 0 if successful, 1 otherwise.
138 | */
139 | static int setopts_post_json_curl(CURL *hnd, const char *url,
140 | const char *json_payload, struct curl_slist **slist)
141 | {
142 | struct curl_slist *s = *slist;
143 |
144 | s = NULL;
145 | s = curl_slist_append(s, "Content-Type: application/json");
146 | s = curl_slist_append(s, "Accept: application/json");
147 | if (!s) {
148 | *slist = s;
149 | return 1;
150 | }
151 |
152 | curl_easy_setopt(hnd, CURLOPT_HTTPHEADER, s);
153 | curl_easy_setopt(hnd, CURLOPT_URL, url);
154 | curl_easy_setopt(hnd, CURLOPT_NOPROGRESS, 1L);
155 | curl_easy_setopt(hnd, CURLOPT_USERAGENT, CURL_USER_AGENT);
156 | curl_easy_setopt(hnd, CURLOPT_MAXREDIRS, 3L);
157 | curl_easy_setopt(hnd, CURLOPT_WRITEFUNCTION, libcurl_noop_cb);
158 | #ifdef CURL_VERBOSE
159 | curl_easy_setopt(hnd, CURLOPT_VERBOSE, 1L);
160 | #endif
161 | #ifndef VALIDATE_CERTS
162 | curl_easy_setopt(hnd, CURLOPT_SSL_VERIFYPEER, 0L);
163 | #endif
164 | curl_easy_setopt(hnd, CURLOPT_POSTFIELDS, json_payload);
165 | *slist = s;
166 | return 0;
167 | }
168 |
169 | /**
170 | * @brief Sends a generic webhook POST request with JSON payload in the
171 | * format {"text": "text here"}.
172 | *
173 | * @param url Target webhook URL.
174 | * @param text Text to be sent in the json payload.
175 | *
176 | * @return Returns CURLE_OK if success, 1 if error.
177 | */
178 | static int send_generic_webhook(const char *url, const char *text)
179 | {
180 | CURL *hnd = NULL;
181 | struct curl_slist *s = NULL;
182 | struct str_ab payload_data;
183 | const char *t;
184 |
185 | if (!(hnd = curl_easy_init())) {
186 | log_msg("Failed to initialize libcurl!\n");
187 | return 1;
188 | }
189 |
190 | ab_init(&payload_data);
191 | ab_append_str(&payload_data, "{\"text\":\"", 9);
192 |
193 | /* Append the payload data text while escaping double
194 | * quotes.
195 | */
196 | for (t = text; *t != '\0'; t++) {
197 | if (*t != '"') {
198 | if (ab_append_chr(&payload_data, *t) < 0)
199 | return 1;
200 | }
201 | else {
202 | if (ab_append_str(&payload_data, "\\\"", 2) < 0)
203 | return 1;
204 | }
205 | }
206 |
207 | /* End the string. */
208 | if (ab_append_str(&payload_data, "\"}", 2) < 0)
209 | return 1;
210 |
211 | if (setopts_post_json_curl(hnd, url, payload_data.buff, &s))
212 | return 1;
213 |
214 | log_msg("> Sending notification!\n");
215 | return do_curl(hnd, NULL, s);
216 | }
217 |
218 |
219 | ///////////////////////////////////////////////////////////////////////////////
220 | //////////////////////////////// TELEGRAM /////////////////////////////////////
221 | ///////////////////////////////////////////////////////////////////////////////
222 |
223 | /* Telegram & request settings. */
224 | static char *telegram_bot_token;
225 | static char *telegram_chat_id;
226 |
227 | void setup_telegram(struct notifier *self)
228 | {
229 | static int setup = 0;
230 | if (setup)
231 | return;
232 |
233 | ((void)self);
234 |
235 | telegram_bot_token = getenv("TELEGRAM_BOT_TOKEN");
236 | telegram_chat_id = getenv("TELEGRAM_CHAT_ID");
237 | if (!telegram_bot_token || !telegram_chat_id) {
238 | panic(
239 | "Unable to find env vars, please check if you have all of the "
240 | "following set:\n"
241 | "- TELEGRAM_BOT_TOKEN\n"
242 | "- TELEGRAM_CHAT_ID\n"
243 | );
244 | }
245 | setup = 1;
246 | }
247 |
248 | static int send_telegram_notification(const struct notifier *self, const char *msg)
249 | {
250 | struct str_ab full_request_url;
251 | char *escaped_msg = NULL;
252 | CURL *hnd = NULL;
253 | int ret;
254 |
255 | ((void)self);
256 |
257 | if (!(hnd = curl_easy_init())) {
258 | log_msg("Failed to initialize libcurl!\n");
259 | return -1;
260 | }
261 |
262 | escaped_msg = curl_easy_escape(hnd, msg, 0);
263 | if (!escaped_msg) {
264 | log_msg("> Unable to escape notification message...\n");
265 | do_curl_cleanup(hnd, escaped_msg, NULL);
266 | }
267 |
268 | ab_init(&full_request_url);
269 |
270 | ret = ab_append_fmt(&full_request_url,
271 | "https://api.telegram.org/bot%s/sendMessage?chat_id=%s&text=%s",
272 | telegram_bot_token, telegram_chat_id, escaped_msg);
273 |
274 | if (ret)
275 | return -1;
276 |
277 | setopts_get_curl(hnd, full_request_url.buff);
278 | log_msg("> Sending notification!\n");
279 | return do_curl(hnd, escaped_msg, NULL);
280 | }
281 |
282 | ///////////////////////////////////////////////////////////////////////////////
283 | ///////////////////////////////// GENERIC /////////////////////////////////////
284 | ///////////////////////////////////////////////////////////////////////////////
285 |
286 | void setup_generic_webhook(struct notifier *self)
287 | {
288 | struct webhook_data *data = self->data;
289 |
290 | if (data->webhook_url)
291 | return;
292 |
293 | data->webhook_url = getenv(data->env_var);
294 | if (!data->webhook_url) {
295 | panic("Unable to find env vars, please check if you have set the %s!!\n",
296 | data->env_var);
297 | }
298 | }
299 |
300 | static int send_generic_webhook_notification(
301 | const struct notifier *self, const char *msg)
302 | {
303 | struct webhook_data *data = self->data;
304 | return send_generic_webhook(data->webhook_url, msg);
305 | }
306 |
307 | ///////////////////////////////////////////////////////////////////////////////
308 | ///////////////////////////////// DISCORD /////////////////////////////////////
309 | ///////////////////////////////////////////////////////////////////////////////
310 |
311 | /* Discord in Slack-compatible mode. */
312 | static int send_discord_notification(
313 | const struct notifier *self, const char *msg)
314 | {
315 | struct webhook_data *data = self->data;
316 | struct str_ab url;
317 | ab_init(&url);
318 | if (ab_append_fmt(&url, "%s/slack", data->webhook_url) < 0)
319 | return 1;
320 | return send_generic_webhook(url.buff, msg);
321 | }
322 |
323 | ////////////////////////////////// END ////////////////////////////////////////
324 | ///////////////////////////////////////////////////////////////////////////////
325 |
326 | const char *const notifiers_str[] = {
327 | "Telegram", "Slack", "Teams", "Discord",
328 | "Generic1", "Generic2", "Generic3", "Generic4"
329 | };
330 |
331 | struct notifier notifiers[] = {
332 | /* Telegram. */
333 | {
334 | .setup = setup_telegram,
335 | .send_notification = send_telegram_notification,
336 | },
337 | /* Slack. */
338 | {
339 | .setup = setup_generic_webhook,
340 | .send_notification = send_generic_webhook_notification,
341 | .data = &(struct webhook_data)
342 | {.env_var = "SLACK_WEBHOOK_URL"},
343 | },
344 | /* Teams. */
345 | {
346 | .setup = setup_generic_webhook,
347 | .send_notification = send_generic_webhook_notification,
348 | .data = &(struct webhook_data)
349 | {.env_var = "TEAMS_WEBHOOK_URL"},
350 | },
351 | /*
352 | * Discord:
353 | * Since Discord doesn't follow like the others, we need
354 | * to slightly change the URL before proceeding, so this
355 | * is why its function is not generic!.
356 | */
357 | {
358 | .setup = setup_generic_webhook,
359 | .send_notification = send_discord_notification,
360 | .data = &(struct webhook_data)
361 | {.env_var = "DISCORD_WEBHOOK_URL"},
362 | },
363 |
364 | /* Generic webhook events: 1--4 */
365 | {
366 | .setup = setup_generic_webhook,
367 | .send_notification = send_generic_webhook_notification,
368 | .data = &(struct webhook_data)
369 | {.env_var = "GENERIC1_WEBHOOK_URL"},
370 | },
371 | {
372 | .setup = setup_generic_webhook,
373 | .send_notification = send_generic_webhook_notification,
374 | .data = &(struct webhook_data)
375 | {.env_var = "GENERIC2_WEBHOOK_URL"},
376 | },
377 | {
378 | .setup = setup_generic_webhook,
379 | .send_notification = send_generic_webhook_notification,
380 | .data = &(struct webhook_data)
381 | {.env_var = "GENERIC3_WEBHOOK_URL"},
382 | },
383 | {
384 | .setup = setup_generic_webhook,
385 | .send_notification = send_generic_webhook_notification,
386 | .data = &(struct webhook_data)
387 | {.env_var = "GENERIC4_WEBHOOK_URL"},
388 | },
389 | };
390 |
--------------------------------------------------------------------------------
/notifiers.h:
--------------------------------------------------------------------------------
1 | /*
2 | * Alertik: a tiny 'syslog' server & notification tool for Mikrotik routers.
3 | * This is free and unencumbered software released into the public domain.
4 | */
5 |
6 | #ifndef NOTIFIERS_H
7 | #define NOTIFIERS_H
8 |
9 | /* Uncomment/comment to enable/disable the following settings. */
10 | // #define CURL_VERBOSE
11 | // #define VALIDATE_CERTS
12 | // #define DISABLE_NOTIFICATIONS
13 |
14 | /*
15 | * Notifier indexes.
16 | */
17 | #define NUM_NOTIFIERS 8
18 | #define NOTIFY_IDX_TELE 0
19 | #define NOTIFY_IDX_SLACK 1
20 | #define NOTIFY_IDX_TEAMS 2
21 | #define NOTIFY_IDX_DISCORD 3
22 | #define NOTIFY_IDX_GENRC1 (NUM_NOTIFIERS-4)
23 | #define NOTIFY_IDX_GENRC2 (NUM_NOTIFIERS-3)
24 | #define NOTIFY_IDX_GENRC3 (NUM_NOTIFIERS-2)
25 | #define NOTIFY_IDX_GENRC4 (NUM_NOTIFIERS-1)
26 |
27 | #define CURL_USER_AGENT "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " \
28 | "(KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
29 |
30 | /* Minimum time (in secs) between two */
31 | #define LAST_SENT_THRESHOLD_SECS 10
32 |
33 | /* Notifiers list, like:
34 | * - Telegram
35 | * - Slack
36 | * - Discord
37 | * - Teams
38 | */
39 | extern const char *const notifiers_str[NUM_NOTIFIERS];
40 |
41 | /* Notifier struct. */
42 | struct notifier {
43 | void *data;
44 | void(*setup)(struct notifier *self);
45 | int(*send_notification)(const struct notifier *self, const char *msg);
46 | };
47 |
48 | extern struct notifier notifiers[NUM_NOTIFIERS];
49 | extern int is_within_notify_threshold(void);
50 | extern void update_notify_last_sent(void);
51 |
52 | #endif /* NOTIFIERS_H */
53 |
--------------------------------------------------------------------------------
/str.c:
--------------------------------------------------------------------------------
1 | /*
2 | * Alertik: a tiny 'syslog' server & notification tool for Mikrotik routers.
3 | * This is free and unencumbered software released into the public domain.
4 | */
5 |
6 | /*
7 | * String/append buffer implementation based on Aqua:
8 | * https://gist.github.com/Theldus/09ed2205aa5ba15cdf4571b71cd1c8fc
9 | */
10 |
11 | #include "str.h"
12 | #include "log.h"
13 |
14 | /* Malloc is only used if AB_USE_MALLOC is defined. */
15 | #ifdef AB_USE_MALLOC
16 | #if defined(AB_CALLOC) && defined(AB_REALLOC) && defined(AB_FREE)
17 | # define AB_USE_STDLIB
18 | #elif !defined(AB_CALLOC) && !defined(AB_REALLOC) && !defined(AB_FREE)
19 | # define AB_USE_STDLIB
20 | #else
21 | #error "For custom memory allocators, you should define all three routines!"
22 | #error "Please define: AB_CALLOC, AB_REALLOC and AB_FREE!"
23 | #endif
24 | #endif
25 |
26 | #ifndef AB_CALLOC
27 | #define AB_CALLOC(nmemb,sz) calloc((nmemb),(sz))
28 | #define AB_REALLOC(p,newsz) realloc((p),(newsz))
29 | #define AB_FREE(p) free((p))
30 | #endif
31 |
32 | #ifdef AB_USE_STDLIB
33 | #include
34 | #endif
35 |
36 | #include
37 | #include
38 | #include
39 |
40 | /* ========================================================================= */
41 | /* BUFFER ROUTINES */
42 | /* ========================================================================= */
43 |
44 | #ifdef AB_USE_MALLOC
45 | /**
46 | * @brief Rounds up to the next power of two.
47 | *
48 | * @param target Target number to be rounded.
49 | *
50 | * @return Returns the next power of two.
51 | */
52 | static size_t next_power(size_t target)
53 | {
54 | target--;
55 | target |= target >> 1;
56 | target |= target >> 2;
57 | target |= target >> 4;
58 | target |= target >> 8;
59 | target |= target >> 16;
60 | target++;
61 | return (target);
62 | }
63 | #endif
64 |
65 | /**
66 | * @brief Checks if the new size fits in the append buffer, if not,
67 | * reallocates the buffer size by @p incr bytes.
68 | *
69 | * If the macro AB_USE_MALLOC is not defined (default), this only
70 | * checks if the new size fits the buffer.
71 | *
72 | * @param sh Aqua highlight context.
73 | * @param incr Size (in bytes) to be incremented.
74 | *
75 | * @return Returns 0 if success, -1 otherwise.
76 | *
77 | * @note The new size is the next power of two, that is capable
78 | * to hold the required buffer size.
79 | */
80 | static int increase_buff(struct str_ab *sh, size_t incr)
81 | {
82 | #ifndef AB_USE_MALLOC
83 | if (sh->pos + incr >= MAX_LINE) {
84 | log_msg("(increase buffer) Unable to fit appended message!\n");
85 | log_msg("(static storage) incr: %zu, buff_len: %zu, pos: %zu\n",
86 | incr, sh->pos, sh->buff_len);
87 | return (-1);
88 | }
89 | #else
90 | char *new;
91 | size_t new_size;
92 | if (sh->pos + incr >= sh->buff_len)
93 | {
94 | new_size = next_power(sh->buff_len + incr);
95 | new = AB_REALLOC(sh->buff, new_size);
96 | if (new == NULL)
97 | {
98 | AB_FREE(sh->buff);
99 | sh->buff = NULL;
100 |
101 | log_msg("(increase buffer) Unable to fit appended message!\n");
102 | log_msg("(realloc storage) incr: %zu, buff_len: %zu, pos: %zu\n",
103 | incr, sh->pos, sh->buff_len);
104 | return (-1);
105 | }
106 | sh->buff_len = new_size;
107 | sh->buff = new;
108 | }
109 | #endif
110 | return (0);
111 | }
112 |
113 | /**
114 | * @brief Initializes the append buffer context.
115 | *
116 | * @param ab Append buffer structure.
117 | *
118 | * @return Returns 0 if success, -1 otherwise.
119 | */
120 | int ab_init(struct str_ab *ab)
121 | {
122 | if (!ab)
123 | return (-1);
124 |
125 | memset(ab, 0, sizeof(*ab));
126 |
127 | #ifndef AB_USE_MALLOC
128 | ab->buff_len = MAX_LINE;
129 | #else
130 | ab->buff = AB_CALLOC(MAX_LINE, 1);
131 | if (!ab->buff)
132 | return (-1);
133 | ab->buff_len = MAX_LINE;
134 | #endif
135 |
136 | return (0);
137 | }
138 |
139 | /**
140 | * @brief Append a given char @p c into the buffer.
141 | *
142 | * @param sh Aqua highlight context.
143 | * @param c Char to be appended.
144 | *
145 | * @return Returns 0 if success, -1 otherwise.
146 | */
147 | int ab_append_chr(struct str_ab *sh, char c)
148 | {
149 | if (increase_buff(sh, 2) < 0)
150 | return (-1);
151 |
152 | sh->buff[sh->pos + 0] = c;
153 | sh->buff[sh->pos + 1] = '\0';
154 | sh->pos++;
155 | return (0);
156 | }
157 |
158 | /**
159 | * @brief Appends a given string pointed by @p s of size @p len
160 | * into the current buffer.
161 | *
162 | * If @p len is 0, the string is assumed to be null-terminated
163 | * and its length is obtained.
164 | *
165 | * @param ab Append buffer context.
166 | * @param s String to be append into the buffer.
167 | * @param len String size, if 0, it's length is obtained.
168 | *
169 | * @return Returns 0 if success, -1 otherwise.
170 | */
171 | int ab_append_str(struct str_ab *ab, const char *s, size_t len)
172 | {
173 | if (!len)
174 | len = strlen(s);
175 |
176 | if (increase_buff(ab, len + 1) < 0)
177 | return (-1);
178 |
179 | memcpy(ab->buff + ab->pos, s, len);
180 | ab->pos += len;
181 | ab->buff[ab->pos] = '\0';
182 | return (0);
183 | }
184 |
185 | /**
186 | * @brief Appends a given formatted string pointed by @p fmt.
187 | *
188 | * @param ab Append buffer context.
189 | * @param fmt Formatted string to be appended.
190 | *
191 | * @return Returns 0 if success, -1 otherwise.
192 | */
193 | int ab_append_fmt(struct str_ab *ab, const char *fmt, ...)
194 | {
195 | int str_len, ab_len, orig_ab_pos;
196 | char *buff_st;
197 | va_list ap;
198 |
199 | buff_st = ab->buff + ab->pos;
200 | ab_len = ab->buff_len - ab->pos;
201 |
202 | va_start(ap, fmt);
203 | str_len = vsnprintf(buff_st, ab_len, fmt, ap);
204 | if (str_len < 0) {
205 | log_msg("Unable to fit appended message!\n");
206 | return (-1);
207 | }
208 | va_end(ap);
209 |
210 | /* If it fits, just happily returns. */
211 | if (str_len + 1 <= ab_len) {
212 | ab->pos += str_len;
213 | return (0);
214 | }
215 |
216 | /* Otherwise, adjust current pos and try to increase buffer. */
217 | else {
218 | orig_ab_pos = ab->pos;
219 |
220 | /* temporarily advance our buffer
221 | * to trick our increase buffer. */
222 | ab->pos = ab->buff_len;
223 |
224 | if (increase_buff(ab, (str_len + 1) - ab_len))
225 | return (-1);
226 |
227 | ab->pos = orig_ab_pos;
228 | }
229 |
230 | buff_st = ab->buff + ab->pos;
231 | ab_len = ab->buff_len - ab->pos;
232 |
233 | va_start(ap, fmt);
234 | str_len = vsnprintf(buff_st, ab_len, fmt, ap);
235 | if (str_len < 0 || (str_len + 1) > ab_len) {
236 | log_msg("Unable to fit appended message!\n");
237 | return (-1);
238 | }
239 | va_end(ap);
240 |
241 | ab->pos += str_len;
242 | return (0);
243 | }
244 |
--------------------------------------------------------------------------------
/str.h:
--------------------------------------------------------------------------------
1 | /*
2 | * Alertik: a tiny 'syslog' server & notification tool for Mikrotik routers.
3 | * This is free and unencumbered software released into the public domain.
4 | */
5 |
6 | #ifndef STR_H
7 | #define STR_H
8 |
9 | #include
10 |
11 | /*
12 | * Enable to disable malloc support and enable dinamically
13 | * allocated buffer.
14 | */
15 | #if 0
16 | #define AB_USE_MALLOC
17 | #endif
18 |
19 | /* Maximum highlighted line len, when built without malloc. */
20 | #define MAX_LINE 4096
21 |
22 | /* Append buffer. */
23 | struct str_ab
24 | {
25 | #ifndef AB_USE_MALLOC
26 | char buff[MAX_LINE + 1];
27 | #else
28 | char *buff;
29 | #endif
30 | size_t buff_len;
31 | size_t pos;
32 | };
33 |
34 | extern int ab_init(struct str_ab *ab);
35 | extern int ab_append_chr(struct str_ab *sh, char c);
36 | extern int ab_append_str(struct str_ab *ab, const char *s, size_t len);
37 | extern int ab_append_fmt(struct str_ab *ab, const char *fmt, ...);
38 |
39 | #endif /* STR_H. */
40 |
--------------------------------------------------------------------------------
/syslog.c:
--------------------------------------------------------------------------------
1 | /*
2 | * Alertik: a tiny 'syslog' server & notification tool for Mikrotik routers.
3 | * This is free and unencumbered software released into the public domain.
4 | */
5 |
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 | #include
14 | #include
15 |
16 | #include "events.h"
17 | #include "log.h"
18 | #include "syslog.h"
19 |
20 | /*
21 | * UDP message handling and FIFO.
22 | */
23 |
24 | /* Forward server data. */
25 | static struct addrinfo *fwd_addr_info;
26 | static int fwd_fd;
27 |
28 | /* Circular message buffer. */
29 | static struct circ_buffer {
30 | int head;
31 | int tail;
32 | struct log_event log_ev [FIFO_MAX];
33 | } circ_buffer = {0};
34 |
35 | /* Sync. */
36 | static pthread_mutex_t fifo_mutex = PTHREAD_MUTEX_INITIALIZER;
37 | static pthread_cond_t fifo_new_log_entry = PTHREAD_COND_INITIALIZER;
38 | static int syslog_push_msg_into_fifo(const char *, time_t);
39 |
40 |
41 | /**
42 | * @brief Create an UDP socket to read from.
43 | *
44 | * @return Returns the UDP socket fd if success.
45 | */
46 | int syslog_create_udp_socket(void)
47 | {
48 | struct sockaddr_in svaddr;
49 | int yes;
50 | int fd;
51 |
52 | fd = socket(AF_INET, SOCK_DGRAM, 0);
53 | if (fd < 0)
54 | panic_errno("Unable to create UDP socket...");
55 |
56 | memset(&svaddr, 0, sizeof(svaddr));
57 | svaddr.sin_family = AF_INET;
58 | svaddr.sin_addr.s_addr = INADDR_ANY;
59 | svaddr.sin_port = SYSLOG_PORT;
60 |
61 | if (bind(fd, (const struct sockaddr *)&svaddr, sizeof(svaddr)) < 0)
62 | panic_errno("Unable to bind...");
63 |
64 | yes = 1;
65 | if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes,
66 | sizeof(yes)) < 0) {
67 | panic_errno("Unable to reuse address...");
68 | }
69 |
70 | return fd;
71 | }
72 |
73 | /**
74 | * @brief Initializes the forwarding to the syslog server if
75 | * the required environment vars were informed.
76 | *
77 | * @return Returns 0.
78 | */
79 | int syslog_init_forward(void)
80 | {
81 | struct addrinfo hints, *results, *try;
82 | char *host, *port;
83 | int sock = 0;
84 |
85 | /* Check if we should forward messages. */
86 | host = getenv("FORWARD_HOST");
87 | port = getenv("FORWARD_PORT");
88 | if (!host && !port) {
89 | log_msg("Forward Mode: disabled\n\n");
90 | return 0;
91 | }
92 |
93 | if (!host || !port)
94 | panic("FORWARD_ADDR and FORWARD_PORT must be specified!\n");
95 |
96 | memset(&hints, 0, sizeof(hints));
97 | hints.ai_family = AF_UNSPEC;
98 | hints.ai_socktype = SOCK_DGRAM;
99 |
100 | if (getaddrinfo(host, port, &hints, &results) != 0)
101 | panic_errno("Unable to getaddrinfo...");
102 |
103 | /* Iterate over results. */
104 | for (try = results; try != NULL; try = try->ai_next) {
105 | sock = socket(try->ai_family, try->ai_socktype, try->ai_protocol);
106 | if (sock < 0)
107 | continue;
108 | break;
109 | }
110 |
111 | if (sock < 0)
112 | panic("Unable to create a socket for forward...\n");
113 |
114 | fwd_fd = sock;
115 | fwd_addr_info = try;
116 |
117 | log_msg("Forward Mode: enabled:\n");
118 | log_msg("----------------------\n");
119 | log_msg("FORWARD_HOST: %s\n", host);
120 | log_msg("FORWARD_PORT: %s\n\n", port);
121 | return 0;
122 | }
123 |
124 | /**
125 | * @brief Sends a message @p msg of length @p len to the
126 | * configured syslog server.
127 | */
128 | static int syslog_fwd_msg(const char *msg, size_t len) {
129 | return (
130 | sendto(fwd_fd, msg, len, 0, fwd_addr_info->ai_addr,
131 | fwd_addr_info->ai_addrlen)
132 | );
133 | }
134 |
135 | /**
136 | * @brief Receives a new UDP message and then adds it
137 | * to the message queue. Additionally, also forwards
138 | * the message to a previously configured syslog server.
139 | *
140 | * @param fd UDP file descriptor to receive from.
141 | *
142 | * @return Returns 0 if success, -1 otherwise.
143 | */
144 | int syslog_enqueue_new_upd_msg(int fd)
145 | {
146 | struct sockaddr_storage cli = {0};
147 | char msg[MSG_MAX] = {0};
148 | socklen_t clilen;
149 | ssize_t ret;
150 |
151 | clilen = sizeof(cli);
152 | ret = recvfrom(fd, msg, sizeof msg - 1, 0, (struct sockaddr*)&cli,
153 | &clilen);
154 |
155 | if (ret < 0)
156 | return -1;
157 |
158 | /* Forward message if forwarding was configured. */
159 | if (fwd_fd) {
160 | if (syslog_fwd_msg(msg, ret) < 0)
161 | log_errno("Unable to forward message...\n");
162 | }
163 |
164 | if (syslog_push_msg_into_fifo(msg, time(NULL)) < 0)
165 | panic("Circular buffer full! (size: %d)\n", FIFO_MAX);
166 |
167 | return 0;
168 | }
169 |
170 |
171 |
172 | ///////////////////////////////// FIFO ////////////////////////////////////////
173 | /**
174 | * @brief For a given message @p msg and a timestamp @p timestamp,
175 | * adds both to the message queue and then wakes up the waiting
176 | * thread.
177 | *
178 | * @param msg Read message from UDP.
179 | * @param timestamp Current timestamp.
180 | *
181 | * @return Returns 0 if success, -1 otherwise.
182 | */
183 | static int syslog_push_msg_into_fifo(const char *msg, time_t timestamp)
184 | {
185 | int next;
186 | int head;
187 |
188 | pthread_mutex_lock(&fifo_mutex);
189 | head = circ_buffer.head;
190 | next = head + 1;
191 | if (next >= FIFO_MAX)
192 | next = 0;
193 |
194 | if (next == circ_buffer.tail) {
195 | pthread_mutex_unlock(&fifo_mutex);
196 | return -1;
197 | }
198 | memcpy(circ_buffer.log_ev[head].msg, msg, MSG_MAX);
199 | circ_buffer.log_ev[head].timestamp = timestamp;
200 |
201 | circ_buffer.head = next;
202 | pthread_cond_signal(&fifo_new_log_entry);
203 | pthread_mutex_unlock(&fifo_mutex);
204 | return 0;
205 | }
206 |
207 | /**
208 | * @brief Pops a single message from the message queue (if any),
209 | * and saves it into @p ev.
210 | *
211 | * @param ev Target buffer to the retrieved log event.
212 | *
213 | * @return Returns 0.
214 | */
215 | int syslog_pop_msg_from_fifo(struct log_event *ev)
216 | {
217 | int next;
218 | int tail;
219 |
220 | pthread_mutex_lock(&fifo_mutex);
221 | while (circ_buffer.head == circ_buffer.tail) {
222 | pthread_cond_wait(&fifo_new_log_entry, &fifo_mutex);
223 | }
224 |
225 | next = circ_buffer.tail + 1;
226 | if (next >= FIFO_MAX)
227 | next = 0;
228 |
229 | tail = circ_buffer.tail;
230 | ev->timestamp = circ_buffer.log_ev[tail].timestamp;
231 | memcpy(ev->msg, circ_buffer.log_ev[tail].msg, MSG_MAX);
232 |
233 | circ_buffer.tail = next;
234 | pthread_mutex_unlock(&fifo_mutex);
235 | return 0;
236 | }
237 |
--------------------------------------------------------------------------------
/syslog.h:
--------------------------------------------------------------------------------
1 | /*
2 | * Alertik: a tiny 'syslog' server & notification tool for Mikrotik routers.
3 | * This is free and unencumbered software released into the public domain.
4 | */
5 |
6 | #ifndef SYSLOG_H
7 | #define SYSLOG_H
8 |
9 | struct log_event;
10 |
11 | #define FIFO_MAX 64
12 | #define SYSLOG_PORT 5140
13 |
14 | extern int syslog_init_forward(void);
15 | extern int syslog_create_udp_socket(void);
16 | extern int syslog_enqueue_new_upd_msg(int fd);
17 | extern int syslog_pop_msg_from_fifo(struct log_event *ev);
18 |
19 | #endif /* SYSLOG_H */
20 |
--------------------------------------------------------------------------------
/toolchain/armv6.mk:
--------------------------------------------------------------------------------
1 | # conf file for armv6 builds on BearSSL
2 | include conf/Unix.mk
3 |
4 | # We override the build directory.
5 | BUILD = armv6
6 |
7 | # C compiler, linker, and static library builder.
8 | CC = armv6-linux-musleabi-gcc
9 | CFLAGS = -W -Wall -Os
10 | LD = armv6-linux-musleabi-gcc
11 | AR = armv6-linux-musleabi-ar
12 |
13 | # We compile only the static library.
14 | DLL = no
15 | TOOLS = no
16 | TESTS = no
17 |
--------------------------------------------------------------------------------
/toolchain/toolchain.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #
4 | # Alertik: a tiny 'syslog' server & notification tool for Mikrotik routers.
5 | # This is free and unencumbered software released into the public domain.
6 | #
7 |
8 | set -e
9 |
10 | # Backup current folder
11 | pushd .
12 | export CURDIR="$( cd "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
13 |
14 | cd "$CURDIR/"
15 | export PATH="$PATH:$CURDIR/armv6-linux-musleabi-cross/bin"
16 | export MUSL_PREFIX="$CURDIR/armv6-linux-musleabi-cross/armv6-linux-musleabi"
17 |
18 | # Misc
19 | MUSL_ARMv6_LINK="https://musl.cc/armv6-linux-musleabi-cross.tgz"
20 | CURL_LINK="https://github.com/curl/curl/releases/download/curl-8_8_0/curl-8.8.0.tar.xz"
21 | BEARSSL_REPO="https://www.bearssl.org/git/BearSSL"
22 | BEARSSL_HASH="79c060eea3eea1257797f15ea1608a9a9923aa6f"
23 |
24 | download_musl_armv6() {
25 | echo "[+] Downloading musl ..."
26 | wget "$MUSL_ARMv6_LINK" -O armv6-musl.tgz
27 | tar xvf armv6-musl.tgz
28 | popd
29 | }
30 |
31 | download_build_bearssl() {
32 | echo "[+] Cloning BearSSL ..."
33 | git clone "$BEARSSL_REPO"
34 | cd BearSSL/
35 | git checkout "$BEARSSL_HASH"
36 | cp ../armv6.mk conf/
37 | echo "[+] Building ..."
38 | make CONF=armv6
39 | # Copy to the right path
40 | echo "[+] Installing ..."
41 | cp armv6/libbearssl.a "$MUSL_PREFIX/lib"
42 | cp inc/* "$MUSL_PREFIX/include"
43 | popd
44 | }
45 |
46 | download_build_libcurl() {
47 | echo "[+] Downloading cURL ..."
48 | wget "$CURL_LINK" -O curl.tar.xz
49 | tar xvf curl.tar.xz
50 |
51 | echo "[+] Building cURL ..."
52 | cd curl*/
53 | mkdir -p build && cd build/
54 | export CFLAGS="-Os -ffunction-sections -fdata-sections -fno-unwind-tables -fno-asynchronous-unwind-tables -flto"
55 | export LDFLAGS="-Wl,-s -Wl,-Bsymbolic -Wl,--gc-sections"
56 | ../configure \
57 | --prefix="$MUSL_PREFIX" \
58 | --target=armv6-linux-musleabi \
59 | --host=armv6-linux-musleabi \
60 | --build=x86_64-linux-gnu \
61 | --with-bearssl \
62 | --without-zlib \
63 | --without-zstd \
64 | --without-brotli \
65 | --without-librtmp \
66 | --disable-headers-api \
67 | --disable-verbose \
68 | --disable-http-auth \
69 | --disable-cookies \
70 | --disable-ipv6 \
71 | --disable-ftp \
72 | --disable-gopher \
73 | --disable-imap \
74 | --disable-ipfs \
75 | --disable-ipns \
76 | --disable-mqtt \
77 | --disable-pop3 \
78 | --disable-rtsp \
79 | --disable-smtp \
80 | --disable-telnet \
81 | --disable-tftp \
82 | --disable-hsts \
83 | --disable-doh \
84 | --disable-largefile \
85 | --disable-dependency-tracking \
86 | --disable-shared \
87 | --disable-proxy \
88 | --disable-dict \
89 | --disable-file \
90 | --disable-unix-sockets \
91 | --disable-alt-svc \
92 | --disable-manual \
93 | --disable-docs \
94 | --disable-libcurl-option \
95 | --disable-sspi \
96 | --disable-progress-meter \
97 | --disable-netrc \
98 | --disable-dateparse \
99 | --disable-mime \
100 | --enable-pthreads
101 |
102 | make -j$(nproc)
103 | make install
104 | popd
105 | }
106 |
107 | # This is slightly better than using the CI file
108 | # because our env vars are already set!
109 | build_alertik_armv6() {
110 | popd
111 | echo "[+] Building Alertik!"
112 | make CROSS=armv6
113 | echo "[+] File type:"
114 | file alertik
115 | echo "[+] File size:"
116 | ls -lah alertik
117 | echo "[+] File hash:"
118 | sha256sum alertik
119 | }
120 |
121 | # Dispatcher
122 | if [ "$1" == "download_musl_armv6" ]; then
123 | download_musl_armv6
124 | elif [ "$1" == "download_build_bearssl" ]; then
125 | download_build_bearssl
126 | elif [ "$1" == "download_build_libcurl" ]; then
127 | download_build_libcurl
128 | elif [ "$1" == "build_alertik_armv6" ]; then
129 | build_alertik_armv6
130 | else
131 | echo "No option found!"
132 | exit 1
133 | fi
134 |
--------------------------------------------------------------------------------
/tools/Makefile:
--------------------------------------------------------------------------------
1 | #
2 | # Alertik: a tiny 'syslog' server & notification tool for Mikrotik routers.
3 | # This is free and unencumbered software released into the public domain.
4 | #
5 |
6 | CC ?= cc
7 | CC_JS = emcc
8 | CFLAGS += -Wall -Wextra -O2
9 | CFLAGS_JS += $(CFLAGS)
10 | CFLAGS_JS += -s EXPORTED_FUNCTIONS='["_do_regex", "_malloc", "_free"]'
11 | CFLAGS_JS += -s 'EXPORTED_RUNTIME_METHODS=["stringToUTF8", "UTF8ToString", "setValue"]'
12 |
13 | all: regext.js regext Makefile
14 |
15 | regext.js: regext.c
16 | $(CC_JS) $(CFLAGS_JS) regext.c -o regext.js
17 |
18 | regext: regext.c
19 | $(CC) $(CFLAGS) -DUSE_C regext.c -o regext
20 |
21 | clean:
22 | rm -f regext.js regext.wasm regext *.o
23 |
--------------------------------------------------------------------------------
/tools/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | POSIX Regex Extended Validator
7 |
100 |
101 |
102 |
103 |
POSIX Regex Extended Validator
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
130 |
131 |
137 |
138 |
139 |
140 |
231 |
232 |
233 |
--------------------------------------------------------------------------------
/tools/regext.c:
--------------------------------------------------------------------------------
1 | /*
2 | * Alertik: a tiny 'syslog' server & notification tool for Mikrotik routers.
3 | * This is free and unencumbered software released into the public domain.
4 | */
5 |
6 | #include
7 | #include
8 | #include
9 | #include
10 |
11 | #ifdef DBG
12 | #define LOG(...) printf(__VA_ARGS__)
13 | #else
14 | #define LOG(...)
15 | #endif
16 |
17 | int do_regex(
18 | const char *re, const char *str,
19 | int32_t *sub_expr, int32_t *rm_so, int32_t *rm_eo,
20 | char *error_msg)
21 | {
22 | int ret;
23 | regex_t regex;
24 | regmatch_t pmatch[32];
25 |
26 | if ((ret = regcomp(®ex, re, REG_EXTENDED))) {
27 | regerror(ret, ®ex, error_msg, 128);
28 | regfree(®ex);
29 | LOG("Error: %s\n", error_msg);
30 | return (-1);
31 | }
32 |
33 | if (regexec(®ex, str, 32, pmatch, 0) == REG_NOMATCH) {
34 | LOG("No match!\n");
35 | return (0);
36 | }
37 |
38 | *sub_expr = regex.re_nsub;
39 | if (!regex.re_nsub) {
40 | regfree(®ex);
41 | LOG("Match without subexpressions!\n");
42 | return (1);
43 | }
44 |
45 | LOG("N subexpr: %d\n", *sub_expr);
46 |
47 | /* If exists sub-expressions, save them. */
48 | for (size_t i = 0; i < regex.re_nsub + 1; i++) {
49 | rm_so[i - 0] = pmatch[i].rm_so;
50 | rm_eo[i - 0] = pmatch[i].rm_eo;
51 | LOG("rm_so[i-1] = %d\nrm_eo[i-1] = %d\n",
52 | rm_so[i],
53 | rm_eo[i]);
54 | }
55 |
56 | regfree(®ex);
57 | return (2);
58 | }
59 |
60 |
61 | #ifdef USE_C
62 | int main(int argc, char **argv)
63 | {
64 | int32_t se, so[32], eo[32];
65 | char msg[128] = {0};
66 |
67 | char *re = argv[1];
68 | char *in = argv[2];
69 |
70 | if (argc < 3) {
71 | fprintf(stderr, "Usage: %s \n", argv[0]);
72 | return (1);
73 | }
74 |
75 | printf("Regex : %s\n", re);
76 | printf("input-text: %s\n", in);
77 |
78 | int r = do_regex(re, in, &se, so, eo, msg);
79 | switch (r) {
80 | case -1:
81 | printf("Error, reason: %s\n", msg);
82 | break;
83 | case 0:
84 | printf("No match!\n");
85 | break;
86 | case 1:
87 | printf("Match without sub-expressions!\n");
88 | break;
89 | case 2:
90 | printf("Match!!: %.*s\n", eo[0]-so[0], in+so[0]);
91 | printf("Found %d sub-expressions:\n", se);
92 | for (int i = 1; i < se+1; i++) {
93 | printf("$%d: %.*s\n", i, eo[i]-so[i], in+so[i]);
94 | }
95 | break;
96 | }
97 |
98 | return (0);
99 | }
100 | #endif
101 |
--------------------------------------------------------------------------------