VirtualBox

source: vbox/trunk/src/VBox/Main/src-server/NATNetworkImpl.cpp@ 106378

Last change on this file since 106378 was 106061, checked in by vboxsync, 3 months ago

Copyright year updates by scm.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 37.9 KB
Line 
1/* $Id: NATNetworkImpl.cpp 106061 2024-09-16 14:03:52Z vboxsync $ */
2/** @file
3 * INATNetwork implementation.
4 */
5
6/*
7 * Copyright (C) 2013-2024 Oracle and/or its affiliates.
8 *
9 * This file is part of VirtualBox base platform packages, as
10 * available from https://www.virtualbox.org.
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU General Public License
14 * as published by the Free Software Foundation, in version 3 of the
15 * License.
16 *
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with this program; if not, see <https://www.gnu.org/licenses>.
24 *
25 * SPDX-License-Identifier: GPL-3.0-only
26 */
27
28#define LOG_GROUP LOG_GROUP_MAIN_NATNETWORK
29#include "NetworkServiceRunner.h"
30#include "DHCPServerImpl.h"
31#include "NATNetworkImpl.h"
32#include "AutoCaller.h"
33
34#include <iprt/asm.h>
35#include <iprt/cpp/utils.h>
36#include <iprt/net.h>
37#include <iprt/cidr.h>
38#include <iprt/net.h>
39#include <VBox/com/array.h>
40#include <VBox/com/ptr.h>
41#include <VBox/settings.h>
42
43#include "EventImpl.h"
44#include "LoggingNew.h"
45
46#include "VirtualBoxImpl.h"
47#include <algorithm>
48#include <list>
49
50#ifndef RT_OS_WINDOWS
51# include <netinet/in.h>
52#else
53# define IN_LOOPBACKNET 127
54#endif
55
56
57// constructor / destructor
58/////////////////////////////////////////////////////////////////////////////
59struct NATNetwork::Data
60{
61 Data()
62 : pVirtualBox(NULL)
63 , offGateway(0)
64 , offDhcp(0)
65 {
66 }
67 virtual ~Data(){}
68 const ComObjPtr<EventSource> pEventSource;
69#ifdef VBOX_WITH_NAT_SERVICE
70 NATNetworkServiceRunner NATRunner;
71 ComObjPtr<IDHCPServer> dhcpServer;
72#endif
73 /** weak VirtualBox parent */
74 VirtualBox * const pVirtualBox;
75
76 /** NATNetwork settings */
77 settings::NATNetwork s;
78
79 com::Utf8Str IPv4Gateway;
80 com::Utf8Str IPv4NetworkMask;
81 com::Utf8Str IPv4DhcpServer;
82 com::Utf8Str IPv4DhcpServerLowerIp;
83 com::Utf8Str IPv4DhcpServerUpperIp;
84
85 uint32_t offGateway;
86 uint32_t offDhcp;
87
88 void recalculatePortForwarding(const RTNETADDRIPV4 &AddrNew, const RTNETADDRIPV4 &MaskNew);
89};
90
91
92NATNetwork::NATNetwork()
93 : m(NULL)
94{
95}
96
97
98NATNetwork::~NATNetwork()
99{
100}
101
102
103HRESULT NATNetwork::FinalConstruct()
104{
105 return BaseFinalConstruct();
106}
107
108
109void NATNetwork::FinalRelease()
110{
111 uninit();
112
113 BaseFinalRelease();
114}
115
116
117void NATNetwork::uninit()
118{
119 /* Enclose the state transition Ready->InUninit->NotReady */
120 AutoUninitSpan autoUninitSpan(this);
121 if (autoUninitSpan.uninitDone())
122 return;
123 unconst(m->pVirtualBox) = NULL;
124 delete m;
125 m = NULL;
126}
127
128HRESULT NATNetwork::init(VirtualBox *aVirtualBox, com::Utf8Str aName)
129{
130 AutoInitSpan autoInitSpan(this);
131 AssertReturn(autoInitSpan.isOk(), E_FAIL);
132
133 m = new Data();
134 /* share VirtualBox weakly */
135 unconst(m->pVirtualBox) = aVirtualBox;
136 m->s.strNetworkName = aName;
137 m->s.strIPv4NetworkCidr = "10.0.2.0/24";
138 m->offGateway = 1;
139 i_recalculateIPv6Prefix(); /* set m->strIPv6Prefix based on IPv4 */
140
141 settings::NATHostLoopbackOffset off;
142 off.strLoopbackHostAddress = "127.0.0.1";
143 off.u32Offset = (uint32_t)2;
144 m->s.llHostLoopbackOffsetList.push_back(off);
145
146 i_recalculateIpv4AddressAssignments();
147
148 HRESULT hrc = unconst(m->pEventSource).createObject();
149 if (FAILED(hrc)) throw hrc;
150
151 hrc = m->pEventSource->init();
152 if (FAILED(hrc)) throw hrc;
153
154 /* Confirm a successful initialization */
155 autoInitSpan.setSucceeded();
156
157 return S_OK;
158}
159
160
161HRESULT NATNetwork::setErrorBusy()
162{
163 return setError(E_FAIL,
164 tr("Unable to change settings"
165 " while NATNetwork instance is running"));
166}
167
168
169HRESULT NATNetwork::i_loadSettings(const settings::NATNetwork &data)
170{
171 AutoCaller autoCaller(this);
172 AssertComRCReturnRC(autoCaller.hrc());
173
174 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
175 m->s = data;
176 if ( m->s.strIPv6Prefix.isEmpty()
177 /* also clean up bogus old default */
178 || m->s.strIPv6Prefix == "fe80::/64")
179 i_recalculateIPv6Prefix(); /* set m->strIPv6Prefix based on IPv4 */
180 i_recalculateIpv4AddressAssignments();
181
182 return S_OK;
183}
184
185HRESULT NATNetwork::i_saveSettings(settings::NATNetwork &data)
186{
187 AutoCaller autoCaller(this);
188 if (FAILED(autoCaller.hrc())) return autoCaller.hrc();
189
190 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
191 AssertReturn(!m->s.strNetworkName.isEmpty(), E_FAIL);
192 data = m->s;
193
194 m->pVirtualBox->i_onNATNetworkSetting(m->s.strNetworkName,
195 m->s.fEnabled,
196 m->s.strIPv4NetworkCidr,
197 m->IPv4Gateway,
198 m->s.fAdvertiseDefaultIPv6Route,
199 m->s.fNeedDhcpServer);
200
201 /* Notify listeners listening on this network only */
202 ::FireNATNetworkSettingEvent(m->pEventSource,
203 m->s.strNetworkName,
204 m->s.fEnabled,
205 m->s.strIPv4NetworkCidr,
206 m->IPv4Gateway,
207 m->s.fAdvertiseDefaultIPv6Route,
208 m->s.fNeedDhcpServer);
209
210 return S_OK;
211}
212
213HRESULT NATNetwork::getEventSource(ComPtr<IEventSource> &aEventSource)
214{
215 /* event source is const, no need to lock */
216 m->pEventSource.queryInterfaceTo(aEventSource.asOutParam());
217 return S_OK;
218}
219
220HRESULT NATNetwork::getNetworkName(com::Utf8Str &aNetworkName)
221{
222 AssertReturn(!m->s.strNetworkName.isEmpty(), E_FAIL);
223 aNetworkName = m->s.strNetworkName;
224 return S_OK;
225}
226
227HRESULT NATNetwork::setNetworkName(const com::Utf8Str &aNetworkName)
228{
229 if (aNetworkName.isEmpty())
230 return setError(E_INVALIDARG,
231 tr("Network name cannot be empty"));
232
233 {
234 AutoReadLock alockNatNetList(m->pVirtualBox->i_getNatNetLock() COMMA_LOCKVAL_SRC_POS);
235 if (m->pVirtualBox->i_isNatNetStarted(m->s.strNetworkName))
236 return setErrorBusy();
237
238 /** @todo r=uwe who ensures there's no other network with that name? */
239
240 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
241 if (aNetworkName == m->s.strNetworkName)
242 return S_OK;
243
244 m->s.strNetworkName = aNetworkName;
245 }
246
247
248 AutoWriteLock vboxLock(m->pVirtualBox COMMA_LOCKVAL_SRC_POS);
249 HRESULT hrc = m->pVirtualBox->i_saveSettings();
250 ComAssertComRCRetRC(hrc);
251
252 return S_OK;
253}
254
255HRESULT NATNetwork::getEnabled(BOOL *aEnabled)
256{
257 *aEnabled = m->s.fEnabled;
258
259 i_recalculateIpv4AddressAssignments();
260 return S_OK;
261}
262
263HRESULT NATNetwork::setEnabled(const BOOL aEnabled)
264{
265 {
266 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
267 if (RT_BOOL(aEnabled) == m->s.fEnabled)
268 return S_OK;
269 m->s.fEnabled = RT_BOOL(aEnabled);
270 }
271
272 AutoWriteLock vboxLock(m->pVirtualBox COMMA_LOCKVAL_SRC_POS);
273 HRESULT hrc = m->pVirtualBox->i_saveSettings();
274 ComAssertComRCRetRC(hrc);
275 return S_OK;
276}
277
278HRESULT NATNetwork::getGateway(com::Utf8Str &aIPv4Gateway)
279{
280 aIPv4Gateway = m->IPv4Gateway;
281 return S_OK;
282}
283
284HRESULT NATNetwork::getNetwork(com::Utf8Str &aNetwork)
285{
286 aNetwork = m->s.strIPv4NetworkCidr;
287 return S_OK;
288}
289
290
291HRESULT NATNetwork::setNetwork(const com::Utf8Str &aIPv4NetworkCidr)
292{
293 RTNETADDRIPV4 Net;
294 int iPrefix;
295 int vrc = RTNetStrToIPv4Cidr(aIPv4NetworkCidr.c_str(), &Net, &iPrefix);
296 if (RT_FAILURE(vrc))
297 return setErrorBoth(E_FAIL, vrc, tr("%s is not a valid IPv4 CIDR notation"), aIPv4NetworkCidr.c_str());
298
299 /*
300 * /32 is a single address, not a network, /31 is the degenerate
301 * point-to-point case, so reject these. Larger values and
302 * negative values are already treated as errors by the
303 * conversion.
304 */
305 if (iPrefix > 30)
306 return setError(E_FAIL, tr("%s network is too small"), aIPv4NetworkCidr.c_str());
307
308 if (iPrefix == 0)
309 return setError(E_FAIL, tr("%s specifies zero prefix"), aIPv4NetworkCidr.c_str());
310
311 RTNETADDRIPV4 Mask;
312 vrc = RTNetPrefixToMaskIPv4(iPrefix, &Mask);
313 AssertRCReturn(vrc, setErrorBoth(E_FAIL, vrc, tr("%s: internal error: failed to convert prefix %d to netmask: %Rrc"),
314 aIPv4NetworkCidr.c_str(), iPrefix, vrc));
315
316 if ((Net.u & ~Mask.u) != 0)
317 return setError(E_FAIL, tr("%s: the specified address is longer than the specified prefix"),
318 aIPv4NetworkCidr.c_str());
319
320 /** @todo r=uwe Check the address is unicast, not a loopback, etc. */
321
322 /* normalized CIDR notation */
323 com::Utf8StrFmt strCidr("%RTnaipv4/%d", Net.u, iPrefix);
324
325 {
326 AutoReadLock alockNatNetList(m->pVirtualBox->i_getNatNetLock() COMMA_LOCKVAL_SRC_POS);
327 if (m->pVirtualBox->i_isNatNetStarted(m->s.strNetworkName))
328 return setErrorBusy();
329
330 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
331
332 if (m->s.strIPv4NetworkCidr == strCidr)
333 return S_OK;
334
335 m->recalculatePortForwarding(Net, Mask);
336
337 m->s.strIPv4NetworkCidr = strCidr;
338 i_recalculateIpv4AddressAssignments();
339 }
340
341 AutoWriteLock vboxLock(m->pVirtualBox COMMA_LOCKVAL_SRC_POS);
342 HRESULT hrc = m->pVirtualBox->i_saveSettings();
343 ComAssertComRCRetRC(hrc);
344 return S_OK;
345}
346
347
348/**
349 * Do best effort attempt at converting existing port forwarding rules
350 * from the old prefix to the new one. This might not be possible if
351 * the new prefix is longer (i.e. the network is smaller) or if a rule
352 * lists destination not from the network (though that rule wouldn't
353 * be terribly useful, at least currently).
354 */
355void NATNetwork::Data::recalculatePortForwarding(const RTNETADDRIPV4 &NetNew,
356 const RTNETADDRIPV4 &MaskNew)
357{
358 if (s.mapPortForwardRules4.empty())
359 return; /* nothing to do */
360
361 RTNETADDRIPV4 NetOld;
362 int iPrefixOld;
363 int vrc = RTNetStrToIPv4Cidr(s.strIPv4NetworkCidr.c_str(), &NetOld, &iPrefixOld);
364 if (RT_FAILURE(vrc))
365 return;
366
367 RTNETADDRIPV4 MaskOld;
368 vrc = RTNetPrefixToMaskIPv4(iPrefixOld, &MaskOld);
369 if (RT_FAILURE(vrc))
370 return;
371
372 for (settings::NATRulesMap::iterator it = s.mapPortForwardRules4.begin();
373 it != s.mapPortForwardRules4.end();
374 ++it)
375 {
376 settings::NATRule &rule = it->second;
377
378 /* parse the old destination address */
379 RTNETADDRIPV4 AddrOld;
380 vrc = RTNetStrToIPv4Addr(rule.strGuestIP.c_str(), &AddrOld);
381 if (RT_FAILURE(vrc))
382 continue;
383
384 /* is it in the old network? (likely) */
385 if ((AddrOld.u & MaskOld.u) != NetOld.u)
386 continue;
387
388 uint32_t u32Host = (AddrOld.u & ~MaskOld.u);
389
390 /* does it fit into the new network? */
391 if ((u32Host & MaskNew.u) != 0)
392 continue;
393
394 rule.strGuestIP.printf("%RTnaipv4", NetNew.u | u32Host);
395 }
396}
397
398
399HRESULT NATNetwork::getIPv6Enabled(BOOL *aIPv6Enabled)
400{
401 *aIPv6Enabled = m->s.fIPv6Enabled;
402
403 return S_OK;
404}
405
406
407HRESULT NATNetwork::setIPv6Enabled(const BOOL aIPv6Enabled)
408{
409 {
410 AutoReadLock alockNatNetList(m->pVirtualBox->i_getNatNetLock() COMMA_LOCKVAL_SRC_POS);
411 if (m->pVirtualBox->i_isNatNetStarted(m->s.strNetworkName))
412 return setErrorBusy();
413
414 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
415
416 if (RT_BOOL(aIPv6Enabled) == m->s.fIPv6Enabled)
417 return S_OK;
418
419 /*
420 * If we are enabling ipv6 and the prefix is not set, provide
421 * the default based on ipv4.
422 */
423 if (aIPv6Enabled && m->s.strIPv6Prefix.isEmpty())
424 i_recalculateIPv6Prefix();
425
426 m->s.fIPv6Enabled = RT_BOOL(aIPv6Enabled);
427 }
428
429 AutoWriteLock vboxLock(m->pVirtualBox COMMA_LOCKVAL_SRC_POS);
430 HRESULT hrc = m->pVirtualBox->i_saveSettings();
431 ComAssertComRCRetRC(hrc);
432
433 return S_OK;
434}
435
436
437HRESULT NATNetwork::getIPv6Prefix(com::Utf8Str &aIPv6Prefix)
438{
439 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
440
441 aIPv6Prefix = m->s.strIPv6Prefix;
442 return S_OK;
443}
444
445HRESULT NATNetwork::setIPv6Prefix(const com::Utf8Str &aIPv6Prefix)
446{
447 HRESULT hrc;
448 int vrc;
449
450 /* Since we store it in text form, use canonical representation */
451 com::Utf8Str strNormalizedIPv6Prefix;
452
453 const char *pcsz = RTStrStripL(aIPv6Prefix.c_str());
454 if (*pcsz != '\0') /* verify it first if not empty/blank */
455 {
456 RTNETADDRIPV6 Net6;
457 int iPrefixLength;
458 vrc = RTNetStrToIPv6Cidr(aIPv6Prefix.c_str(), &Net6, &iPrefixLength);
459 if (RT_FAILURE(vrc))
460 return setError(E_INVALIDARG, tr("%s is not a valid IPv6 prefix"), aIPv6Prefix.c_str());
461
462 /* Accept both addr:: and addr::/64 */
463 if (iPrefixLength == 128) /* no length was specified after the address? */
464 iPrefixLength = 64; /* take it to mean /64 which we require anyway */
465 else if (iPrefixLength != 64)
466 return setError(E_INVALIDARG, tr("Invalid IPv6 prefix length %d, must be 64"), iPrefixLength);
467
468 /* Verify the address is unicast. */
469 if ( (Net6.au8[0] & 0xe0) != 0x20 /* global 2000::/3 */
470 && (Net6.au8[0] & 0xfe) != 0xfc) /* local fc00::/7 */
471 return setError(E_INVALIDARG, tr("IPv6 prefix %RTnaipv6 is not unicast"), &Net6);
472
473 /* Verify the interfaces ID part is zero */
474 if (Net6.au64[1] != 0)
475 return setError(E_INVALIDARG, tr("Non-zero bits in the interface ID part of the IPv6 prefix %RTnaipv6/64"), &Net6);
476
477 vrc = strNormalizedIPv6Prefix.printfNoThrow("%RTnaipv6/64", &Net6);
478 if (RT_FAILURE(vrc))
479 {
480 if (vrc == VERR_NO_MEMORY)
481 return setError(E_OUTOFMEMORY);
482 return setError(E_FAIL, tr("Internal error"));
483 }
484 }
485
486 {
487 AutoReadLock alockNatNetList(m->pVirtualBox->i_getNatNetLock() COMMA_LOCKVAL_SRC_POS);
488 if (m->pVirtualBox->i_isNatNetStarted(m->s.strNetworkName))
489 return setErrorBusy();
490
491 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
492
493 if (strNormalizedIPv6Prefix == m->s.strIPv6Prefix)
494 return S_OK;
495
496 /* only allow prefix to be empty if IPv6 is disabled */
497 if (strNormalizedIPv6Prefix.isEmpty() && m->s.fIPv6Enabled)
498 return setError(E_FAIL, tr("Setting an empty IPv6 prefix when IPv6 is enabled"));
499
500 /**
501 * @todo
502 * silently ignore network IPv6 prefix update.
503 * todo: see similar todo in NATNetwork::COMSETTER(Network)(IN_BSTR)
504 */
505 if (!m->s.mapPortForwardRules6.empty())
506 return S_OK;
507
508 m->s.strIPv6Prefix = strNormalizedIPv6Prefix;
509 }
510
511 AutoWriteLock vboxLock(m->pVirtualBox COMMA_LOCKVAL_SRC_POS);
512 hrc = m->pVirtualBox->i_saveSettings();
513 ComAssertComRCRetRC(hrc);
514
515 return S_OK;
516}
517
518
519HRESULT NATNetwork::getAdvertiseDefaultIPv6RouteEnabled(BOOL *aAdvertiseDefaultIPv6Route)
520{
521 *aAdvertiseDefaultIPv6Route = m->s.fAdvertiseDefaultIPv6Route;
522
523 return S_OK;
524}
525
526
527HRESULT NATNetwork::setAdvertiseDefaultIPv6RouteEnabled(const BOOL aAdvertiseDefaultIPv6Route)
528{
529 {
530 AutoReadLock alockNatNetList(m->pVirtualBox->i_getNatNetLock() COMMA_LOCKVAL_SRC_POS);
531 if (m->pVirtualBox->i_isNatNetStarted(m->s.strNetworkName))
532 return setErrorBusy();
533
534 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
535
536 if (RT_BOOL(aAdvertiseDefaultIPv6Route) == m->s.fAdvertiseDefaultIPv6Route)
537 return S_OK;
538
539 m->s.fAdvertiseDefaultIPv6Route = RT_BOOL(aAdvertiseDefaultIPv6Route);
540
541 }
542
543 AutoWriteLock vboxLock(m->pVirtualBox COMMA_LOCKVAL_SRC_POS);
544 HRESULT hrc = m->pVirtualBox->i_saveSettings();
545 ComAssertComRCRetRC(hrc);
546
547 return S_OK;
548}
549
550
551HRESULT NATNetwork::getNeedDhcpServer(BOOL *aNeedDhcpServer)
552{
553 *aNeedDhcpServer = m->s.fNeedDhcpServer;
554
555 return S_OK;
556}
557
558HRESULT NATNetwork::setNeedDhcpServer(const BOOL aNeedDhcpServer)
559{
560 {
561 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
562
563 if (RT_BOOL(aNeedDhcpServer) == m->s.fNeedDhcpServer)
564 return S_OK;
565
566 m->s.fNeedDhcpServer = RT_BOOL(aNeedDhcpServer);
567
568 i_recalculateIpv4AddressAssignments();
569
570 }
571
572 AutoWriteLock vboxLock(m->pVirtualBox COMMA_LOCKVAL_SRC_POS);
573 HRESULT hrc = m->pVirtualBox->i_saveSettings();
574 ComAssertComRCRetRC(hrc);
575
576 return S_OK;
577}
578
579HRESULT NATNetwork::getLocalMappings(std::vector<com::Utf8Str> &aLocalMappings)
580{
581 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
582
583 aLocalMappings.resize(m->s.llHostLoopbackOffsetList.size());
584 size_t i = 0;
585 for (settings::NATLoopbackOffsetList::const_iterator it = m->s.llHostLoopbackOffsetList.begin();
586 it != m->s.llHostLoopbackOffsetList.end(); ++it, ++i)
587 {
588 aLocalMappings[i] = Utf8StrFmt("%s=%d",
589 (*it).strLoopbackHostAddress.c_str(),
590 (*it).u32Offset);
591 }
592
593 return S_OK;
594}
595
596HRESULT NATNetwork::addLocalMapping(const com::Utf8Str &aHostId, LONG aOffset)
597{
598 RTNETADDRIPV4 addr;
599 int vrc = RTNetStrToIPv4Addr(Utf8Str(aHostId).c_str(), &addr);
600 if (RT_FAILURE(vrc))
601 return E_INVALIDARG;
602
603 /* check against 127/8 */
604 if ((RT_N2H_U32(addr.u) >> IN_CLASSA_NSHIFT) != IN_LOOPBACKNET)
605 return E_INVALIDARG;
606
607 /* check against networkid vs network mask */
608 RTNETADDRIPV4 net, mask;
609 vrc = RTCidrStrToIPv4(Utf8Str(m->s.strIPv4NetworkCidr).c_str(), &net, &mask);
610 if (RT_FAILURE(vrc))
611 return E_INVALIDARG;
612
613 if (((net.u + (uint32_t)aOffset) & mask.u) != net.u)
614 return E_INVALIDARG;
615
616 settings::NATLoopbackOffsetList::iterator it;
617
618 it = std::find(m->s.llHostLoopbackOffsetList.begin(),
619 m->s.llHostLoopbackOffsetList.end(),
620 aHostId);
621 if (it != m->s.llHostLoopbackOffsetList.end())
622 {
623 if (aOffset == 0) /* erase */
624 m->s.llHostLoopbackOffsetList.erase(it, it);
625 else /* modify */
626 {
627 settings::NATLoopbackOffsetList::iterator it1;
628 it1 = std::find(m->s.llHostLoopbackOffsetList.begin(),
629 m->s.llHostLoopbackOffsetList.end(),
630 (uint32_t)aOffset);
631 if (it1 != m->s.llHostLoopbackOffsetList.end())
632 return E_INVALIDARG; /* this offset is already registered. */
633
634 (*it).u32Offset = (uint32_t)aOffset;
635 }
636
637 AutoWriteLock vboxLock(m->pVirtualBox COMMA_LOCKVAL_SRC_POS);
638 return m->pVirtualBox->i_saveSettings();
639 }
640
641 /* injection */
642 it = std::find(m->s.llHostLoopbackOffsetList.begin(),
643 m->s.llHostLoopbackOffsetList.end(),
644 (uint32_t)aOffset);
645
646 if (it != m->s.llHostLoopbackOffsetList.end())
647 return E_INVALIDARG; /* offset is already registered. */
648
649 settings::NATHostLoopbackOffset off;
650 off.strLoopbackHostAddress = aHostId;
651 off.u32Offset = (uint32_t)aOffset;
652 m->s.llHostLoopbackOffsetList.push_back(off);
653
654 AutoWriteLock vboxLock(m->pVirtualBox COMMA_LOCKVAL_SRC_POS);
655 return m->pVirtualBox->i_saveSettings();
656}
657
658
659HRESULT NATNetwork::getLoopbackIp6(LONG *aLoopbackIp6)
660{
661 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
662
663 *aLoopbackIp6 = (LONG)m->s.u32HostLoopback6Offset;
664 return S_OK;
665}
666
667
668HRESULT NATNetwork::setLoopbackIp6(LONG aLoopbackIp6)
669{
670 {
671 AutoReadLock alockNatNetList(m->pVirtualBox->i_getNatNetLock() COMMA_LOCKVAL_SRC_POS);
672 if (m->pVirtualBox->i_isNatNetStarted(m->s.strNetworkName))
673 return setErrorBusy();
674
675 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
676
677 if (aLoopbackIp6 < 0)
678 return E_INVALIDARG;
679
680 if (static_cast<uint32_t>(aLoopbackIp6) == m->s.u32HostLoopback6Offset)
681 return S_OK;
682
683 m->s.u32HostLoopback6Offset = (uint32_t)aLoopbackIp6;
684 }
685
686 AutoWriteLock vboxLock(m->pVirtualBox COMMA_LOCKVAL_SRC_POS);
687 return m->pVirtualBox->i_saveSettings();
688}
689
690
691HRESULT NATNetwork::getPortForwardRules4(std::vector<com::Utf8Str> &aPortForwardRules4)
692{
693 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
694 i_getPortForwardRulesFromMap(aPortForwardRules4,
695 m->s.mapPortForwardRules4);
696 return S_OK;
697}
698
699HRESULT NATNetwork::getPortForwardRules6(std::vector<com::Utf8Str> &aPortForwardRules6)
700{
701 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
702 i_getPortForwardRulesFromMap(aPortForwardRules6,
703 m->s.mapPortForwardRules6);
704 return S_OK;
705}
706
707HRESULT NATNetwork::addPortForwardRule(BOOL aIsIpv6,
708 const com::Utf8Str &aPortForwardRuleName,
709 NATProtocol_T aProto,
710 const com::Utf8Str &aHostIp,
711 USHORT aHostPort,
712 const com::Utf8Str &aGuestIp,
713 USHORT aGuestPort)
714{
715 {
716 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
717 Utf8Str name = aPortForwardRuleName;
718 Utf8Str proto;
719 settings::NATRule r;
720 settings::NATRulesMap &mapRules = aIsIpv6 ? m->s.mapPortForwardRules6 : m->s.mapPortForwardRules4;
721 switch (aProto)
722 {
723 case NATProtocol_TCP:
724 proto = "tcp";
725 break;
726 case NATProtocol_UDP:
727 proto = "udp";
728 break;
729 default:
730 return E_INVALIDARG;
731 }
732 if (name.isEmpty())
733 name = Utf8StrFmt("%s_[%s]%%%d_[%s]%%%d", proto.c_str(),
734 aHostIp.c_str(), aHostPort,
735 aGuestIp.c_str(), aGuestPort);
736
737 for (settings::NATRulesMap::iterator it = mapRules.begin(); it != mapRules.end(); ++it)
738 {
739 r = it->second;
740 if (it->first == name)
741 return setError(E_INVALIDARG,
742 tr("A NAT rule of this name already exists"));
743 if ( r.strHostIP == aHostIp
744 && r.u16HostPort == aHostPort
745 && r.proto == aProto)
746 return setError(E_INVALIDARG,
747 tr("A NAT rule for this host port and this host IP already exists"));
748 }
749
750 r.strName = name.c_str();
751 r.proto = aProto;
752 r.strHostIP = aHostIp;
753 r.u16HostPort = aHostPort;
754 r.strGuestIP = aGuestIp;
755 r.u16GuestPort = aGuestPort;
756 mapRules.insert(std::make_pair(name, r));
757 }
758 {
759 AutoWriteLock vboxLock(m->pVirtualBox COMMA_LOCKVAL_SRC_POS);
760 HRESULT hrc = m->pVirtualBox->i_saveSettings();
761 ComAssertComRCRetRC(hrc);
762 }
763
764 m->pVirtualBox->i_onNATNetworkPortForward(m->s.strNetworkName, TRUE, aIsIpv6,
765 aPortForwardRuleName, aProto,
766 aHostIp, aHostPort,
767 aGuestIp, aGuestPort);
768
769 /* Notify listeners listening on this network only */
770 ::FireNATNetworkPortForwardEvent(m->pEventSource, m->s.strNetworkName, TRUE,
771 aIsIpv6, aPortForwardRuleName, aProto,
772 aHostIp, aHostPort,
773 aGuestIp, aGuestPort);
774
775 return S_OK;
776}
777
778HRESULT NATNetwork::removePortForwardRule(BOOL aIsIpv6, const com::Utf8Str &aPortForwardRuleName)
779{
780 Utf8Str strHostIP;
781 Utf8Str strGuestIP;
782 uint16_t u16HostPort;
783 uint16_t u16GuestPort;
784 NATProtocol_T proto;
785
786 {
787 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
788 settings::NATRulesMap &mapRules = aIsIpv6 ? m->s.mapPortForwardRules6 : m->s.mapPortForwardRules4;
789 settings::NATRulesMap::iterator it = mapRules.find(aPortForwardRuleName);
790
791 if (it == mapRules.end())
792 return E_INVALIDARG;
793
794 strHostIP = it->second.strHostIP;
795 strGuestIP = it->second.strGuestIP;
796 u16HostPort = it->second.u16HostPort;
797 u16GuestPort = it->second.u16GuestPort;
798 proto = it->second.proto;
799
800 mapRules.erase(it);
801 }
802
803 {
804 AutoWriteLock vboxLock(m->pVirtualBox COMMA_LOCKVAL_SRC_POS);
805 HRESULT hrc = m->pVirtualBox->i_saveSettings();
806 ComAssertComRCRetRC(hrc);
807 }
808
809 m->pVirtualBox->i_onNATNetworkPortForward(m->s.strNetworkName, FALSE, aIsIpv6, aPortForwardRuleName, proto,
810 strHostIP, u16HostPort, strGuestIP, u16GuestPort);
811
812 /* Notify listeners listening on this network only */
813 ::FireNATNetworkPortForwardEvent(m->pEventSource, m->s.strNetworkName, FALSE, aIsIpv6, aPortForwardRuleName, proto,
814 strHostIP, u16HostPort, strGuestIP, u16GuestPort);
815 return S_OK;
816}
817
818
819void NATNetwork::i_updateDomainNameOption(ComPtr<IHost> &host)
820{
821 com::Bstr domain;
822 if (FAILED(host->COMGETTER(DomainName)(domain.asOutParam())))
823 LogRel(("NATNetwork: Failed to get host's domain name\n"));
824 ComPtr<IDHCPGlobalConfig> pDHCPConfig;
825 HRESULT hrc = m->dhcpServer->COMGETTER(GlobalConfig)(pDHCPConfig.asOutParam());
826 if (FAILED(hrc))
827 {
828 LogRel(("NATNetwork: Failed to get global DHCP config when updating domain name option with %Rhrc\n", hrc));
829 return;
830 }
831 if (domain.isNotEmpty())
832 {
833 hrc = pDHCPConfig->SetOption(DHCPOption_DomainName, DHCPOptionEncoding_Normal, domain.raw());
834 if (FAILED(hrc))
835 LogRel(("NATNetwork: Failed to add domain name option with %Rhrc\n", hrc));
836 }
837 else
838 pDHCPConfig->RemoveOption(DHCPOption_DomainName);
839}
840
841void NATNetwork::i_updateDomainNameServerOption(ComPtr<IHost> &host)
842{
843 RTNETADDRIPV4 networkid, netmask;
844 int vrc = RTCidrStrToIPv4(m->s.strIPv4NetworkCidr.c_str(), &networkid, &netmask);
845 if (RT_FAILURE(vrc))
846 {
847 LogRel(("NATNetwork: Failed to parse cidr %s with %Rrc\n", m->s.strIPv4NetworkCidr.c_str(), vrc));
848 return;
849 }
850
851 /* XXX: these are returned, surprisingly, in host order */
852 networkid.u = RT_H2N_U32(networkid.u);
853 netmask.u = RT_H2N_U32(netmask.u);
854
855 com::SafeArray<BSTR> nameServers;
856 HRESULT hrc = host->COMGETTER(NameServers)(ComSafeArrayAsOutParam(nameServers));
857 if (FAILED(hrc))
858 {
859 LogRel(("NATNetwork: Failed to get name servers from host with %Rhrc\n", hrc));
860 return;
861 }
862 ComPtr<IDHCPGlobalConfig> pDHCPConfig;
863 hrc = m->dhcpServer->COMGETTER(GlobalConfig)(pDHCPConfig.asOutParam());
864 if (FAILED(hrc))
865 {
866 LogRel(("NATNetwork: Failed to get global DHCP config when updating domain name server option with %Rhrc\n", hrc));
867 return;
868 }
869
870 size_t cAddresses = nameServers.size();
871 if (cAddresses)
872 {
873 RTCList<RTCString> lstServers;
874 /* The following code was copied (and adapted a bit) from VBoxNetDhcp::hostDnsServers */
875 /*
876 * Recent fashion is to run dnsmasq on 127.0.1.1 which we
877 * currently can't map. If that's the only nameserver we've got,
878 * we need to use DNS proxy for VMs to reach it.
879 */
880 bool fUnmappedLoopback = false;
881
882 for (size_t i = 0; i < cAddresses; ++i)
883 {
884 com::Utf8Str strNameServerAddress(nameServers[i]);
885 RTNETADDRIPV4 addr;
886 vrc = RTNetStrToIPv4Addr(strNameServerAddress.c_str(), &addr);
887 if (RT_FAILURE(vrc))
888 {
889 LogRel(("NATNetwork: Failed to parse IP address %s with %Rrc\n", strNameServerAddress.c_str(), vrc));
890 continue;
891 }
892
893 if (addr.u == INADDR_ANY)
894 {
895 /*
896 * This doesn't seem to be very well documented except for
897 * RTFS of res_init.c, but INADDR_ANY is a valid value for
898 * for "nameserver".
899 */
900 addr.u = RT_H2N_U32_C(INADDR_LOOPBACK);
901 }
902
903 if (addr.au8[0] == 127)
904 {
905 settings::NATLoopbackOffsetList::const_iterator it;
906
907 it = std::find(m->s.llHostLoopbackOffsetList.begin(),
908 m->s.llHostLoopbackOffsetList.end(),
909 strNameServerAddress);
910 if (it == m->s.llHostLoopbackOffsetList.end())
911 {
912 fUnmappedLoopback = true;
913 continue;
914 }
915 addr.u = RT_H2N_U32(RT_N2H_U32(networkid.u) + it->u32Offset);
916 }
917 lstServers.append(RTCStringFmt("%RTnaipv4", addr));
918 }
919
920 if (lstServers.isEmpty() && fUnmappedLoopback)
921 lstServers.append(RTCStringFmt("%RTnaipv4", networkid.u | RT_H2N_U32_C(1U))); /* proxy */
922
923 hrc = pDHCPConfig->SetOption(DHCPOption_DomainNameServers, DHCPOptionEncoding_Normal, Bstr(RTCString::join(lstServers, " ")).raw());
924 if (FAILED(hrc))
925 LogRel(("NATNetwork: Failed to add domain name server option '%s' with %Rhrc\n", RTCString::join(lstServers, " ").c_str(), hrc));
926 }
927 else
928 pDHCPConfig->RemoveOption(DHCPOption_DomainNameServers);
929}
930
931void NATNetwork::i_updateDnsOptions()
932{
933 ComPtr<IHost> host;
934 if (SUCCEEDED(m->pVirtualBox->COMGETTER(Host)(host.asOutParam())))
935 {
936 i_updateDomainNameOption(host);
937 i_updateDomainNameServerOption(host);
938 }
939}
940
941
942HRESULT NATNetwork::start()
943{
944#ifdef VBOX_WITH_NAT_SERVICE
945 if (!m->s.fEnabled) return S_OK;
946 AssertReturn(!m->s.strNetworkName.isEmpty(), E_FAIL);
947
948 m->NATRunner.resetArguments();
949 m->NATRunner.addArgPair(NetworkServiceRunner::kpszKeyNetwork, Utf8Str(m->s.strNetworkName).c_str());
950
951 /* No portforwarding rules from command-line, all will be fetched via API */
952
953 if (m->s.fNeedDhcpServer)
954 {
955 /*
956 * Just to as idea... via API (on creation user pass the cidr of network and)
957 * and we calculate it's addreses (mutable?).
958 */
959
960 /*
961 * Configuration and running DHCP server:
962 * 1. find server first createDHCPServer
963 * 2. if return status is E_INVALARG => server already exists just find and start.
964 * 3. if return status neither E_INVALRG nor S_OK => return E_FAIL
965 * 4. if return status S_OK proceed to DHCP server configuration
966 * 5. call setConfiguration() and pass all required parameters
967 * 6. start dhcp server.
968 */
969 HRESULT hrc = m->pVirtualBox->FindDHCPServerByNetworkName(Bstr(m->s.strNetworkName).raw(),
970 m->dhcpServer.asOutParam());
971 switch (hrc)
972 {
973 case E_INVALIDARG:
974 /* server haven't beeen found let create it then */
975 hrc = m->pVirtualBox->CreateDHCPServer(Bstr(m->s.strNetworkName).raw(),
976 m->dhcpServer.asOutParam());
977 if (FAILED(hrc))
978 return E_FAIL;
979 /* breakthrough */
980
981 {
982 LogFunc(("gateway: %s, dhcpserver:%s, dhcplowerip:%s, dhcpupperip:%s\n",
983 m->IPv4Gateway.c_str(),
984 m->IPv4DhcpServer.c_str(),
985 m->IPv4DhcpServerLowerIp.c_str(),
986 m->IPv4DhcpServerUpperIp.c_str()));
987
988 hrc = m->dhcpServer->COMSETTER(Enabled)(true);
989
990 hrc = m->dhcpServer->SetConfiguration(Bstr(m->IPv4DhcpServer).raw(),
991 Bstr(m->IPv4NetworkMask).raw(),
992 Bstr(m->IPv4DhcpServerLowerIp).raw(),
993 Bstr(m->IPv4DhcpServerUpperIp).raw());
994 break;
995 }
996 case S_OK:
997 break;
998
999 default:
1000 return E_FAIL;
1001 }
1002
1003#ifdef VBOX_WITH_DHCPD
1004 i_updateDnsOptions();
1005#endif /* VBOX_WITH_DHCPD */
1006 /* XXX: AddGlobalOption(DhcpOpt_Router,) - enables attachement of DhcpServer to Main (no longer true with VBoxNetDhcpd). */
1007 ComPtr<IDHCPGlobalConfig> pDHCPConfig;
1008 hrc = m->dhcpServer->COMGETTER(GlobalConfig)(pDHCPConfig.asOutParam());
1009 if (FAILED(hrc))
1010 {
1011 LogRel(("NATNetwork: Failed to get global DHCP config when updating IPv4 gateway option with %Rhrc\n", hrc));
1012 m->dhcpServer.setNull();
1013 return E_FAIL;
1014 }
1015 pDHCPConfig->SetOption(DHCPOption_Routers, DHCPOptionEncoding_Normal, Bstr(m->IPv4Gateway).raw());
1016
1017 hrc = m->dhcpServer->Start(Bstr::Empty.raw(), Bstr(TRUNKTYPE_WHATEVER).raw());
1018 if (FAILED(hrc))
1019 {
1020 m->dhcpServer.setNull();
1021 return E_FAIL;
1022 }
1023 }
1024
1025 if (RT_SUCCESS(m->NATRunner.start(false /* KillProcOnStop */)))
1026 {
1027 m->pVirtualBox->i_onNATNetworkStartStop(m->s.strNetworkName, TRUE);
1028 return S_OK;
1029 }
1030 /** @todo missing setError()! */
1031 return E_FAIL;
1032#else
1033 ReturnComNotImplemented();
1034#endif
1035}
1036
1037HRESULT NATNetwork::stop()
1038{
1039#ifdef VBOX_WITH_NAT_SERVICE
1040 m->pVirtualBox->i_onNATNetworkStartStop(m->s.strNetworkName, FALSE);
1041
1042 if (!m->dhcpServer.isNull())
1043 m->dhcpServer->Stop();
1044
1045 if (RT_SUCCESS(m->NATRunner.stop()))
1046 return S_OK;
1047
1048 /** @todo missing setError()! */
1049 return E_FAIL;
1050#else
1051 ReturnComNotImplemented();
1052#endif
1053}
1054
1055
1056void NATNetwork::i_getPortForwardRulesFromMap(std::vector<com::Utf8Str> &aPortForwardRules, settings::NATRulesMap &aRules)
1057{
1058 aPortForwardRules.resize(aRules.size());
1059 size_t i = 0;
1060 for (settings::NATRulesMap::const_iterator it = aRules.begin();
1061 it != aRules.end(); ++it, ++i)
1062 {
1063 settings::NATRule r = it->second;
1064 aPortForwardRules[i] = Utf8StrFmt("%s:%s:[%s]:%d:[%s]:%d",
1065 r.strName.c_str(),
1066 (r.proto == NATProtocol_TCP ? "tcp" : "udp"),
1067 r.strHostIP.c_str(),
1068 r.u16HostPort,
1069 r.strGuestIP.c_str(),
1070 r.u16GuestPort);
1071 }
1072}
1073
1074
1075int NATNetwork::i_findFirstAvailableOffset(ADDRESSLOOKUPTYPE addrType, uint32_t *poff)
1076{
1077 RTNETADDRIPV4 network, netmask;
1078 int vrc = RTCidrStrToIPv4(m->s.strIPv4NetworkCidr.c_str(), &network, &netmask);
1079 AssertRCReturn(vrc, vrc);
1080
1081 uint32_t off;
1082 for (off = 1; off < ~netmask.u; ++off)
1083 {
1084 bool skip = false;
1085 for (settings::NATLoopbackOffsetList::iterator it = m->s.llHostLoopbackOffsetList.begin();
1086 it != m->s.llHostLoopbackOffsetList.end();
1087 ++it)
1088 {
1089 if ((*it).u32Offset == off)
1090 {
1091 skip = true;
1092 break;
1093 }
1094
1095 }
1096
1097 if (skip)
1098 continue;
1099
1100 if (off == m->offGateway)
1101 {
1102 if (addrType == ADDR_GATEWAY)
1103 break;
1104 else
1105 continue;
1106 }
1107
1108 if (off == m->offDhcp)
1109 {
1110 if (addrType == ADDR_DHCP)
1111 break;
1112 else
1113 continue;
1114 }
1115
1116 if (!skip)
1117 break;
1118 }
1119
1120 if (poff)
1121 *poff = off;
1122
1123 return VINF_SUCCESS;
1124}
1125
1126int NATNetwork::i_recalculateIpv4AddressAssignments()
1127{
1128 RTNETADDRIPV4 network, netmask;
1129 int vrc = RTCidrStrToIPv4(m->s.strIPv4NetworkCidr.c_str(), &network, &netmask);
1130 AssertRCReturn(vrc, vrc);
1131
1132 i_findFirstAvailableOffset(ADDR_GATEWAY, &m->offGateway);
1133 if (m->s.fNeedDhcpServer)
1134 i_findFirstAvailableOffset(ADDR_DHCP, &m->offDhcp);
1135
1136 /* I don't remember the reason CIDR calculated on the host. */
1137 RTNETADDRIPV4 gateway = network;
1138 gateway.u += m->offGateway;
1139 gateway.u = RT_H2N_U32(gateway.u);
1140 char szTmpIp[16];
1141 RTStrPrintf(szTmpIp, sizeof(szTmpIp), "%RTnaipv4", gateway);
1142 m->IPv4Gateway = szTmpIp;
1143
1144 if (m->s.fNeedDhcpServer)
1145 {
1146 RTNETADDRIPV4 dhcpserver = network;
1147 dhcpserver.u += m->offDhcp;
1148
1149 /* XXX: adding more services should change the math here */
1150 RTNETADDRIPV4 dhcplowerip = network;
1151 uint32_t offDhcpLowerIp;
1152 i_findFirstAvailableOffset(ADDR_DHCPLOWERIP, &offDhcpLowerIp);
1153 dhcplowerip.u = RT_H2N_U32(dhcplowerip.u + offDhcpLowerIp);
1154
1155 RTNETADDRIPV4 dhcpupperip;
1156 dhcpupperip.u = RT_H2N_U32((network.u | ~netmask.u) - 1);
1157
1158 dhcpserver.u = RT_H2N_U32(dhcpserver.u);
1159 network.u = RT_H2N_U32(network.u);
1160
1161 RTStrPrintf(szTmpIp, sizeof(szTmpIp), "%RTnaipv4", dhcpserver);
1162 m->IPv4DhcpServer = szTmpIp;
1163 RTStrPrintf(szTmpIp, sizeof(szTmpIp), "%RTnaipv4", dhcplowerip);
1164 m->IPv4DhcpServerLowerIp = szTmpIp;
1165 RTStrPrintf(szTmpIp, sizeof(szTmpIp), "%RTnaipv4", dhcpupperip);
1166 m->IPv4DhcpServerUpperIp = szTmpIp;
1167
1168 LogFunc(("network:%RTnaipv4, dhcpserver:%RTnaipv4, dhcplowerip:%RTnaipv4, dhcpupperip:%RTnaipv4\n",
1169 network, dhcpserver, dhcplowerip, dhcpupperip));
1170 }
1171
1172 /* we need IPv4NetworkMask for NAT's gw service start */
1173 netmask.u = RT_H2N_U32(netmask.u);
1174 RTStrPrintf(szTmpIp, 16, "%RTnaipv4", netmask);
1175 m->IPv4NetworkMask = szTmpIp;
1176
1177 LogFlowFunc(("getaway:%RTnaipv4, netmask:%RTnaipv4\n", gateway, netmask));
1178 return VINF_SUCCESS;
1179}
1180
1181
1182int NATNetwork::i_recalculateIPv6Prefix()
1183{
1184 RTNETADDRIPV4 net, mask;
1185 int vrc = RTCidrStrToIPv4(Utf8Str(m->s.strIPv4NetworkCidr).c_str(), &net, &mask);
1186 if (RT_FAILURE(vrc))
1187 return vrc;
1188
1189 net.u = RT_H2N_U32(net.u); /* XXX: fix RTCidrStrToIPv4! */
1190
1191 /*
1192 * [fd17:625c:f037:XXXX::/64] - RFC 4193 (ULA) Locally Assigned
1193 * Global ID where XXXX, 16 bit Subnet ID, are two bytes from the
1194 * middle of the IPv4 address, e.g. :dead: for 10.222.173.1
1195 */
1196 RTNETADDRIPV6 prefix;
1197 RT_ZERO(prefix);
1198
1199 prefix.au8[0] = 0xFD;
1200 prefix.au8[1] = 0x17;
1201
1202 prefix.au8[2] = 0x62;
1203 prefix.au8[3] = 0x5C;
1204
1205 prefix.au8[4] = 0xF0;
1206 prefix.au8[5] = 0x37;
1207
1208 prefix.au8[6] = net.au8[1];
1209 prefix.au8[7] = net.au8[2];
1210
1211 char szBuf[32];
1212 RTStrPrintf(szBuf, sizeof(szBuf), "%RTnaipv6/64", &prefix);
1213
1214 m->s.strIPv6Prefix = szBuf;
1215 return VINF_SUCCESS;
1216}
Note: See TracBrowser for help on using the repository browser.

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