From 1ecaaa60ffaddaf166682d41ad447e0d7f6d212d Mon Sep 17 00:00:00 2001 From: core Date: Tue, 5 Dec 2023 10:24:19 -0500 Subject: [PATCH] Add DNS resolver support --- .../defined/mobile_nebula/NebulaVpnService.kt | 5 + .../kotlin/net/defined/mobile_nebula/Sites.kt | 3 + .../PacketTunnelProvider.swift | 5 + ios/NebulaNetworkExtension/Site.swift | 4 + lib/models/Site.dart | 14 +++ lib/screens/siteConfig/AdvancedScreen.dart | 25 ++++- lib/screens/siteConfig/DNSResolverScreen.dart | 77 +++++++++++++ .../siteConfig/DNSResolversScreen.dart | 103 ++++++++++++++++++ lib/screens/siteConfig/SiteConfigScreen.dart | 1 + nebula/config.go | 1 + nebula/mobileNebula.go | 7 ++ 11 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 lib/screens/siteConfig/DNSResolverScreen.dart create mode 100644 lib/screens/siteConfig/DNSResolversScreen.dart diff --git a/android/app/src/main/kotlin/net/defined/mobile_nebula/NebulaVpnService.kt b/android/app/src/main/kotlin/net/defined/mobile_nebula/NebulaVpnService.kt index af7ba60..f9ae582 100644 --- a/android/app/src/main/kotlin/net/defined/mobile_nebula/NebulaVpnService.kt +++ b/android/app/src/main/kotlin/net/defined/mobile_nebula/NebulaVpnService.kt @@ -131,6 +131,11 @@ class NebulaVpnService : VpnService() { builder.addRoute(unsafeIPNet.network, unsafeIPNet.maskSize.toInt()) } + // Add our DNS resolvers + site!!.dnsResolvers.forEach { dnsResolver -> + builder.addDnsServer(dnsResolver) + } + try { vpnInterface = builder.establish() nebula = mobileNebula.MobileNebula.newNebula(site!!.config, site!!.getKey(this), site!!.logFile, vpnInterface!!.detachFd().toLong()) diff --git a/android/app/src/main/kotlin/net/defined/mobile_nebula/Sites.kt b/android/app/src/main/kotlin/net/defined/mobile_nebula/Sites.kt index c86d617..e009625 100644 --- a/android/app/src/main/kotlin/net/defined/mobile_nebula/Sites.kt +++ b/android/app/src/main/kotlin/net/defined/mobile_nebula/Sites.kt @@ -201,6 +201,7 @@ class Site(context: Context, siteDir: File) { val id: String val staticHostmap: HashMap val unsafeRoutes: List + val dnsResolvers: List var cert: CertificateInfo? = null var ca: Array val lhDuration: Int @@ -236,6 +237,7 @@ class Site(context: Context, siteDir: File) { id = incomingSite.id staticHostmap = incomingSite.staticHostmap unsafeRoutes = incomingSite.unsafeRoutes ?: ArrayList() + dnsResolvers = incomingSite.dnsResolvers ?: ArrayList() lhDuration = incomingSite.lhDuration port = incomingSite.port mtu = incomingSite.mtu ?: 1300 @@ -340,6 +342,7 @@ class IncomingSite( val id: String, val staticHostmap: HashMap, val unsafeRoutes: List?, + val dnsResolvers: List?, val cert: String, val ca: String, val lhDuration: Int, diff --git a/ios/NebulaNetworkExtension/PacketTunnelProvider.swift b/ios/NebulaNetworkExtension/PacketTunnelProvider.swift index 61e131a..4588d91 100644 --- a/ios/NebulaNetworkExtension/PacketTunnelProvider.swift +++ b/ios/NebulaNetworkExtension/PacketTunnelProvider.swift @@ -78,6 +78,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider { tunnelNetworkSettings.ipv4Settings!.includedRoutes = routes tunnelNetworkSettings.mtu = _site.mtu as NSNumber + + if !_site.dnsResolvers.isEmpty { + let dnsSettings = NEDNSSettings(servers: _site.dnsResolvers) + tunnelNetworkSettings.dnsSettings = dnsSettings + } self.setTunnelNetworkSettings(tunnelNetworkSettings, completionHandler: {(error:Error?) in if (error != nil) { diff --git a/ios/NebulaNetworkExtension/Site.swift b/ios/NebulaNetworkExtension/Site.swift index 5708850..6df2f96 100644 --- a/ios/NebulaNetworkExtension/Site.swift +++ b/ios/NebulaNetworkExtension/Site.swift @@ -121,6 +121,7 @@ class Site: Codable { // Stored in proto var staticHostmap: Dictionary var unsafeRoutes: [UnsafeRoute] + var dnsResolvers: [String] var cert: CertificateInfo? var ca: [CertificateInfo] var lhDuration: Int @@ -194,6 +195,7 @@ class Site: Codable { id = incoming.id staticHostmap = incoming.staticHostmap unsafeRoutes = incoming.unsafeRoutes ?? [] + dnsResolvers = incoming.dnsResolvers ?? [] lhDuration = incoming.lhDuration port = incoming.port cipher = incoming.cipher @@ -340,6 +342,7 @@ class Site: Codable { case status case logFile case unsafeRoutes + case dnsResolvers case logVerbosity case errors case mtu @@ -394,6 +397,7 @@ struct IncomingSite: Codable { var id: String var staticHostmap: Dictionary var unsafeRoutes: [UnsafeRoute]? + vat dnsResolvers: [String]? var cert: String var ca: String var lhDuration: Int diff --git a/lib/models/Site.dart b/lib/models/Site.dart index daecef3..994a03d 100644 --- a/lib/models/Site.dart +++ b/lib/models/Site.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'package:flutter/services.dart'; import 'package:mobile_nebula/models/HostInfo.dart'; import 'package:mobile_nebula/models/UnsafeRoute.dart'; +import 'package:mobile_nebula/models/IPAndPort.dart'; import 'package:uuid/uuid.dart'; import 'Certificate.dart'; import 'StaticHosts.dart'; @@ -24,6 +25,7 @@ class Site { // static_host_map late Map staticHostmap; late List unsafeRoutes; + late List dnsResolvers; // pki fields late List ca; @@ -69,6 +71,7 @@ class Site { String logVerbosity = 'info', List? errors, List? unsafeRoutes, + List? dnsResolvers, bool managed = false, String? rawConfig, DateTime? lastManagedUpdate, @@ -89,6 +92,7 @@ class Site { this.logVerbosity = logVerbosity; this.errors = errors ?? []; this.unsafeRoutes = unsafeRoutes ?? []; + this.dnsResolvers = dnsResolvers ?? []; this.managed = managed; this.rawConfig = rawConfig; this.lastManagedUpdate = lastManagedUpdate; @@ -128,6 +132,7 @@ class Site { logVerbosity: decoded['logVerbosity'], errors: decoded['errors'], unsafeRoutes: decoded['unsafeRoutes'], + dnsResolvers: decoded['dnsResolvers'], managed: decoded['managed'], rawConfig: decoded['rawConfig'], lastManagedUpdate: decoded['lastManagedUpdate'], @@ -152,6 +157,7 @@ class Site { this.logVerbosity = decoded['logVerbosity']; this.errors = decoded['errors']; this.unsafeRoutes = decoded['unsafeRoutes']; + this.dnsResolvers = decoded['dnsResolvers']; this.managed = decoded['managed']; this.rawConfig = decoded['rawConfig']; this.lastManagedUpdate = decoded['lastManagedUpdate']; @@ -170,6 +176,12 @@ class Site { unsafeRoutes.add(UnsafeRoute.fromJson(val)); }); + List rawDNSResolvers = json['dnsResolvers']; + List dnsResolvers = []; + rawDNSResolvers.forEach((val) { + dnsResolvers.add(val); + }); + List rawCA = json['ca']; List ca = []; rawCA.forEach((val) { @@ -204,6 +216,7 @@ class Site { "logVerbosity": json['logVerbosity'], "errors": errors, "unsafeRoutes": unsafeRoutes, + "dnsResolvers": dnsResolvers, "managed": json['managed'] ?? false, "rawConfig": json['rawConfig'], "lastManagedUpdate": json["lastManagedUpdate"] == null ? @@ -221,6 +234,7 @@ class Site { 'id': id, 'staticHostmap': staticHostmap, 'unsafeRoutes': unsafeRoutes, + 'dnsResolvers': dnsResolvers, 'ca': ca.map((cert) { return cert.rawCert; }).join('\n'), diff --git a/lib/screens/siteConfig/AdvancedScreen.dart b/lib/screens/siteConfig/AdvancedScreen.dart index f76a5ed..461f942 100644 --- a/lib/screens/siteConfig/AdvancedScreen.dart +++ b/lib/screens/siteConfig/AdvancedScreen.dart @@ -10,6 +10,7 @@ import 'package:mobile_nebula/components/config/ConfigSection.dart'; import 'package:mobile_nebula/models/Site.dart'; import 'package:mobile_nebula/models/UnsafeRoute.dart'; import 'package:mobile_nebula/screens/siteConfig/CipherScreen.dart'; +import 'package:mobile_nebula/screens/siteConfig/DNSResolversScreen.dart'; import 'package:mobile_nebula/screens/siteConfig/LogVerbosityScreen.dart'; import 'package:mobile_nebula/screens/siteConfig/RenderedConfigScreen.dart'; import 'package:mobile_nebula/services/utils.dart'; @@ -28,6 +29,7 @@ class Advanced { String verbosity; List unsafeRoutes; int mtu; + List dnsResolvers; Advanced({ required this.lhDuration, @@ -36,6 +38,7 @@ class Advanced { required this.verbosity, required this.unsafeRoutes, required this.mtu, + required this.dnsResolvers, }); } @@ -66,6 +69,7 @@ class _AdvancedScreenState extends State { verbosity: widget.site.logVerbosity, unsafeRoutes: widget.site.unsafeRoutes, mtu: widget.site.mtu, + dnsResolvers: widget.site.dnsResolvers, ); super.initState(); } @@ -192,7 +196,26 @@ class _AdvancedScreenState extends State { }); }); }, - ) + ), + ConfigPageItem( + label: Text('DNS Resolvers'), + labelWidth: 150, + content: Text( + Utils.itemCountFormat(settings.dnsResolvers.length), + textAlign: TextAlign.end), + onPressed: () { + Utils.openPage(context, (context) { + return DNSResolversScreen( + dnsResolvers: settings.dnsResolversmm + onSave: (dnsResolvers) { + setState(() { + settings.dnsResolvers = dnsResolvers; + changed = true; + }); + }); + }); + }, + ), ], ), ConfigSection( diff --git a/lib/screens/siteConfig/DNSResolverScreen.dart b/lib/screens/siteConfig/DNSResolverScreen.dart new file mode 100644 index 0000000..7fd268f --- /dev/null +++ b/lib/screens/siteConfig/DNSResolverScreen.dart @@ -0,0 +1,77 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; +import 'package:mobile_nebula/components/FormPage.dart'; +import 'package:mobile_nebula/components/IPFormField.dart'; +import 'package:mobile_nebula/components/config/ConfigItem.dart'; +import 'package:mobile_nebula/components/config/ConfigSection.dart'; +import 'package:mobile_nebula/services/utils.dart'; + +lass DNSResolverScreen extends StatefulWidget { + const DNSResolverScreen({Key? key, required this.dnsResolver, required this.onDelete, required this.onSave}) : super(key: key); + + final String dnsResolver; + final ValueChanged onSave; + final Function onDelete; + + @override + _DNSResolverScreenState createState() => _DNSResolverScreenState(); +} + +class _DNSResolverScreenState extends State { + late String dnsResolver; + bool changed = false; + + FocusNode dnsResolverFocus = FocusNode(); + + @override + void initState() { + dnsResolver = widget.dnsResolver; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return FormPage( + title: widget.onDelete == null ? 'New DNS Resolver' : 'Edit DNS Resolver', + changed: changed, + onSave: _onSave, + child: Column(children: [ + ConfigSection(children: [ + ConfigItem( + label: Text('Address'), + content: IPFormField( + initialValue: dnsResolver, + ipOnly: true, + textInputAction: TextInputAction.next, + focusNode: dnsResolverFocus, + onSaved: (v) { + dnsResolver = v.toString(); + })), + ]), + widget.onDelete != null + ? Padding( + padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10), + child: SizedBox( + width: double.infinity, + child: PlatformElevatedButton( + child: Text('Delete'), + color: CupertinoColors.systemRed.resolveFrom(context), + onPressed: () => Utils.confirmDelete(context, 'Delete DNS Resolver?', () { + Navigator.of(context).pop(); + widget.onDelete(); + }), + ))) + : Container() + ])); + } + + _onSave() { + Navigator.pop(context); + if (widget.onSave != null) { + widget.onSave(dnsResolver); + } + } +} diff --git a/lib/screens/siteConfig/DNSResolversScreen.dart b/lib/screens/siteConfig/DNSResolversScreen.dart new file mode 100644 index 0000000..869fba8 --- /dev/null +++ b/lib/screens/siteConfig/DNSResolversScreen.dart @@ -0,0 +1,103 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter/foundation.dart'; +import 'package:mobile_nebula/components/FormPage.dart'; +import 'package:mobile_nebula/components/config/ConfigButtonItem.dart'; +import 'package:mobile_nebula/components/config/ConfigPageItem.dart'; +import 'package:mobile_nebula/components/config/ConfigSection.dart'; +import 'package:mobile_nebula/screens/siteConfig/DNSResolverScreen.dart'; +import 'package:mobile_nebula/services/utils.dart'; + +class DNSResolversScreen extends StatefulWidget { + const DNSResolversScreen( + {Key? key, required this.dnsResolvers, required this.onSave}) + : super(key: key); + + final List dnsResolvers; + final ValueChanged> onSave; + + @override + _DNSResolversScreenState createState() => _DNSResolversScreenState(); +} + +class _DNSResolversScreenState extends State { + late List dnsResolvers = []; + bool changed = false; + + @override + void initState() { + widget.dnsResolvers.forEach((dnsResolver) { + dnsResolvers.add(dnsResolver); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return FormPage( + title: 'DNS Resolvers', + changed: changed, + onSave: _onSave, + child: ConfigSection( + children: _build(), + )); + } + + _onSave() { + Navigator.pop(context); + if (widget.onSave != null) { + widget.onSave(dnsResolvers); + } + } + + List _build() { + List items = []; + for (var i=0; i { site.port = settings.port; site.logVerbosity = settings.verbosity; site.unsafeRoutes = settings.unsafeRoutes; + site.dnsResolvers = settings.dnsResolvers; site.mtu = settings.mtu; }); }); diff --git a/nebula/config.go b/nebula/config.go index 878a6c9..e93a6d7 100644 --- a/nebula/config.go +++ b/nebula/config.go @@ -139,6 +139,7 @@ type configTun struct { MTU *int `yaml:"mtu,omitempty"` Routes []configRoute `yaml:"routes"` UnsafeRoutes []configUnsafeRoute `yaml:"unsafe_routes"` + DNSResolvers []string `yaml:"dns_resolvers"` } type configRoute struct { diff --git a/nebula/mobileNebula.go b/nebula/mobileNebula.go index 64e1a42..dd546cd 100644 --- a/nebula/mobileNebula.go +++ b/nebula/mobileNebula.go @@ -116,6 +116,13 @@ func RenderConfig(configData string, key string) (string, error) { } } + if dnsResolvers, ok := d["dnsResolvers"].([]interface{}); ok { + cfg.Tun.DNSResolvers = make([]string, len(dnsResolvers)) + for i, r := range dnsResolvers { + cfg.Tun.DNSResolvers[i] = r.(string) + } + } + finalConfig, err := yaml.Marshal(cfg) if err != nil { return "", err