VirtualBox

source: vbox/trunk/src/VBox/NetworkServices/NAT/proxy.c@ 73746

Last change on this file since 73746 was 73746, checked in by vboxsync, 7 years ago

NAT/NATNet: Disable Nagle algorighm on the host using TCP_NODELAY (bugref:9226).

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id Revision
File size: 18.8 KB
Line 
1/* $Id: proxy.c 73746 2018-08-17 18:17:59Z vboxsync $ */
2/** @file
3 * NAT Network - proxy setup and utilities.
4 */
5
6/*
7 * Copyright (C) 2013-2017 Oracle Corporation
8 *
9 * This file is part of VirtualBox Open Source Edition (OSE), as
10 * available from http://www.virtualbox.org. This file is free software;
11 * you can redistribute it and/or modify it under the terms of the GNU
12 * General Public License (GPL) as published by the Free Software
13 * Foundation, in version 2 as it comes in the "COPYING" file of the
14 * VirtualBox OSE distribution. VirtualBox OSE is distributed in the
15 * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
16 */
17
18#define LOG_GROUP LOG_GROUP_NAT_SERVICE
19
20#include "winutils.h"
21
22#include "proxy.h"
23#include "proxy_pollmgr.h"
24#include "portfwd.h"
25
26#include "lwip/opt.h"
27
28#include "lwip/sys.h"
29#include "lwip/tcpip.h"
30
31#ifndef RT_OS_WINDOWS
32#include <sys/poll.h>
33#include <sys/socket.h>
34#include <netinet/in.h>
35#include <arpa/inet.h>
36#include <fcntl.h>
37#include <stdio.h>
38#include <iprt/string.h>
39#include <unistd.h>
40#include <err.h>
41#else
42# include <iprt/string.h>
43#endif
44
45#if defined(SOCK_NONBLOCK) && defined(RT_OS_NETBSD) /* XXX: PR kern/47569 */
46# undef SOCK_NONBLOCK
47#endif
48
49#ifndef __arraycount
50# define __arraycount(a) (sizeof(a)/sizeof(a[0]))
51#endif
52
53static FNRTSTRFORMATTYPE proxy_sockerr_rtstrfmt;
54
55static SOCKET proxy_create_socket(int, int);
56
57volatile struct proxy_options *g_proxy_options;
58static sys_thread_t pollmgr_tid;
59
60/* XXX: for mapping loopbacks to addresses in our network (ip4) */
61struct netif *g_proxy_netif;
62
63
64/*
65 * Called on the lwip thread (aka tcpip thread) from tcpip_init() via
66 * its "tcpip_init_done" callback. Raw API is ok to use here
67 * (e.g. rtadvd), but netconn API is not.
68 */
69void
70proxy_init(struct netif *proxy_netif, struct proxy_options *opts)
71{
72 int status;
73
74 LWIP_ASSERT1(opts != NULL);
75 LWIP_UNUSED_ARG(proxy_netif);
76
77 status = RTStrFormatTypeRegister("sockerr", proxy_sockerr_rtstrfmt, NULL);
78 AssertRC(status);
79
80 g_proxy_options = opts;
81 g_proxy_netif = proxy_netif;
82
83#if 1
84 proxy_rtadvd_start(proxy_netif);
85#endif
86
87 /*
88 * XXX: We use stateless DHCPv6 only to report IPv6 address(es) of
89 * nameserver(s). Since we don't yet support IPv6 addresses in
90 * HostDnsService, there's no point in running DHCPv6.
91 */
92#if 0
93 dhcp6ds_init(proxy_netif);
94#endif
95
96 if (opts->tftp_root != NULL) {
97 tftpd_init(proxy_netif, opts->tftp_root);
98 }
99
100 status = pollmgr_init();
101 if (status < 0) {
102 errx(EXIT_FAILURE, "failed to initialize poll manager");
103 /* NOTREACHED */
104 }
105
106 pxtcp_init();
107 pxudp_init();
108
109 portfwd_init();
110
111 pxdns_init(proxy_netif);
112
113 pxping_init(proxy_netif, opts->icmpsock4, opts->icmpsock6);
114
115 pollmgr_tid = sys_thread_new("pollmgr_thread",
116 pollmgr_thread, NULL,
117 DEFAULT_THREAD_STACKSIZE,
118 DEFAULT_THREAD_PRIO);
119 if (!pollmgr_tid) {
120 errx(EXIT_FAILURE, "failed to create poll manager thread");
121 /* NOTREACHED */
122 }
123}
124
125
126#if !defined(RT_OS_WINDOWS)
127/**
128 * Formatter for %R[sockerr] - unix strerror_r() version.
129 */
130static DECLCALLBACK(size_t)
131proxy_sockerr_rtstrfmt(PFNRTSTROUTPUT pfnOutput, void *pvArgOutput,
132 const char *pszType, const void *pvValue,
133 int cchWidth, int cchPrecision, unsigned int fFlags,
134 void *pvUser)
135{
136 const int error = (int)(intptr_t)pvValue;
137
138 const char *msg;
139 char buf[128];
140
141 NOREF(cchWidth);
142 NOREF(cchPrecision);
143 NOREF(fFlags);
144 NOREF(pvUser);
145
146 AssertReturn(strcmp(pszType, "sockerr") == 0, 0);
147
148 /* make sure return type mismatch is caught */
149 buf[0] = '\0';
150#if defined(RT_OS_LINUX) && defined(_GNU_SOURCE)
151 msg = strerror_r(error, buf, sizeof(buf));
152#else
153 strerror_r(error, buf, sizeof(buf));
154 msg = buf;
155#endif
156 return RTStrFormat(pfnOutput, pvArgOutput, NULL, NULL, "%s", msg);
157}
158
159#else /* RT_OS_WINDOWS */
160
161/**
162 * Formatter for %R[sockerr] - windows FormatMessage() version.
163 */
164static DECLCALLBACK(size_t)
165proxy_sockerr_rtstrfmt(PFNRTSTROUTPUT pfnOutput, void *pvArgOutput,
166 const char *pszType, const void *pvValue,
167 int cchWidth, int cchPrecision, unsigned int fFlags,
168 void *pvUser)
169{
170 const int error = (int)(intptr_t)pvValue;
171 size_t cb = 0;
172
173 NOREF(cchWidth);
174 NOREF(cchPrecision);
175 NOREF(fFlags);
176 NOREF(pvUser);
177
178 AssertReturn(strcmp(pszType, "sockerr") == 0, 0);
179
180 /*
181 * XXX: Windows strerror() doesn't handle posix error codes, but
182 * since winsock uses its own, it shouldn't be much of a problem.
183 * If you see a strange error message, it's probably from
184 * FormatMessage() for an error from <WinError.h> that has the
185 * same numeric value.
186 */
187 if (error < _sys_nerr) {
188 char buf[128] = "";
189 int status;
190
191 status = strerror_s(buf, sizeof(buf), error);
192 if (status == 0) {
193 if (strcmp(buf, "Unknown error") == 0) {
194 /* windows strerror() doesn't add the numeric value */
195 cb += RTStrFormat(pfnOutput, pvArgOutput, NULL, NULL,
196 "Unknown error: %d", error);
197 }
198 else {
199 cb += RTStrFormat(pfnOutput, pvArgOutput, NULL, NULL,
200 "%s", buf);
201 }
202 }
203 else {
204 cb += RTStrFormat(pfnOutput, pvArgOutput, NULL, NULL,
205 "Unknown error: %d", error);
206 }
207 }
208 else {
209 DWORD nchars;
210 char *msg = NULL;
211
212 nchars = FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM
213 | FORMAT_MESSAGE_ALLOCATE_BUFFER,
214 NULL, error, LANG_NEUTRAL,
215 (LPSTR)&msg, 0,
216 NULL);
217 if (nchars == 0 || msg == NULL) {
218 cb += RTStrFormat(pfnOutput, pvArgOutput, NULL, NULL,
219 "Unknown error: %d", error);
220 }
221 else {
222 /* FormatMessage() "helpfully" adds newline; get rid of it */
223 char *crpos = strchr(msg, '\r');
224 if (crpos != NULL) {
225 *crpos = '\0';
226 }
227
228 cb += RTStrFormat(pfnOutput, pvArgOutput, NULL, NULL,
229 "%s", msg);
230 }
231
232 if (msg != NULL) {
233 LocalFree(msg);
234 }
235 }
236
237 return cb;
238}
239#endif /* RT_OS_WINDOWS */
240
241
242/**
243 * Send static callback message from poll manager thread to lwip
244 * thread, scheduling a function call in lwip thread context.
245 *
246 * XXX: Existing lwip api only provides non-blocking version for this.
247 * It may fail when lwip thread is not running (mbox invalid) or if
248 * post failed (mbox full). How to handle these?
249 */
250void
251proxy_lwip_post(struct tcpip_msg *msg)
252{
253 struct tcpip_callback_msg *m;
254 err_t error;
255
256 LWIP_ASSERT1(msg != NULL);
257
258 /*
259 * lwip plays games with fake incomplete struct tag to enforce API
260 */
261 m = (struct tcpip_callback_msg *)msg;
262 error = tcpip_callbackmsg(m);
263
264 if (error == ERR_VAL) {
265 /* XXX: lwip thread is not running (mbox invalid) */
266 LWIP_ASSERT1(error != ERR_VAL);
267 }
268
269 LWIP_ASSERT1(error == ERR_OK);
270}
271
272
273/**
274 * Create a non-blocking socket. Disable SIGPIPE for TCP sockets if
275 * possible. On Linux it's not possible and should be disabled for
276 * each send(2) individually.
277 */
278static SOCKET
279proxy_create_socket(int sdom, int stype)
280{
281 SOCKET s;
282 int stype_and_flags;
283 int status;
284
285 LWIP_UNUSED_ARG(status); /* depends on ifdefs */
286
287
288 stype_and_flags = stype;
289
290#if defined(SOCK_NONBLOCK)
291 stype_and_flags |= SOCK_NONBLOCK;
292#endif
293
294 /*
295 * Disable SIGPIPE on disconnected socket. It might be easier to
296 * forgo it and just use MSG_NOSIGNAL on each send*(2), since we
297 * have to do it for Linux anyway, but Darwin does NOT have that
298 * flag (but has SO_NOSIGPIPE socket option).
299 */
300#if !defined(SOCK_NOSIGPIPE) && !defined(SO_NOSIGPIPE) && !defined(MSG_NOSIGNAL)
301#if 0 /* XXX: Solaris has neither, the program should ignore SIGPIPE globally */
302#error Need a way to disable SIGPIPE on connection oriented sockets!
303#endif
304#endif
305
306#if defined(SOCK_NOSIGPIPE)
307 if (stype == SOCK_STREAM) {
308 stype_and_flags |= SOCK_NOSIGPIPE;
309 }
310#endif
311
312 s = socket(sdom, stype_and_flags, 0);
313 if (s == INVALID_SOCKET) {
314 DPRINTF(("socket: %R[sockerr]\n", SOCKERRNO()));
315 return INVALID_SOCKET;
316 }
317
318#if defined(RT_OS_WINDOWS)
319 {
320 u_long mode = 1;
321 status = ioctlsocket(s, FIONBIO, &mode);
322 if (status == SOCKET_ERROR) {
323 DPRINTF(("FIONBIO: %R[sockerr]\n", SOCKERRNO()));
324 closesocket(s);
325 return INVALID_SOCKET;
326 }
327 }
328#elif !defined(SOCK_NONBLOCK)
329 {
330 int sflags;
331
332 sflags = fcntl(s, F_GETFL, 0);
333 if (sflags < 0) {
334 DPRINTF(("F_GETFL: %R[sockerr]\n", SOCKERRNO()));
335 closesocket(s);
336 return INVALID_SOCKET;
337 }
338
339 status = fcntl(s, F_SETFL, sflags | O_NONBLOCK);
340 if (status < 0) {
341 DPRINTF(("O_NONBLOCK: %R[sockerr]\n", SOCKERRNO()));
342 closesocket(s);
343 return INVALID_SOCKET;
344 }
345 }
346#endif
347
348#if !defined(SOCK_NOSIGPIPE) && defined(SO_NOSIGPIPE)
349 if (stype == SOCK_STREAM) {
350 int on = 1;
351 const socklen_t onlen = sizeof(on);
352
353 status = setsockopt(s, SOL_SOCKET, SO_NOSIGPIPE, &on, onlen);
354 if (status < 0) {
355 DPRINTF(("SO_NOSIGPIPE: %R[sockerr]\n", SOCKERRNO()));
356 closesocket(s);
357 return INVALID_SOCKET;
358 }
359 }
360#endif
361
362 /*
363 * Disable the Nagle algorithm. Otherwise the host may hold back
364 * packets that the guest wants to go out, causing potentially
365 * horrible performance. The guest is already applying the Nagle
366 * algorithm (or not) the way it wants.
367 */
368 if (stype == SOCK_STREAM) {
369 int on = 1;
370 const socklen_t onlen = sizeof(on);
371
372 status = setsockopt(s, IPPROTO_TCP, TCP_NODELAY, (char *)&on, onlen);
373 if (status < 0) {
374 DPRINTF(("TCP_NODELAY: %R[sockerr]\n", SOCKERRNO()));
375 closesocket(s);
376 return INVALID_SOCKET;
377 }
378 }
379
380#if defined(RT_OS_WINDOWS)
381 /*
382 * lwIP only holds one packet of "refused data" for us. Proxy
383 * relies on OS socket send buffer and doesn't do its own
384 * buffering. Unfortunately on Windows send buffer is very small
385 * (8K by default) and is not dynamically adpated by the OS it
386 * seems. So a single large write will fill it up and that will
387 * make lwIP drop segments, causing guest TCP into pathologic
388 * resend patterns. As a quick and dirty fix just bump it up.
389 */
390 if (stype == SOCK_STREAM) {
391 int sndbuf;
392 socklen_t optlen = sizeof(sndbuf);
393
394 status = getsockopt(s, SOL_SOCKET, SO_SNDBUF, (char *)&sndbuf, &optlen);
395 if (status == 0) {
396 if (sndbuf < 64 * 1024) {
397 sndbuf = 64 * 1024;
398 status = setsockopt(s, SOL_SOCKET, SO_SNDBUF,
399 (char *)&sndbuf, optlen);
400 if (status != 0) {
401 DPRINTF(("SO_SNDBUF: setsockopt: %R[sockerr]\n", SOCKERRNO()));
402 }
403 }
404 }
405 else {
406 DPRINTF(("SO_SNDBUF: getsockopt: %R[sockerr]\n", SOCKERRNO()));
407 }
408 }
409#endif
410
411 return s;
412}
413
414
415/**
416 * Create a socket for outbound connection to dst_addr:dst_port.
417 *
418 * The socket is non-blocking and TCP sockets has SIGPIPE disabled if
419 * possible. On Linux it's not possible and should be disabled for
420 * each send(2) individually.
421 */
422SOCKET
423proxy_connected_socket(int sdom, int stype,
424 ipX_addr_t *dst_addr, u16_t dst_port)
425{
426 struct sockaddr_in6 dst_sin6;
427 struct sockaddr_in dst_sin;
428 struct sockaddr *pdst_sa;
429 socklen_t dst_sa_len;
430 void *pdst_addr;
431 const struct sockaddr *psrc_sa;
432 socklen_t src_sa_len;
433 int status;
434 int sockerr;
435 SOCKET s;
436
437 LWIP_ASSERT1(sdom == PF_INET || sdom == PF_INET6);
438 LWIP_ASSERT1(stype == SOCK_STREAM || stype == SOCK_DGRAM);
439
440 DPRINTF(("---> %s ", stype == SOCK_STREAM ? "TCP" : "UDP"));
441 if (sdom == PF_INET6) {
442 pdst_sa = (struct sockaddr *)&dst_sin6;
443 pdst_addr = (void *)&dst_sin6.sin6_addr;
444
445 memset(&dst_sin6, 0, sizeof(dst_sin6));
446#if HAVE_SA_LEN
447 dst_sin6.sin6_len =
448#endif
449 dst_sa_len = sizeof(dst_sin6);
450 dst_sin6.sin6_family = AF_INET6;
451 memcpy(&dst_sin6.sin6_addr, &dst_addr->ip6, sizeof(ip6_addr_t));
452 dst_sin6.sin6_port = htons(dst_port);
453
454 DPRINTF(("[%RTnaipv6]:%d ", &dst_sin6.sin6_addr, dst_port));
455 }
456 else { /* sdom = PF_INET */
457 pdst_sa = (struct sockaddr *)&dst_sin;
458 pdst_addr = (void *)&dst_sin.sin_addr;
459
460 memset(&dst_sin, 0, sizeof(dst_sin));
461#if HAVE_SA_LEN
462 dst_sin.sin_len =
463#endif
464 dst_sa_len = sizeof(dst_sin);
465 dst_sin.sin_family = AF_INET;
466 dst_sin.sin_addr.s_addr = dst_addr->ip4.addr; /* byte-order? */
467 dst_sin.sin_port = htons(dst_port);
468
469 DPRINTF(("%RTnaipv4:%d ", dst_sin.sin_addr.s_addr, dst_port));
470 }
471
472 s = proxy_create_socket(sdom, stype);
473 if (s == INVALID_SOCKET) {
474 return INVALID_SOCKET;
475 }
476 DPRINTF(("socket %d\n", s));
477
478 /** @todo needs locking if dynamic modifyvm is allowed */
479 if (sdom == PF_INET6) {
480 psrc_sa = (const struct sockaddr *)g_proxy_options->src6;
481 src_sa_len = sizeof(struct sockaddr_in6);
482 }
483 else {
484 psrc_sa = (const struct sockaddr *)g_proxy_options->src4;
485 src_sa_len = sizeof(struct sockaddr_in);
486 }
487 if (psrc_sa != NULL) {
488 status = bind(s, psrc_sa, src_sa_len);
489 if (status == SOCKET_ERROR) {
490 sockerr = SOCKERRNO();
491 DPRINTF(("socket %d: bind: %R[sockerr]\n", s, sockerr));
492 closesocket(s);
493 SET_SOCKERRNO(sockerr);
494 return INVALID_SOCKET;
495 }
496 }
497
498 status = connect(s, pdst_sa, dst_sa_len);
499 if (status == SOCKET_ERROR
500#if !defined(RT_OS_WINDOWS)
501 && SOCKERRNO() != EINPROGRESS
502#else
503 && SOCKERRNO() != EWOULDBLOCK
504#endif
505 )
506 {
507 sockerr = SOCKERRNO();
508 DPRINTF(("socket %d: connect: %R[sockerr]\n", s, sockerr));
509 closesocket(s);
510 SET_SOCKERRNO(sockerr);
511 return INVALID_SOCKET;
512 }
513
514 return s;
515}
516
517
518/**
519 * Create a socket for inbound (port-forwarded) connections to
520 * src_addr (port is part of sockaddr, so not a separate argument).
521 *
522 * The socket is non-blocking and TCP sockets has SIGPIPE disabled if
523 * possible. On Linux it's not possible and should be disabled for
524 * each send(2) individually.
525 *
526 * TODO?: Support v6-mapped v4 so that user can specify she wants
527 * "udp" and get both versions?
528 */
529SOCKET
530proxy_bound_socket(int sdom, int stype, struct sockaddr *src_addr)
531{
532 SOCKET s;
533 int on;
534 const socklen_t onlen = sizeof(on);
535 int status;
536 int sockerr;
537
538 s = proxy_create_socket(sdom, stype);
539 if (s == INVALID_SOCKET) {
540 return INVALID_SOCKET;
541 }
542 DPRINTF(("socket %d\n", s));
543
544 on = 1;
545 status = setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char *)&on, onlen);
546 if (status < 0) { /* not good, but not fatal */
547 DPRINTF(("SO_REUSEADDR: %R[sockerr]\n", SOCKERRNO()));
548 }
549
550 status = bind(s, src_addr,
551 sdom == PF_INET ?
552 sizeof(struct sockaddr_in)
553 : sizeof(struct sockaddr_in6));
554 if (status == SOCKET_ERROR) {
555 sockerr = SOCKERRNO();
556 DPRINTF(("bind: %R[sockerr]\n", sockerr));
557 closesocket(s);
558 SET_SOCKERRNO(sockerr);
559 return INVALID_SOCKET;
560 }
561
562 if (stype == SOCK_STREAM) {
563 status = listen(s, 5);
564 if (status == SOCKET_ERROR) {
565 sockerr = SOCKERRNO();
566 DPRINTF(("listen: %R[sockerr]\n", sockerr));
567 closesocket(s);
568 SET_SOCKERRNO(sockerr);
569 return INVALID_SOCKET;
570 }
571 }
572
573 return s;
574}
575
576
577void
578proxy_reset_socket(SOCKET s)
579{
580 struct linger linger;
581
582 linger.l_onoff = 1;
583 linger.l_linger = 0;
584
585 /* On Windows we can run into issue here, perhaps SO_LINGER isn't enough, and
586 * we should use WSA{Send,Recv}Disconnect instead.
587 *
588 * Links for the reference:
589 * http://msdn.microsoft.com/en-us/library/windows/desktop/ms738547%28v=vs.85%29.aspx
590 * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4468997
591 */
592 setsockopt(s, SOL_SOCKET, SO_LINGER, (char *)&linger, sizeof(linger));
593
594 closesocket(s);
595}
596
597
598int
599proxy_sendto(SOCKET sock, struct pbuf *p, void *name, size_t namelen)
600{
601 struct pbuf *q;
602 size_t i, clen;
603#ifndef RT_OS_WINDOWS
604 struct msghdr mh;
605 ssize_t nsent;
606#else
607 DWORD nsent;
608#endif
609 int rc;
610 IOVEC fixiov[8]; /* fixed size (typical case) */
611 const size_t fixiovsize = sizeof(fixiov)/sizeof(fixiov[0]);
612 IOVEC *dyniov; /* dynamically sized */
613 IOVEC *iov;
614 int error = 0;
615
616 /*
617 * Static iov[] is usually enough since UDP protocols use small
618 * datagrams to avoid fragmentation, but be prepared.
619 */
620 clen = pbuf_clen(p);
621 if (clen > fixiovsize) {
622 /*
623 * XXX: TODO: check that clen is shorter than IOV_MAX
624 */
625 dyniov = (IOVEC *)malloc(clen * sizeof(*dyniov));
626 if (dyniov == NULL) {
627 error = -errno; /* sic: not a socket error */
628 goto out;
629 }
630 iov = dyniov;
631 }
632 else {
633 dyniov = NULL;
634 iov = fixiov;
635 }
636
637
638 for (q = p, i = 0; i < clen; q = q->next, ++i) {
639 LWIP_ASSERT1(q != NULL);
640
641 IOVEC_SET_BASE(iov[i], q->payload);
642 IOVEC_SET_LEN(iov[i], q->len);
643 }
644
645#ifndef RT_OS_WINDOWS
646 memset(&mh, 0, sizeof(mh));
647 mh.msg_name = name;
648 mh.msg_namelen = namelen;
649 mh.msg_iov = iov;
650 mh.msg_iovlen = clen;
651
652 nsent = sendmsg(sock, &mh, 0);
653 rc = (nsent >= 0) ? 0 : SOCKET_ERROR;
654#else
655 rc = WSASendTo(sock, iov, (DWORD)clen, &nsent, 0,
656 name, (int)namelen, NULL, NULL);
657#endif
658 if (rc == SOCKET_ERROR) {
659 error = SOCKERRNO();
660 DPRINTF(("%s: socket %d: sendmsg: %R[sockerr]\n",
661 __func__, sock, error));
662 error = -error;
663 }
664
665 out:
666 if (dyniov != NULL) {
667 free(dyniov);
668 }
669 return error;
670}
671
672
673static const char *lwiperr[] = {
674 "ERR_OK",
675 "ERR_MEM",
676 "ERR_BUF",
677 "ERR_TIMEOUT",
678 "ERR_RTE",
679 "ERR_INPROGRESS",
680 "ERR_VAL",
681 "ERR_WOULDBLOCK",
682 "ERR_USE",
683 "ERR_ISCONN",
684 "ERR_ABRT",
685 "ERR_RST",
686 "ERR_CLSD",
687 "ERR_CONN",
688 "ERR_ARG",
689 "ERR_IF"
690};
691
692
693const char *
694proxy_lwip_strerr(err_t error)
695{
696 static char buf[32];
697 int e = -error;
698
699 if (0 <= e && e < (int)__arraycount(lwiperr)) {
700 return lwiperr[e];
701 }
702 else {
703 RTStrPrintf(buf, sizeof(buf), "unknown error %d", error);
704 return buf;
705 }
706}
Note: See TracBrowser for help on using the repository browser.

© 2025 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette