OpenVPN
test_openvpnserv.c
Go to the documentation of this file.
1/*
2 * OpenVPN -- An application to securely tunnel IP networks
3 * over a single UDP port, with support for SSL/TLS-based
4 * session authentication and key exchange,
5 * packet encryption, packet authentication, and
6 * packet compression.
7 *
8 * Copyright (C) 2025 Frank Lichtenheld <frank@lichtenheld.com>
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by the
12 * Free Software Foundation, either version 2 of the License,
13 * or (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License along
21 * with this program; if not, see <https://www.gnu.org/licenses/>.
22 */
23
24#ifdef HAVE_CONFIG_H
25#include "config.h"
26#endif
27
28#include <setjmp.h>
29#include <cmocka.h>
30#include "test_common.h"
31
32#include <winsock2.h>
33#include <windows.h>
34
35#include "interactive.c"
36
37BOOL
38ReportStatusToSCMgr(SERVICE_STATUS_HANDLE svc_handle, SERVICE_STATUS *svc_status)
39{
40 return TRUE;
41}
42
43static void
45{
46 PCWSTR domain = L"openvpn.net";
47 size_t domain_len = wcslen(domain);
48 assert_true(ListContainsDomain(domain, domain, domain_len));
49 assert_true(ListContainsDomain(L"openvpn.com,openvpn.net", domain, domain_len));
50 assert_true(ListContainsDomain(L"openvpn.net,openvpn.com", domain, domain_len));
51 assert_true(ListContainsDomain(L"openvpn.org,openvpn.net,openvpn.com", domain, domain_len));
52
53 assert_false(ListContainsDomain(L"openvpn.com", domain, domain_len));
54 assert_false(ListContainsDomain(L"internal.openvpn.net", domain, domain_len));
55 assert_false(ListContainsDomain(L"openvpn.com,internal.openvpn.net", domain, domain_len));
56 assert_false(ListContainsDomain(L"internal.openvpn.net,openvpn.com", domain, domain_len));
57}
58
59/*
60 * Run ConvertItfDnsDomains() on a heap buffer of exactly capacity bytes,
61 * so that ASAN can detect overruns, with everything after the input string
62 * poisoned, so that terminators which are counted but not written show.
63 */
64static void
65check_convert(PCWSTR search_domains, PCWSTR input, DWORD capacity,
66 LSTATUS expected_err, PCWSTR expected)
67{
68 const DWORD glyph_size = sizeof(wchar_t);
69
70 /* Size of the expected MULTI_SZ including both terminators, or 0 */
71 DWORD expected_size = 0;
72 if (expected)
73 {
74 PCWSTR end = expected;
75 while (*end)
76 {
77 end += wcslen(end) + 1;
78 }
79 expected_size = (DWORD)(end - expected + 1) * glyph_size;
80 }
81
82 DWORD size = (DWORD)(wcslen(input) + 1) * glyph_size;
83 assert_true(size <= capacity);
84
85 wchar_t *domains = malloc(capacity);
86 assert_non_null(domains);
87 memset(domains, 0x41, capacity);
88 memcpy(domains, input, size);
89
90 LSTATUS err = ConvertItfDnsDomains(search_domains, domains, &size, capacity);
91 assert_int_equal(err, expected_err);
92 assert_int_equal(size, expected_size);
93 if (expected_size)
94 {
95 assert_memory_equal(domains, expected, expected_size);
96 }
97 free(domains);
98}
99
100#define BUF_SIZE 64
101static void
103{
104 const DWORD glyph_size = sizeof(wchar_t);
105 const DWORD cap = BUF_SIZE * glyph_size;
106
107 /* Remove the domain from a single-entry list */
108 check_convert(L"openvpn.com", L"openvpn.com", cap, ERROR_FILE_NOT_FOUND, NULL);
109
110 /* Remove no domain from a single-entry list */
111 check_convert(L"openvpn.net", L"openvpn.com", cap, NO_ERROR, L".openvpn.com\0");
112
113 /* Remove the second domain from a two-entry list */
114 check_convert(L"openvpn.net", L"openvpn.com,openvpn.net", cap, NO_ERROR, L".openvpn.com\0");
115
116 /* Remove the first domain from a two-entry list */
117 check_convert(L"openvpn.com", L"openvpn.com,openvpn.net", cap, NO_ERROR, L".openvpn.net\0");
118
119 /* Remove no domain from a two-entry list */
120 check_convert(NULL, L"openvpn.com,openvpn.net", cap, NO_ERROR, L".openvpn.com\0.openvpn.net\0");
121
122 /* Remove the first domain from a three-entry list */
123 check_convert(L"openvpn.com", L"openvpn.com,openvpn.net,openvpn.org", cap, NO_ERROR,
124 L".openvpn.net\0.openvpn.org\0");
125
126 /* Remove the middle domain from a three-entry list */
127 check_convert(L"openvpn.net", L"openvpn.com,openvpn.net,openvpn.org", cap, NO_ERROR,
128 L".openvpn.com\0.openvpn.org\0");
129
130 /* Remove the last domain from a three-entry list */
131 check_convert(L"openvpn.org", L"openvpn.com,openvpn.net,openvpn.org", cap, NO_ERROR,
132 L".openvpn.com\0.openvpn.net\0");
133
134 /* Empty entries are skipped */
135 check_convert(NULL, L",a.com", cap, NO_ERROR, L".a.com\0");
136 check_convert(NULL, L"a.com,", cap, NO_ERROR, L".a.com\0");
137 check_convert(NULL, L"a.com,,b.com", cap, NO_ERROR, L".a.com\0.b.com\0");
138 check_convert(L"a.com", L",a.com,", cap, ERROR_FILE_NOT_FOUND, NULL);
139 check_convert(NULL, L",,,", cap, ERROR_FILE_NOT_FOUND, NULL);
140 check_convert(NULL, L"", cap, ERROR_FILE_NOT_FOUND, NULL);
141
142 /* Size is 0 if all domains are removed */
143 check_convert(L"a.com,b.com", L"b.com,a.com", cap, ERROR_FILE_NOT_FOUND, NULL);
144
145 /* Output exactly fills the buffer: 2 * (dot + 5 + NUL) + NUL */
146 check_convert(NULL, L"a.com,b.com", 15 * glyph_size, NO_ERROR, L".a.com\0.b.com\0");
147
148 /* Remove the last domain from a four-entry list because of size constraints,
149 * all four would need 3 * 13 + 28 + 1 = 68 glyphs, the buffer is one glyph short */
150 check_convert(NULL, L"openvpn.com,openvpn.net,openvpn.org,am-ende-noch-eine-lange.de",
151 67 * glyph_size, ERROR_MORE_DATA, L".openvpn.com\0.openvpn.net\0.openvpn.org\0");
152
153 /* Input fills the buffer, no space for dot and terminators */
154 check_convert(NULL, L"a.com", 6 * glyph_size, ERROR_MORE_DATA, NULL);
155}
156
157static void
159{
160 WCHAR list[64];
161
162 /* append to an empty list */
163 wcscpy(list, L"");
164 assert_true(AppendSearchList(list, _countof(list), L"a.com"));
165 assert_int_equal(wcscmp(list, L"a.com"), 0);
166
167 /* append to a non-empty list */
168 wcscpy(list, L"x.com");
169 assert_true(AppendSearchList(list, _countof(list), L"a.com,b.com"));
170 assert_int_equal(wcscmp(list, L"x.com,a.com,b.com"), 0);
171
172 /* appending an empty string is a no-op */
173 wcscpy(list, L"x.com");
174 assert_true(AppendSearchList(list, _countof(list), L""));
175 assert_int_equal(wcscmp(list, L"x.com"), 0);
176
177 /* duplicates are allowed -- relied on by the multiset-aware remove */
178 wcscpy(list, L"x.com");
179 assert_true(AppendSearchList(list, _countof(list), L"x.com"));
180 assert_int_equal(wcscmp(list, L"x.com,x.com"), 0);
181
182 /* overflow: the list is left unchanged */
183 WCHAR tiny[8];
184 wcscpy(tiny, L"abc");
185 assert_false(AppendSearchList(tiny, _countof(tiny), L"long.example.com"));
186 assert_int_equal(wcscmp(tiny, L"abc"), 0);
187}
188
189static void
191{
192 WCHAR list[256];
193
194 /* substring rejection: removing "vpn.corp.local" does NOT touch
195 * "test.vpn.corp.local" -- the original wcsstr-based code did. */
196 wcscpy(list, L"test.vpn.corp.local,other.com,vpn.corp.local");
197 assert_int_equal(RemoveSearchListTokens(list, L"vpn.corp.local"), 1);
198 assert_int_equal(wcscmp(list, L"test.vpn.corp.local,other.com"), 0);
199
200 /* multiset semantics: one undo token removes one occurrence */
201 wcscpy(list, L"corp.local,corp.local");
202 assert_int_equal(RemoveSearchListTokens(list, L"corp.local"), 1);
203 assert_int_equal(wcscmp(list, L"corp.local"), 0);
204
205 /* removal targets the LAST occurrence so a pre-existing prefix is
206 * preserved: with initial list "a,b" a VPN append yields "a,b,a";
207 * removing "a" must restore "a,b" (not "b,a") so the result matches
208 * the stored initial snapshot and a full reset can be triggered */
209 wcscpy(list, L"a,b,a");
210 assert_int_equal(RemoveSearchListTokens(list, L"a"), 1);
211 assert_int_equal(wcscmp(list, L"a,b"), 0);
212
213 /* multiple removes, mixed positions */
214 wcscpy(list, L"a,b,c,d");
215 assert_int_equal(RemoveSearchListTokens(list, L"b,d"), 2);
216 assert_int_equal(wcscmp(list, L"a,c"), 0);
217
218 /* no matches: list unchanged */
219 wcscpy(list, L"a,b,c");
220 assert_int_equal(RemoveSearchListTokens(list, L"x,y"), 0);
221 assert_int_equal(wcscmp(list, L"a,b,c"), 0);
222
223 /* head removal */
224 wcscpy(list, L"a,b,c");
225 assert_int_equal(RemoveSearchListTokens(list, L"a"), 1);
226 assert_int_equal(wcscmp(list, L"b,c"), 0);
227
228 /* tail removal */
229 wcscpy(list, L"a,b,c");
230 assert_int_equal(RemoveSearchListTokens(list, L"c"), 1);
231 assert_int_equal(wcscmp(list, L"a,b"), 0);
232
233 /* remove every token */
234 wcscpy(list, L"a,b");
235 assert_int_equal(RemoveSearchListTokens(list, L"a,b"), 2);
236 assert_int_equal(wcscmp(list, L""), 0);
237
238 /* duplicate undo tokens drain multiple occurrences */
239 wcscpy(list, L"x,x,x");
240 assert_int_equal(RemoveSearchListTokens(list, L"x,x"), 2);
241 assert_int_equal(wcscmp(list, L"x"), 0);
242
243 /* asking to remove more than is present only removes what's there */
244 wcscpy(list, L"x");
245 assert_int_equal(RemoveSearchListTokens(list, L"x,x,x"), 1);
246 assert_int_equal(wcscmp(list, L""), 0);
247
248 /* empty list */
249 wcscpy(list, L"");
250 assert_int_equal(RemoveSearchListTokens(list, L"a"), 0);
251 assert_int_equal(wcscmp(list, L""), 0);
252
253 /* empty remove */
254 wcscpy(list, L"a,b");
255 assert_int_equal(RemoveSearchListTokens(list, L""), 0);
256 assert_int_equal(wcscmp(list, L"a,b"), 0);
257
258 /* stray empty tokens in the list are tolerated -- the splice only
259 * touches the token being removed (and one adjacent comma) */
260 wcscpy(list, L"a,,b");
261 assert_int_equal(RemoveSearchListTokens(list, L"b"), 1);
262 assert_int_equal(wcscmp(list, L"a,"), 0);
263}
264
265/* Merge the environments and compare the result with the expected block,
266 * including the terminating empty string. A macro, so that failures are
267 * reported with the line of the test case. */
268#define assert_merge_result(sys_env, usr_env, expected) \
269 do \
270 { \
271 PWSTR env_ = MergeOpenvpnEnvironment(sys_env, usr_env); \
272 assert_non_null(env_); \
273 size_t size_ = EnvBlockSize(expected); \
274 assert_int_equal(EnvBlockSize(env_), size_); \
275 assert_memory_equal(env_, expected, size_); \
276 free(env_); \
277 } while (0)
278
279static void
281{
282 /* string literals add the second terminating '\0' of the block */
283 PCWSTR sys_env = L"A=sys\0PATH=sys\0TEMP=sys\0USERNAME=sys\0";
284
285 /* user-specific variables come from the user, everything else from the
286 * system, and variables only the user defined are dropped */
288 sys_env,
289 L"A=usr\0APPDATA=usr\0PATH=usr\0TEMP=usr\0TMP=usr\0USERNAME=usr\0USERPROFILE=usr\0USR_ONLY=usr\0",
290 L"A=sys\0PATH=sys\0APPDATA=usr\0TEMP=usr\0TMP=usr\0USERNAME=usr\0USERPROFILE=usr\0");
291
292 /* user-specific variables missing from the user environment are not set */
293 assert_merge_result(sys_env, L"TEMP=usr\0", L"A=sys\0PATH=sys\0TEMP=usr\0");
294
295 /* only full names are user-specific variables, not prefixes */
296 assert_merge_result(sys_env, L"TEMPDIR=usr\0USER=usr\0", L"A=sys\0PATH=sys\0");
297
298 /* empty environments */
299 assert_merge_result(sys_env, L"", L"A=sys\0PATH=sys\0");
300 assert_merge_result(L"", L"TEMP=usr\0USR_ONLY=usr\0", L"TEMP=usr\0");
301 assert_merge_result(L"", L"", L"");
302}
303
304int
305wmain(void)
306{
308 const struct CMUnitTest tests[] = {
309 cmocka_unit_test(test_list_contains_domain),
310 cmocka_unit_test(test_convert_itf_dns_domains),
311 cmocka_unit_test(test_append_search_list),
312 cmocka_unit_test(test_remove_search_list_tokens),
313 cmocka_unit_test(test_merge_openvpn_environment),
314 };
315
316 int ret = cmocka_run_group_tests_name("openvpnserv tests", tests, NULL, NULL);
317
318 return ret;
319}
static BOOL AppendSearchList(PWSTR list, size_t list_cap, PCWSTR add)
Append a comma-separated list of domains to another comma-separated list, in place.
static size_t RemoveSearchListTokens(PWSTR list, PCWSTR remove)
Remove tokens from a comma-separated search list with multiset semantics: for each comma-separated to...
static BOOL ListContainsDomain(PCWSTR list, PCWSTR domain, size_t len)
Check if a domain is contained in a comma separated list of domains.
static LSTATUS ConvertItfDnsDomains(PCWSTR search_domains, PWSTR domains, PDWORD size, const DWORD capacity)
Convert interface specific domain suffix(es) from comma-separated string to MULTI_SZ string.
static void openvpn_unit_test_setup(void)
Sets up the environment for unit tests like making both stderr and stdout non-buffered to avoid messa...
Definition test_common.h:63
int wmain(void)
static void check_convert(PCWSTR search_domains, PCWSTR input, DWORD capacity, LSTATUS expected_err, PCWSTR expected)
BOOL ReportStatusToSCMgr(SERVICE_STATUS_HANDLE svc_handle, SERVICE_STATUS *svc_status)
static void test_list_contains_domain(void **state)
static void test_convert_itf_dns_domains(void **state)
static void test_merge_openvpn_environment(void **state)
#define BUF_SIZE
#define assert_merge_result(sys_env, usr_env, expected)
static void test_remove_search_list_tokens(void **state)
static void test_append_search_list(void **state)