VirtualBox

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

Last change on this file since 98103 was 98103, checked in by vboxsync, 20 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: 38.1 KB
Line 
1/* $Id: NATNetworkImpl.cpp 98103 2023-01-17 14:15:46Z vboxsync $ */
2/** @file
3 * INATNetwork implementation.
4 */
5
6/*
7 * Copyright (C) 2013-2023 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.rc());
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.rc())) return autoCaller.rc();
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 rc = m->pVirtualBox->i_saveSettings();
250 ComAssertComRCRetRC(rc);
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 rc = m->pVirtualBox->i_saveSettings();
274 ComAssertComRCRetRC(rc);
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, Mask;
294 int iPrefix;
295 int rc;
296
297 rc = RTNetStrToIPv4Cidr(aIPv4NetworkCidr.c_str(), &Net, &iPrefix);
298 if (RT_FAILURE(rc))
299 return setError(E_FAIL, tr("%s is not a valid IPv4 CIDR notation"),
300 aIPv4NetworkCidr.c_str());
301
302 /*
303 * /32 is a single address, not a network, /31 is the degenerate
304 * point-to-point case, so reject these. Larger values and
305 * negative values are already treated as errors by the
306 * conversion.
307 */
308 if (iPrefix > 30)
309 return setError(E_FAIL, tr("%s network is too small"), aIPv4NetworkCidr.c_str());
310
311 if (iPrefix == 0)
312 return setError(E_FAIL, tr("%s specifies zero prefix"), aIPv4NetworkCidr.c_str());
313
314 rc = RTNetPrefixToMaskIPv4(iPrefix, &Mask);
315 AssertRCReturn(rc, setError(E_FAIL,
316 "%s: internal error: failed to convert prefix %d to netmask: %Rrc",
317 aIPv4NetworkCidr.c_str(), iPrefix, rc));
318
319 if ((Net.u & ~Mask.u) != 0)
320 return setError(E_FAIL,
321 tr("%s: the specified address is longer than the specified prefix"),
322 aIPv4NetworkCidr.c_str());
323
324 /** @todo r=uwe Check the address is unicast, not a loopback, etc. */
325
326 /* normalized CIDR notation */
327 com::Utf8StrFmt strCidr("%RTnaipv4/%d", Net.u, iPrefix);
328
329 {
330 AutoReadLock alockNatNetList(m->pVirtualBox->i_getNatNetLock() COMMA_LOCKVAL_SRC_POS);
331 if (m->pVirtualBox->i_isNatNetStarted(m->s.strNetworkName))
332 return setErrorBusy();
333
334 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
335
336 if (m->s.strIPv4NetworkCidr == strCidr)
337 return S_OK;
338
339 m->recalculatePortForwarding(Net, Mask);
340
341 m->s.strIPv4NetworkCidr = strCidr;
342 i_recalculateIpv4AddressAssignments();
343 }
344
345 AutoWriteLock vboxLock(m->pVirtualBox COMMA_LOCKVAL_SRC_POS);
346 HRESULT hrc = m->pVirtualBox->i_saveSettings();
347 ComAssertComRCRetRC(hrc);
348 return S_OK;
349}
350
351
352/**
353 * Do best effort attempt at converting existing port forwarding rules
354 * from the old prefix to the new one. This might not be possible if
355 * the new prefix is longer (i.e. the network is smaller) or if a rule
356 * lists destination not from the network (though that rule wouldn't
357 * be terribly useful, at least currently).
358 */
359void NATNetwork::Data::recalculatePortForwarding(const RTNETADDRIPV4 &NetNew,
360 const RTNETADDRIPV4 &MaskNew)
361{
362 RTNETADDRIPV4 NetOld, MaskOld;
363 int iPrefixOld;
364 int rc;
365
366 if (s.mapPortForwardRules4.empty())
367 return; /* nothing to do */
368
369 rc = RTNetStrToIPv4Cidr(s.strIPv4NetworkCidr.c_str(), &NetOld, &iPrefixOld);
370 if (RT_FAILURE(rc))
371 return;
372
373 rc = RTNetPrefixToMaskIPv4(iPrefixOld, &MaskOld);
374 if (RT_FAILURE(rc))
375 return;
376
377 for (settings::NATRulesMap::iterator it = s.mapPortForwardRules4.begin();
378 it != s.mapPortForwardRules4.end();
379 ++it)
380 {
381 settings::NATRule &rule = it->second;
382
383 /* parse the old destination address */
384 RTNETADDRIPV4 AddrOld;
385 rc = RTNetStrToIPv4Addr(rule.strGuestIP.c_str(), &AddrOld);
386 if (RT_FAILURE(rc))
387 continue;
388
389 /* is it in the old network? (likely) */
390 if ((AddrOld.u & MaskOld.u) != NetOld.u)
391 continue;
392
393 uint32_t u32Host = (AddrOld.u & ~MaskOld.u);
394
395 /* does it fit into the new network? */
396 if ((u32Host & MaskNew.u) != 0)
397 continue;
398
399 rule.strGuestIP.printf("%RTnaipv4", NetNew.u | u32Host);
400 }
401}
402
403
404HRESULT NATNetwork::getIPv6Enabled(BOOL *aIPv6Enabled)
405{
406 *aIPv6Enabled = m->s.fIPv6Enabled;
407
408 return S_OK;
409}
410
411
412HRESULT NATNetwork::setIPv6Enabled(const BOOL aIPv6Enabled)
413{
414 {
415 AutoReadLock alockNatNetList(m->pVirtualBox->i_getNatNetLock() COMMA_LOCKVAL_SRC_POS);
416 if (m->pVirtualBox->i_isNatNetStarted(m->s.strNetworkName))
417 return setErrorBusy();
418
419 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
420
421 if (RT_BOOL(aIPv6Enabled) == m->s.fIPv6Enabled)
422 return S_OK;
423
424 /*
425 * If we are enabling ipv6 and the prefix is not set, provide
426 * the default based on ipv4.
427 */
428 if (aIPv6Enabled && m->s.strIPv6Prefix.isEmpty())
429 i_recalculateIPv6Prefix();
430
431 m->s.fIPv6Enabled = RT_BOOL(aIPv6Enabled);
432 }
433
434 AutoWriteLock vboxLock(m->pVirtualBox COMMA_LOCKVAL_SRC_POS);
435 HRESULT rc = m->pVirtualBox->i_saveSettings();
436 ComAssertComRCRetRC(rc);
437
438 return S_OK;
439}
440
441
442HRESULT NATNetwork::getIPv6Prefix(com::Utf8Str &aIPv6Prefix)
443{
444 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
445
446 aIPv6Prefix = m->s.strIPv6Prefix;
447 return S_OK;
448}
449
450HRESULT NATNetwork::setIPv6Prefix(const com::Utf8Str &aIPv6Prefix)
451{
452 HRESULT hrc;
453 int rc;
454
455 /* Since we store it in text form, use canonical representation */
456 com::Utf8Str strNormalizedIPv6Prefix;
457
458 const char *pcsz = RTStrStripL(aIPv6Prefix.c_str());
459 if (*pcsz != '\0') /* verify it first if not empty/blank */
460 {
461 RTNETADDRIPV6 Net6;
462 int iPrefixLength;
463 rc = RTNetStrToIPv6Cidr(aIPv6Prefix.c_str(), &Net6, &iPrefixLength);
464 if (RT_FAILURE(rc))
465 return setError(E_INVALIDARG,
466 tr("%s is not a valid IPv6 prefix"),
467 aIPv6Prefix.c_str());
468
469 /* Accept both addr:: and addr::/64 */
470 if (iPrefixLength == 128) /* no length was specified after the address? */
471 iPrefixLength = 64; /* take it to mean /64 which we require anyway */
472 else if (iPrefixLength != 64)
473 return setError(E_INVALIDARG,
474 tr("Invalid IPv6 prefix length %d, must be 64"),
475 iPrefixLength);
476
477 /* Verify the address is unicast. */
478 if ( ((Net6.au8[0] & 0xe0) != 0x20) /* global 2000::/3 */
479 && ((Net6.au8[0] & 0xfe) != 0xfc)) /* local fc00::/7 */
480 return setError(E_INVALIDARG,
481 tr("IPv6 prefix %RTnaipv6 is not unicast"),
482 &Net6);
483
484 /* Verify the interfaces ID part is zero */
485 if (Net6.au64[1] != 0)
486 return setError(E_INVALIDARG,
487 tr("Non-zero bits in the interface ID part"
488 " of the IPv6 prefix %RTnaipv6/64"),
489 &Net6);
490
491 rc = strNormalizedIPv6Prefix.printfNoThrow("%RTnaipv6/64", &Net6);
492 if (RT_FAILURE(rc))
493 {
494 if (rc == VERR_NO_MEMORY)
495 return setError(E_OUTOFMEMORY);
496 else
497 return setError(E_FAIL, tr("Internal error"));
498 }
499 }
500
501 {
502 AutoReadLock alockNatNetList(m->pVirtualBox->i_getNatNetLock() COMMA_LOCKVAL_SRC_POS);
503 if (m->pVirtualBox->i_isNatNetStarted(m->s.strNetworkName))
504 return setErrorBusy();
505
506 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
507
508 if (strNormalizedIPv6Prefix == m->s.strIPv6Prefix)
509 return S_OK;
510
511 /* only allow prefix to be empty if IPv6 is disabled */
512 if (strNormalizedIPv6Prefix.isEmpty() && m->s.fIPv6Enabled)
513 return setError(E_FAIL, tr("Setting an empty IPv6 prefix when IPv6 is enabled"));
514
515 /**
516 * @todo
517 * silently ignore network IPv6 prefix update.
518 * todo: see similar todo in NATNetwork::COMSETTER(Network)(IN_BSTR)
519 */
520 if (!m->s.mapPortForwardRules6.empty())
521 return S_OK;
522
523 m->s.strIPv6Prefix = strNormalizedIPv6Prefix;
524 }
525
526 AutoWriteLock vboxLock(m->pVirtualBox COMMA_LOCKVAL_SRC_POS);
527 hrc = m->pVirtualBox->i_saveSettings();
528 ComAssertComRCRetRC(hrc);
529
530 return S_OK;
531}
532
533
534HRESULT NATNetwork::getAdvertiseDefaultIPv6RouteEnabled(BOOL *aAdvertiseDefaultIPv6Route)
535{
536 *aAdvertiseDefaultIPv6Route = m->s.fAdvertiseDefaultIPv6Route;
537
538 return S_OK;
539}
540
541
542HRESULT NATNetwork::setAdvertiseDefaultIPv6RouteEnabled(const BOOL aAdvertiseDefaultIPv6Route)
543{
544 {
545 AutoReadLock alockNatNetList(m->pVirtualBox->i_getNatNetLock() COMMA_LOCKVAL_SRC_POS);
546 if (m->pVirtualBox->i_isNatNetStarted(m->s.strNetworkName))
547 return setErrorBusy();
548
549 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
550
551 if (RT_BOOL(aAdvertiseDefaultIPv6Route) == m->s.fAdvertiseDefaultIPv6Route)
552 return S_OK;
553
554 m->s.fAdvertiseDefaultIPv6Route = RT_BOOL(aAdvertiseDefaultIPv6Route);
555
556 }
557
558 AutoWriteLock vboxLock(m->pVirtualBox COMMA_LOCKVAL_SRC_POS);
559 HRESULT rc = m->pVirtualBox->i_saveSettings();
560 ComAssertComRCRetRC(rc);
561
562 return S_OK;
563}
564
565
566HRESULT NATNetwork::getNeedDhcpServer(BOOL *aNeedDhcpServer)
567{
568 *aNeedDhcpServer = m->s.fNeedDhcpServer;
569
570 return S_OK;
571}
572
573HRESULT NATNetwork::setNeedDhcpServer(const BOOL aNeedDhcpServer)
574{
575 {
576 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
577
578 if (RT_BOOL(aNeedDhcpServer) == m->s.fNeedDhcpServer)
579 return S_OK;
580
581 m->s.fNeedDhcpServer = RT_BOOL(aNeedDhcpServer);
582
583 i_recalculateIpv4AddressAssignments();
584
585 }
586
587 AutoWriteLock vboxLock(m->pVirtualBox COMMA_LOCKVAL_SRC_POS);
588 HRESULT rc = m->pVirtualBox->i_saveSettings();
589 ComAssertComRCRetRC(rc);
590
591 return S_OK;
592}
593
594HRESULT NATNetwork::getLocalMappings(std::vector<com::Utf8Str> &aLocalMappings)
595{
596 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
597
598 aLocalMappings.resize(m->s.llHostLoopbackOffsetList.size());
599 size_t i = 0;
600 for (settings::NATLoopbackOffsetList::const_iterator it = m->s.llHostLoopbackOffsetList.begin();
601 it != m->s.llHostLoopbackOffsetList.end(); ++it, ++i)
602 {
603 aLocalMappings[i] = Utf8StrFmt("%s=%d",
604 (*it).strLoopbackHostAddress.c_str(),
605 (*it).u32Offset);
606 }
607
608 return S_OK;
609}
610
611HRESULT NATNetwork::addLocalMapping(const com::Utf8Str &aHostId, LONG aOffset)
612{
613 RTNETADDRIPV4 addr, net, mask;
614
615 int rc = RTNetStrToIPv4Addr(Utf8Str(aHostId).c_str(), &addr);
616 if (RT_FAILURE(rc))
617 return E_INVALIDARG;
618
619 /* check against 127/8 */
620 if ((RT_N2H_U32(addr.u) >> IN_CLASSA_NSHIFT) != IN_LOOPBACKNET)
621 return E_INVALIDARG;
622
623 /* check against networkid vs network mask */
624 rc = RTCidrStrToIPv4(Utf8Str(m->s.strIPv4NetworkCidr).c_str(), &net, &mask);
625 if (RT_FAILURE(rc))
626 return E_INVALIDARG;
627
628 if (((net.u + (uint32_t)aOffset) & mask.u) != net.u)
629 return E_INVALIDARG;
630
631 settings::NATLoopbackOffsetList::iterator it;
632
633 it = std::find(m->s.llHostLoopbackOffsetList.begin(),
634 m->s.llHostLoopbackOffsetList.end(),
635 aHostId);
636 if (it != m->s.llHostLoopbackOffsetList.end())
637 {
638 if (aOffset == 0) /* erase */
639 m->s.llHostLoopbackOffsetList.erase(it, it);
640 else /* modify */
641 {
642 settings::NATLoopbackOffsetList::iterator it1;
643 it1 = std::find(m->s.llHostLoopbackOffsetList.begin(),
644 m->s.llHostLoopbackOffsetList.end(),
645 (uint32_t)aOffset);
646 if (it1 != m->s.llHostLoopbackOffsetList.end())
647 return E_INVALIDARG; /* this offset is already registered. */
648
649 (*it).u32Offset = (uint32_t)aOffset;
650 }
651
652 AutoWriteLock vboxLock(m->pVirtualBox COMMA_LOCKVAL_SRC_POS);
653 return m->pVirtualBox->i_saveSettings();
654 }
655
656 /* injection */
657 it = std::find(m->s.llHostLoopbackOffsetList.begin(),
658 m->s.llHostLoopbackOffsetList.end(),
659 (uint32_t)aOffset);
660
661 if (it != m->s.llHostLoopbackOffsetList.end())
662 return E_INVALIDARG; /* offset is already registered. */
663
664 settings::NATHostLoopbackOffset off;
665 off.strLoopbackHostAddress = aHostId;
666 off.u32Offset = (uint32_t)aOffset;
667 m->s.llHostLoopbackOffsetList.push_back(off);
668
669 AutoWriteLock vboxLock(m->pVirtualBox COMMA_LOCKVAL_SRC_POS);
670 return m->pVirtualBox->i_saveSettings();
671}
672
673
674HRESULT NATNetwork::getLoopbackIp6(LONG *aLoopbackIp6)
675{
676 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
677
678 *aLoopbackIp6 = (LONG)m->s.u32HostLoopback6Offset;
679 return S_OK;
680}
681
682
683HRESULT NATNetwork::setLoopbackIp6(LONG aLoopbackIp6)
684{
685 {
686 AutoReadLock alockNatNetList(m->pVirtualBox->i_getNatNetLock() COMMA_LOCKVAL_SRC_POS);
687 if (m->pVirtualBox->i_isNatNetStarted(m->s.strNetworkName))
688 return setErrorBusy();
689
690 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
691
692 if (aLoopbackIp6 < 0)
693 return E_INVALIDARG;
694
695 if (static_cast<uint32_t>(aLoopbackIp6) == m->s.u32HostLoopback6Offset)
696 return S_OK;
697
698 m->s.u32HostLoopback6Offset = (uint32_t)aLoopbackIp6;
699 }
700
701 AutoWriteLock vboxLock(m->pVirtualBox COMMA_LOCKVAL_SRC_POS);
702 return m->pVirtualBox->i_saveSettings();
703}
704
705
706HRESULT NATNetwork::getPortForwardRules4(std::vector<com::Utf8Str> &aPortForwardRules4)
707{
708 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
709 i_getPortForwardRulesFromMap(aPortForwardRules4,
710 m->s.mapPortForwardRules4);
711 return S_OK;
712}
713
714HRESULT NATNetwork::getPortForwardRules6(std::vector<com::Utf8Str> &aPortForwardRules6)
715{
716 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
717 i_getPortForwardRulesFromMap(aPortForwardRules6,
718 m->s.mapPortForwardRules6);
719 return S_OK;
720}
721
722HRESULT NATNetwork::addPortForwardRule(BOOL aIsIpv6,
723 const com::Utf8Str &aPortForwardRuleName,
724 NATProtocol_T aProto,
725 const com::Utf8Str &aHostIp,
726 USHORT aHostPort,
727 const com::Utf8Str &aGuestIp,
728 USHORT aGuestPort)
729{
730 {
731 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
732 Utf8Str name = aPortForwardRuleName;
733 Utf8Str proto;
734 settings::NATRule r;
735 settings::NATRulesMap &mapRules = aIsIpv6 ? m->s.mapPortForwardRules6 : m->s.mapPortForwardRules4;
736 switch (aProto)
737 {
738 case NATProtocol_TCP:
739 proto = "tcp";
740 break;
741 case NATProtocol_UDP:
742 proto = "udp";
743 break;
744 default:
745 return E_INVALIDARG;
746 }
747 if (name.isEmpty())
748 name = Utf8StrFmt("%s_[%s]%%%d_[%s]%%%d", proto.c_str(),
749 aHostIp.c_str(), aHostPort,
750 aGuestIp.c_str(), aGuestPort);
751
752 for (settings::NATRulesMap::iterator it = mapRules.begin(); it != mapRules.end(); ++it)
753 {
754 r = it->second;
755 if (it->first == name)
756 return setError(E_INVALIDARG,
757 tr("A NAT rule of this name already exists"));
758 if ( r.strHostIP == aHostIp
759 && r.u16HostPort == aHostPort
760 && r.proto == aProto)
761 return setError(E_INVALIDARG,
762 tr("A NAT rule for this host port and this host IP already exists"));
763 }
764
765 r.strName = name.c_str();
766 r.proto = aProto;
767 r.strHostIP = aHostIp;
768 r.u16HostPort = aHostPort;
769 r.strGuestIP = aGuestIp;
770 r.u16GuestPort = aGuestPort;
771 mapRules.insert(std::make_pair(name, r));
772 }
773 {
774 AutoWriteLock vboxLock(m->pVirtualBox COMMA_LOCKVAL_SRC_POS);
775 HRESULT rc = m->pVirtualBox->i_saveSettings();
776 ComAssertComRCRetRC(rc);
777 }
778
779 m->pVirtualBox->i_onNATNetworkPortForward(m->s.strNetworkName, TRUE, aIsIpv6,
780 aPortForwardRuleName, aProto,
781 aHostIp, aHostPort,
782 aGuestIp, aGuestPort);
783
784 /* Notify listeners listening on this network only */
785 ::FireNATNetworkPortForwardEvent(m->pEventSource, m->s.strNetworkName, TRUE,
786 aIsIpv6, aPortForwardRuleName, aProto,
787 aHostIp, aHostPort,
788 aGuestIp, aGuestPort);
789
790 return S_OK;
791}
792
793HRESULT NATNetwork::removePortForwardRule(BOOL aIsIpv6, const com::Utf8Str &aPortForwardRuleName)
794{
795 Utf8Str strHostIP;
796 Utf8Str strGuestIP;
797 uint16_t u16HostPort;
798 uint16_t u16GuestPort;
799 NATProtocol_T proto;
800
801 {
802 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
803 settings::NATRulesMap &mapRules = aIsIpv6 ? m->s.mapPortForwardRules6 : m->s.mapPortForwardRules4;
804 settings::NATRulesMap::iterator it = mapRules.find(aPortForwardRuleName);
805
806 if (it == mapRules.end())
807 return E_INVALIDARG;
808
809 strHostIP = it->second.strHostIP;
810 strGuestIP = it->second.strGuestIP;
811 u16HostPort = it->second.u16HostPort;
812 u16GuestPort = it->second.u16GuestPort;
813 proto = it->second.proto;
814
815 mapRules.erase(it);
816 }
817
818 {
819 AutoWriteLock vboxLock(m->pVirtualBox COMMA_LOCKVAL_SRC_POS);
820 HRESULT rc = m->pVirtualBox->i_saveSettings();
821 ComAssertComRCRetRC(rc);
822 }
823
824 m->pVirtualBox->i_onNATNetworkPortForward(m->s.strNetworkName, FALSE, aIsIpv6, aPortForwardRuleName, proto,
825 strHostIP, u16HostPort, strGuestIP, u16GuestPort);
826
827 /* Notify listeners listening on this network only */
828 ::FireNATNetworkPortForwardEvent(m->pEventSource, m->s.strNetworkName, FALSE, aIsIpv6, aPortForwardRuleName, proto,
829 strHostIP, u16HostPort, strGuestIP, u16GuestPort);
830 return S_OK;
831}
832
833
834void NATNetwork::i_updateDomainNameOption(ComPtr<IHost> &host)
835{
836 com::Bstr domain;
837 if (FAILED(host->COMGETTER(DomainName)(domain.asOutParam())))
838 LogRel(("NATNetwork: Failed to get host's domain name\n"));
839 ComPtr<IDHCPGlobalConfig> pDHCPConfig;
840 HRESULT hrc = m->dhcpServer->COMGETTER(GlobalConfig)(pDHCPConfig.asOutParam());
841 if (FAILED(hrc))
842 {
843 LogRel(("NATNetwork: Failed to get global DHCP config when updating domain name option with %Rhrc\n", hrc));
844 return;
845 }
846 if (domain.isNotEmpty())
847 {
848 hrc = pDHCPConfig->SetOption(DHCPOption_DomainName, DHCPOptionEncoding_Normal, domain.raw());
849 if (FAILED(hrc))
850 LogRel(("NATNetwork: Failed to add domain name option with %Rhrc\n", hrc));
851 }
852 else
853 pDHCPConfig->RemoveOption(DHCPOption_DomainName);
854}
855
856void NATNetwork::i_updateDomainNameServerOption(ComPtr<IHost> &host)
857{
858 RTNETADDRIPV4 networkid, netmask;
859
860 int rc = RTCidrStrToIPv4(m->s.strIPv4NetworkCidr.c_str(), &networkid, &netmask);
861 if (RT_FAILURE(rc))
862 {
863 LogRel(("NATNetwork: Failed to parse cidr %s with %Rrc\n", m->s.strIPv4NetworkCidr.c_str(), rc));
864 return;
865 }
866
867 /* XXX: these are returned, surprisingly, in host order */
868 networkid.u = RT_H2N_U32(networkid.u);
869 netmask.u = RT_H2N_U32(netmask.u);
870
871 com::SafeArray<BSTR> nameServers;
872 HRESULT hrc = host->COMGETTER(NameServers)(ComSafeArrayAsOutParam(nameServers));
873 if (FAILED(hrc))
874 {
875 LogRel(("NATNetwork: Failed to get name servers from host with %Rhrc\n", hrc));
876 return;
877 }
878 ComPtr<IDHCPGlobalConfig> pDHCPConfig;
879 hrc = m->dhcpServer->COMGETTER(GlobalConfig)(pDHCPConfig.asOutParam());
880 if (FAILED(hrc))
881 {
882 LogRel(("NATNetwork: Failed to get global DHCP config when updating domain name server option with %Rhrc\n", hrc));
883 return;
884 }
885
886 size_t cAddresses = nameServers.size();
887 if (cAddresses)
888 {
889 RTCList<RTCString> lstServers;
890 /* The following code was copied (and adapted a bit) from VBoxNetDhcp::hostDnsServers */
891 /*
892 * Recent fashion is to run dnsmasq on 127.0.1.1 which we
893 * currently can't map. If that's the only nameserver we've got,
894 * we need to use DNS proxy for VMs to reach it.
895 */
896 bool fUnmappedLoopback = false;
897
898 for (size_t i = 0; i < cAddresses; ++i)
899 {
900 RTNETADDRIPV4 addr;
901
902 com::Utf8Str strNameServerAddress(nameServers[i]);
903 rc = RTNetStrToIPv4Addr(strNameServerAddress.c_str(), &addr);
904 if (RT_FAILURE(rc))
905 {
906 LogRel(("NATNetwork: Failed to parse IP address %s with %Rrc\n", strNameServerAddress.c_str(), rc));
907 continue;
908 }
909
910 if (addr.u == INADDR_ANY)
911 {
912 /*
913 * This doesn't seem to be very well documented except for
914 * RTFS of res_init.c, but INADDR_ANY is a valid value for
915 * for "nameserver".
916 */
917 addr.u = RT_H2N_U32_C(INADDR_LOOPBACK);
918 }
919
920 if (addr.au8[0] == 127)
921 {
922 settings::NATLoopbackOffsetList::const_iterator it;
923
924 it = std::find(m->s.llHostLoopbackOffsetList.begin(),
925 m->s.llHostLoopbackOffsetList.end(),
926 strNameServerAddress);
927 if (it == m->s.llHostLoopbackOffsetList.end())
928 {
929 fUnmappedLoopback = true;
930 continue;
931 }
932 addr.u = RT_H2N_U32(RT_N2H_U32(networkid.u) + it->u32Offset);
933 }
934 lstServers.append(RTCStringFmt("%RTnaipv4", addr));
935 }
936
937 if (lstServers.isEmpty() && fUnmappedLoopback)
938 lstServers.append(RTCStringFmt("%RTnaipv4", networkid.u | RT_H2N_U32_C(1U))); /* proxy */
939
940 hrc = pDHCPConfig->SetOption(DHCPOption_DomainNameServers, DHCPOptionEncoding_Normal, Bstr(RTCString::join(lstServers, " ")).raw());
941 if (FAILED(hrc))
942 LogRel(("NATNetwork: Failed to add domain name server option '%s' with %Rhrc\n", RTCString::join(lstServers, " ").c_str(), hrc));
943 }
944 else
945 pDHCPConfig->RemoveOption(DHCPOption_DomainNameServers);
946}
947
948void NATNetwork::i_updateDnsOptions()
949{
950 ComPtr<IHost> host;
951 if (SUCCEEDED(m->pVirtualBox->COMGETTER(Host)(host.asOutParam())))
952 {
953 i_updateDomainNameOption(host);
954 i_updateDomainNameServerOption(host);
955 }
956}
957
958
959HRESULT NATNetwork::start()
960{
961#ifdef VBOX_WITH_NAT_SERVICE
962 if (!m->s.fEnabled) return S_OK;
963 AssertReturn(!m->s.strNetworkName.isEmpty(), E_FAIL);
964
965 m->NATRunner.resetArguments();
966 m->NATRunner.addArgPair(NetworkServiceRunner::kpszKeyNetwork, Utf8Str(m->s.strNetworkName).c_str());
967
968 /* No portforwarding rules from command-line, all will be fetched via API */
969
970 if (m->s.fNeedDhcpServer)
971 {
972 /*
973 * Just to as idea... via API (on creation user pass the cidr of network and)
974 * and we calculate it's addreses (mutable?).
975 */
976
977 /*
978 * Configuration and running DHCP server:
979 * 1. find server first createDHCPServer
980 * 2. if return status is E_INVALARG => server already exists just find and start.
981 * 3. if return status neither E_INVALRG nor S_OK => return E_FAIL
982 * 4. if return status S_OK proceed to DHCP server configuration
983 * 5. call setConfiguration() and pass all required parameters
984 * 6. start dhcp server.
985 */
986 HRESULT hrc = m->pVirtualBox->FindDHCPServerByNetworkName(Bstr(m->s.strNetworkName).raw(),
987 m->dhcpServer.asOutParam());
988 switch (hrc)
989 {
990 case E_INVALIDARG:
991 /* server haven't beeen found let create it then */
992 hrc = m->pVirtualBox->CreateDHCPServer(Bstr(m->s.strNetworkName).raw(),
993 m->dhcpServer.asOutParam());
994 if (FAILED(hrc))
995 return E_FAIL;
996 /* breakthrough */
997
998 {
999 LogFunc(("gateway: %s, dhcpserver:%s, dhcplowerip:%s, dhcpupperip:%s\n",
1000 m->IPv4Gateway.c_str(),
1001 m->IPv4DhcpServer.c_str(),
1002 m->IPv4DhcpServerLowerIp.c_str(),
1003 m->IPv4DhcpServerUpperIp.c_str()));
1004
1005 hrc = m->dhcpServer->COMSETTER(Enabled)(true);
1006
1007 hrc = m->dhcpServer->SetConfiguration(Bstr(m->IPv4DhcpServer).raw(),
1008 Bstr(m->IPv4NetworkMask).raw(),
1009 Bstr(m->IPv4DhcpServerLowerIp).raw(),
1010 Bstr(m->IPv4DhcpServerUpperIp).raw());
1011 }
1012 case S_OK:
1013 break;
1014
1015 default:
1016 return E_FAIL;
1017 }
1018
1019#ifdef VBOX_WITH_DHCPD
1020 i_updateDnsOptions();
1021#endif /* VBOX_WITH_DHCPD */
1022 /* XXX: AddGlobalOption(DhcpOpt_Router,) - enables attachement of DhcpServer to Main (no longer true with VBoxNetDhcpd). */
1023 ComPtr<IDHCPGlobalConfig> pDHCPConfig;
1024 hrc = m->dhcpServer->COMGETTER(GlobalConfig)(pDHCPConfig.asOutParam());
1025 if (FAILED(hrc))
1026 {
1027 LogRel(("NATNetwork: Failed to get global DHCP config when updating IPv4 gateway option with %Rhrc\n", hrc));
1028 m->dhcpServer.setNull();
1029 return E_FAIL;
1030 }
1031 pDHCPConfig->SetOption(DHCPOption_Routers, DHCPOptionEncoding_Normal, Bstr(m->IPv4Gateway).raw());
1032
1033 hrc = m->dhcpServer->Start(Bstr::Empty.raw(), Bstr(TRUNKTYPE_WHATEVER).raw());
1034 if (FAILED(hrc))
1035 {
1036 m->dhcpServer.setNull();
1037 return E_FAIL;
1038 }
1039 }
1040
1041 if (RT_SUCCESS(m->NATRunner.start(false /* KillProcOnStop */)))
1042 {
1043 m->pVirtualBox->i_onNATNetworkStartStop(m->s.strNetworkName, TRUE);
1044 return S_OK;
1045 }
1046 /** @todo missing setError()! */
1047 return E_FAIL;
1048#else
1049 ReturnComNotImplemented();
1050#endif
1051}
1052
1053HRESULT NATNetwork::stop()
1054{
1055#ifdef VBOX_WITH_NAT_SERVICE
1056 m->pVirtualBox->i_onNATNetworkStartStop(m->s.strNetworkName, FALSE);
1057
1058 if (!m->dhcpServer.isNull())
1059 m->dhcpServer->Stop();
1060
1061 if (RT_SUCCESS(m->NATRunner.stop()))
1062 return S_OK;
1063
1064 /** @todo missing setError()! */
1065 return E_FAIL;
1066#else
1067 ReturnComNotImplemented();
1068#endif
1069}
1070
1071
1072void NATNetwork::i_getPortForwardRulesFromMap(std::vector<com::Utf8Str> &aPortForwardRules, settings::NATRulesMap &aRules)
1073{
1074 aPortForwardRules.resize(aRules.size());
1075 size_t i = 0;
1076 for (settings::NATRulesMap::const_iterator it = aRules.begin();
1077 it != aRules.end(); ++it, ++i)
1078 {
1079 settings::NATRule r = it->second;
1080 aPortForwardRules[i] = Utf8StrFmt("%s:%s:[%s]:%d:[%s]:%d",
1081 r.strName.c_str(),
1082 (r.proto == NATProtocol_TCP ? "tcp" : "udp"),
1083 r.strHostIP.c_str(),
1084 r.u16HostPort,
1085 r.strGuestIP.c_str(),
1086 r.u16GuestPort);
1087 }
1088}
1089
1090
1091int NATNetwork::i_findFirstAvailableOffset(ADDRESSLOOKUPTYPE addrType, uint32_t *poff)
1092{
1093 RTNETADDRIPV4 network, netmask;
1094
1095 int rc = RTCidrStrToIPv4(m->s.strIPv4NetworkCidr.c_str(),
1096 &network,
1097 &netmask);
1098 AssertRCReturn(rc, rc);
1099
1100 uint32_t off;
1101 for (off = 1; off < ~netmask.u; ++off)
1102 {
1103 bool skip = false;
1104 for (settings::NATLoopbackOffsetList::iterator it = m->s.llHostLoopbackOffsetList.begin();
1105 it != m->s.llHostLoopbackOffsetList.end();
1106 ++it)
1107 {
1108 if ((*it).u32Offset == off)
1109 {
1110 skip = true;
1111 break;
1112 }
1113
1114 }
1115
1116 if (skip)
1117 continue;
1118
1119 if (off == m->offGateway)
1120 {
1121 if (addrType == ADDR_GATEWAY)
1122 break;
1123 else
1124 continue;
1125 }
1126
1127 if (off == m->offDhcp)
1128 {
1129 if (addrType == ADDR_DHCP)
1130 break;
1131 else
1132 continue;
1133 }
1134
1135 if (!skip)
1136 break;
1137 }
1138
1139 if (poff)
1140 *poff = off;
1141
1142 return VINF_SUCCESS;
1143}
1144
1145int NATNetwork::i_recalculateIpv4AddressAssignments()
1146{
1147 RTNETADDRIPV4 network, netmask;
1148 int rc = RTCidrStrToIPv4(m->s.strIPv4NetworkCidr.c_str(),
1149 &network,
1150 &netmask);
1151 AssertRCReturn(rc, rc);
1152
1153 i_findFirstAvailableOffset(ADDR_GATEWAY, &m->offGateway);
1154 if (m->s.fNeedDhcpServer)
1155 i_findFirstAvailableOffset(ADDR_DHCP, &m->offDhcp);
1156
1157 /* I don't remember the reason CIDR calculated on the host. */
1158 RTNETADDRIPV4 gateway = network;
1159 gateway.u += m->offGateway;
1160 gateway.u = RT_H2N_U32(gateway.u);
1161 char szTmpIp[16];
1162 RTStrPrintf(szTmpIp, sizeof(szTmpIp), "%RTnaipv4", gateway);
1163 m->IPv4Gateway = szTmpIp;
1164
1165 if (m->s.fNeedDhcpServer)
1166 {
1167 RTNETADDRIPV4 dhcpserver = network;
1168 dhcpserver.u += m->offDhcp;
1169
1170 /* XXX: adding more services should change the math here */
1171 RTNETADDRIPV4 dhcplowerip = network;
1172 uint32_t offDhcpLowerIp;
1173 i_findFirstAvailableOffset(ADDR_DHCPLOWERIP, &offDhcpLowerIp);
1174 dhcplowerip.u = RT_H2N_U32(dhcplowerip.u + offDhcpLowerIp);
1175
1176 RTNETADDRIPV4 dhcpupperip;
1177 dhcpupperip.u = RT_H2N_U32((network.u | ~netmask.u) - 1);
1178
1179 dhcpserver.u = RT_H2N_U32(dhcpserver.u);
1180 network.u = RT_H2N_U32(network.u);
1181
1182 RTStrPrintf(szTmpIp, sizeof(szTmpIp), "%RTnaipv4", dhcpserver);
1183 m->IPv4DhcpServer = szTmpIp;
1184 RTStrPrintf(szTmpIp, sizeof(szTmpIp), "%RTnaipv4", dhcplowerip);
1185 m->IPv4DhcpServerLowerIp = szTmpIp;
1186 RTStrPrintf(szTmpIp, sizeof(szTmpIp), "%RTnaipv4", dhcpupperip);
1187 m->IPv4DhcpServerUpperIp = szTmpIp;
1188
1189 LogFunc(("network:%RTnaipv4, dhcpserver:%RTnaipv4, dhcplowerip:%RTnaipv4, dhcpupperip:%RTnaipv4\n",
1190 network, dhcpserver, dhcplowerip, dhcpupperip));
1191 }
1192
1193 /* we need IPv4NetworkMask for NAT's gw service start */
1194 netmask.u = RT_H2N_U32(netmask.u);
1195 RTStrPrintf(szTmpIp, 16, "%RTnaipv4", netmask);
1196 m->IPv4NetworkMask = szTmpIp;
1197
1198 LogFlowFunc(("getaway:%RTnaipv4, netmask:%RTnaipv4\n", gateway, netmask));
1199 return VINF_SUCCESS;
1200}
1201
1202
1203int NATNetwork::i_recalculateIPv6Prefix()
1204{
1205 int rc;
1206
1207 RTNETADDRIPV4 net, mask;
1208 rc = RTCidrStrToIPv4(Utf8Str(m->s.strIPv4NetworkCidr).c_str(), &net, &mask);
1209 if (RT_FAILURE(rc))
1210 return rc;
1211
1212 net.u = RT_H2N_U32(net.u); /* XXX: fix RTCidrStrToIPv4! */
1213
1214 /*
1215 * [fd17:625c:f037:XXXX::/64] - RFC 4193 (ULA) Locally Assigned
1216 * Global ID where XXXX, 16 bit Subnet ID, are two bytes from the
1217 * middle of the IPv4 address, e.g. :dead: for 10.222.173.1
1218 */
1219 RTNETADDRIPV6 prefix;
1220 RT_ZERO(prefix);
1221
1222 prefix.au8[0] = 0xFD;
1223 prefix.au8[1] = 0x17;
1224
1225 prefix.au8[2] = 0x62;
1226 prefix.au8[3] = 0x5C;
1227
1228 prefix.au8[4] = 0xF0;
1229 prefix.au8[5] = 0x37;
1230
1231 prefix.au8[6] = net.au8[1];
1232 prefix.au8[7] = net.au8[2];
1233
1234 char szBuf[32];
1235 RTStrPrintf(szBuf, sizeof(szBuf), "%RTnaipv6/64", &prefix);
1236
1237 m->s.strIPv6Prefix = szBuf;
1238 return VINF_SUCCESS;
1239}
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