├── .swiftformatignore
├── ios
├── Gemfile
├── Runner
│ ├── Runner-Bridging-Header.h
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── LaunchImage.imageset
│ │ │ ├── LaunchImage.png
│ │ │ ├── LaunchImage@2x.png
│ │ │ ├── LaunchImage@3x.png
│ │ │ ├── README.md
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ ├── nebula icon white@2x-20.png
│ │ │ ├── nebula icon white@2x-29.png
│ │ │ ├── nebula icon white@2x-40.png
│ │ │ ├── nebula icon white@2x-76.png
│ │ │ ├── nebula icon white@2x-1024.png
│ │ │ ├── nebula icon white@2x-20@2x.png
│ │ │ ├── nebula icon white@2x-20@3x.png
│ │ │ ├── nebula icon white@2x-29@2x.png
│ │ │ ├── nebula icon white@2x-29@3x.png
│ │ │ ├── nebula icon white@2x-40@2x.png
│ │ │ ├── nebula icon white@2x-40@3x.png
│ │ │ ├── nebula icon white@2x-60@2x.png
│ │ │ ├── nebula icon white@2x-60@3x.png
│ │ │ ├── nebula icon white@2x-76@2x.png
│ │ │ ├── nebula icon white@2x-83.5@2x.png
│ │ │ └── Contents.json
│ ├── Runner.entitlements
│ ├── PackageInfo.swift
│ ├── APIClient.swift
│ ├── Base.lproj
│ │ ├── Main.storyboard
│ │ └── LaunchScreen.storyboard
│ ├── Info.plist
│ └── DNUpdate.swift
├── Flutter
│ ├── Debug.xcconfig
│ ├── Release.xcconfig
│ └── AppFrameworkInfo.plist
├── Runner.xcodeproj
│ ├── project.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Runner.xcscheme
├── Runner.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── fastlane
│ ├── Appfile
│ ├── Matchfile
│ └── Fastfile
├── NebulaNetworkExtension
│ ├── CtlInfo.h
│ ├── NebulaNetworkExtension.entitlements
│ ├── Info.plist
│ └── Keychain.swift
├── .gitignore
├── Podfile
└── Podfile.lock
├── android
├── fastlane
│ ├── Appfile
│ ├── README.md
│ └── Fastfile
├── app
│ ├── src
│ │ ├── main
│ │ │ ├── ic_launcher-playstore.png
│ │ │ ├── res
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ ├── ic_launcher_round.png
│ │ │ │ │ ├── ic_launcher_background.png
│ │ │ │ │ └── ic_launcher_foreground.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ ├── ic_launcher_round.png
│ │ │ │ │ ├── ic_launcher_background.png
│ │ │ │ │ └── ic_launcher_foreground.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ ├── ic_launcher_round.png
│ │ │ │ │ ├── ic_launcher_background.png
│ │ │ │ │ └── ic_launcher_foreground.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ ├── ic_launcher_round.png
│ │ │ │ │ ├── ic_launcher_background.png
│ │ │ │ │ └── ic_launcher_foreground.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ ├── ic_launcher_round.png
│ │ │ │ │ ├── ic_launcher_background.png
│ │ │ │ │ └── ic_launcher_foreground.png
│ │ │ │ ├── xml
│ │ │ │ │ └── provider_paths.xml
│ │ │ │ ├── values
│ │ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ │ └── styles.xml
│ │ │ │ ├── mipmap-anydpi-v26
│ │ │ │ │ ├── ic_launcher.xml
│ │ │ │ │ └── ic_launcher_round.xml
│ │ │ │ └── drawable
│ │ │ │ │ └── launch_background.xml
│ │ │ ├── kotlin
│ │ │ │ └── net
│ │ │ │ │ └── defined
│ │ │ │ │ └── mobile_nebula
│ │ │ │ │ ├── MyApplication.kt
│ │ │ │ │ ├── PackageInfo.kt
│ │ │ │ │ ├── APIClient.kt
│ │ │ │ │ ├── EncFile.kt
│ │ │ │ │ └── DNUpdateWorker.kt
│ │ │ └── AndroidManifest.xml
│ │ ├── debug
│ │ │ ├── res
│ │ │ │ └── values
│ │ │ │ │ └── ic_launcher_background.xml
│ │ │ └── AndroidManifest.xml
│ │ └── profile
│ │ │ └── AndroidManifest.xml
│ ├── proguard-rules.pro
│ └── build.gradle
├── gradle.properties
├── .gitignore
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
├── Gemfile
├── build.gradle
├── mobileNebula
│ └── build.gradle
└── settings.gradle
├── fonts
└── RobotoMono-Regular.ttf
├── env.sh.example
├── .git-blame-ignore-revs
├── lib
├── models
│ ├── Hostmap.dart
│ ├── UnsafeRoute.dart
│ ├── CIDR.dart
│ ├── IPAndPort.dart
│ ├── StaticHosts.dart
│ ├── HostInfo.dart
│ └── Certificate.dart
├── validators
│ ├── mtuValidator.dart
│ ├── ipValidator.dart
│ └── dnsValidator.dart
├── components
│ ├── config
│ │ ├── ConfigTextItem.dart
│ │ ├── ConfigButtonItem.dart
│ │ ├── ConfigHeader.dart
│ │ ├── ConfigItem.dart
│ │ ├── ConfigCheckboxItem.dart
│ │ ├── ConfigSection.dart
│ │ └── ConfigPageItem.dart
│ ├── SiteTitle.dart
│ ├── buttons
│ │ └── PrimaryButton.dart
│ ├── DangerButton.dart
│ ├── SiteItem.dart
│ ├── FormPage.dart
│ ├── CIDRField.dart
│ ├── IPAndPortField.dart
│ ├── SimplePage.dart
│ ├── SpecialTextField.dart
│ ├── IPField.dart
│ ├── SpecialButton.dart
│ └── IPFormField.dart
├── services
│ ├── logs.dart
│ ├── result.dart
│ ├── storage.dart
│ ├── share.dart
│ └── settings.dart
└── screens
│ ├── siteConfig
│ ├── RenderedConfigScreen.dart
│ ├── LogVerbosityScreen.dart
│ ├── CipherScreen.dart
│ ├── UnsafeRoutesScreen.dart
│ └── UnsafeRouteScreen.dart
│ ├── LicensesScreen.dart
│ ├── AboutScreen.dart
│ ├── SettingsScreen.dart
│ └── SiteTunnelsScreen.dart
├── .metadata
├── .github
└── workflows
│ ├── gofmt.sh
│ ├── swiftfmt.yml
│ ├── gofmt.yml
│ └── fluttercheck.yml
├── images
├── dn-logo-dark.svg
└── dn-logo-light.svg
├── nebula
├── Makefile
├── mobileNebula_test.go
├── go.mod
├── control.go
└── site.go
├── .gitignore
├── CHANGELOG.md
├── gen-artifacts.sh
├── analysis_options.yaml
├── swift-format.sh
├── README.md
├── pubspec.yaml
└── RELEASE.md
/.swiftformatignore:
--------------------------------------------------------------------------------
1 | ios/Pods/**
--------------------------------------------------------------------------------
/ios/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem "fastlane"
4 |
--------------------------------------------------------------------------------
/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
--------------------------------------------------------------------------------
/android/fastlane/Appfile:
--------------------------------------------------------------------------------
1 | package_name("net.defined.mobile_nebula")
2 | json_key_file(ENV['GOOGLE_PLAY_API_JWT_PATH'])
--------------------------------------------------------------------------------
/fonts/RobotoMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/fonts/RobotoMono-Regular.ttf
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/env.sh.example:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Ensure your go and flutter bin folders are here
4 | export PATH="$PATH:/path/to/go/bin:/path/to/flutter/bin"
--------------------------------------------------------------------------------
/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/android/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/android/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Keep our class names for gson
2 | -keep class net.defined.mobile_nebula.** { *; }
3 | -keep class androidx.security.crypto.** { *; }
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/xml/provider_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4096M
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 | android.nonTransitiveRClass=false
5 | android.nonFinalResIds=false
6 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/android/app/src/debug/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #f2c10d
4 |
5 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-20.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-29.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-40.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-76.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-1024.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-20@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-20@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-29@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-29@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-40@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-40@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-60@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-60@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-76@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DefinedNet/mobile_nebula/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/nebula icon white@2x-83.5@2x.png
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 | /build/build-attribution/
9 | /mobileNebula/mobileNebula.aar
10 |
--------------------------------------------------------------------------------
/.git-blame-ignore-revs:
--------------------------------------------------------------------------------
1 | # Big flutter format run
2 | 9934f226e3e79c3567ce07dbab9e9f6443e7afc5
3 |
4 | # Another big flutter format run
5 | ed348ab126160e64ba09899c946383ca9e54768c
6 |
7 | # Start formatting with swift-format
8 | 4621cbc0006b3c64c8948d920f69b0dc3f503565
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | zipStoreBase=GRADLE_USER_HOME
4 | zipStorePath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
6 |
--------------------------------------------------------------------------------
/lib/models/Hostmap.dart:
--------------------------------------------------------------------------------
1 | import 'IPAndPort.dart';
2 |
3 | class Hostmap {
4 | String nebulaIp;
5 | List destinations;
6 | bool lighthouse;
7 |
8 | Hostmap({required this.nebulaIp, required this.destinations, required this.lighthouse});
9 | }
10 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ios/fastlane/Appfile:
--------------------------------------------------------------------------------
1 | app_identifier("net.defined.mobileNebula") # The bundle identifier of your app
2 | itc_team_id("633953") # App Store Connect Team ID
3 | team_id("576H3XS7FP") # Developer Portal Team ID
4 |
5 | # For more information about the Appfile, see:
6 | # https://docs.fastlane.tools/advanced/#appfile
7 |
--------------------------------------------------------------------------------
/android/Gemfile:
--------------------------------------------------------------------------------
1 | # Autogenerated by fastlane
2 | #
3 | # Ensure this file is checked in to source control!
4 |
5 | source "https://rubygems.org"
6 |
7 | gem 'fastlane'
8 |
9 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
10 | eval_gemfile(plugins_path) if File.exist?(plugins_path)
11 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: 0b8abb4724aa590dd0f429683339b1e045a1594d
8 | channel: stable
9 |
10 | project_type: app
11 |
--------------------------------------------------------------------------------
/.github/workflows/gofmt.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if [ -z "$1" ]; then
4 | rm -f ./gofmterr
5 | find . -iname '*.go' ! -name '*.pb.go' -exec "$0" {} \;
6 | [ -f ./gofmterr ] && exit 1
7 | exit 0
8 | fi
9 |
10 | OUT="$(./nebula/goimports -d "$1" | awk '{printf "%s%%0A",$0}')"
11 | if [ -n "$OUT" ]; then
12 | echo "::error file=$1::$OUT"
13 | touch ./gofmterr
14 | fi
--------------------------------------------------------------------------------
/lib/models/UnsafeRoute.dart:
--------------------------------------------------------------------------------
1 | class UnsafeRoute {
2 | String? route;
3 | String? via;
4 |
5 | UnsafeRoute({this.route, this.via});
6 |
7 | factory UnsafeRoute.fromJson(Map json) {
8 | return UnsafeRoute(route: json['route'], via: json['via']);
9 | }
10 |
11 | Map toJson() {
12 | return {'route': route, 'via': via};
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md:
--------------------------------------------------------------------------------
1 | # Launch Screen Assets
2 |
3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory.
4 |
5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
--------------------------------------------------------------------------------
/lib/validators/mtuValidator.dart:
--------------------------------------------------------------------------------
1 | Function mtuValidator(bool required) {
2 | return (String str) {
3 | if (str == "") {
4 | return required ? 'Please fill out this field' : null;
5 | }
6 |
7 | var mtu = int.tryParse(str);
8 | if (mtu == null || mtu < 0 || mtu > 65535) {
9 | return 'Please enter a valid mtu';
10 | }
11 |
12 | return null;
13 | };
14 | }
15 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | allprojects {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 | }
7 |
8 | rootProject.buildDir = '../build'
9 | subprojects {
10 | project.buildDir = "${rootProject.buildDir}/${project.name}"
11 | }
12 | subprojects {
13 | project.evaluationDependsOn(':app')
14 | }
15 |
16 | tasks.register("clean", Delete) {
17 | delete rootProject.buildDir
18 | }
19 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
--------------------------------------------------------------------------------
/images/dn-logo-dark.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/images/dn-logo-light.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/ios/NebulaNetworkExtension/CtlInfo.h:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | /* */
5 | #define CTLIOCGINFO 0xc0644e03UL
6 | struct ctl_info {
7 | u_int32_t ctl_id;
8 | char ctl_name[96];
9 | };
10 | struct sockaddr_ctl {
11 | u_char sc_len;
12 | u_char sc_family;
13 | u_int16_t ss_sysaddr;
14 | u_int32_t sc_id;
15 | u_int32_t sc_unit;
16 | u_int32_t sc_reserved[5];
17 | };
18 |
--------------------------------------------------------------------------------
/android/mobileNebula/build.gradle:
--------------------------------------------------------------------------------
1 | configurations.maybeCreate("default")
2 |
3 | def sdkDir
4 | def ndkPath
5 |
6 | rootProject.project(':app') { p ->
7 | sdkDir = p.android.sdkDirectory
8 | ndkPath = p.android.ndkDirectory
9 | }
10 |
11 | exec {
12 | workingDir '../../'
13 | commandLine './gen-artifacts.sh', 'android'
14 | environment("ANDROID_HOME", sdkDir)
15 | environment("ANDROID_NDK_HOME", ndkPath)
16 | }
17 |
18 | artifacts.add("default", file('mobileNebula.aar'))
--------------------------------------------------------------------------------
/lib/models/CIDR.dart:
--------------------------------------------------------------------------------
1 | class CIDR {
2 | CIDR({this.ip = '', this.bits = 0});
3 |
4 | String ip;
5 | int bits;
6 |
7 | @override
8 | String toString() {
9 | return '$ip/$bits';
10 | }
11 |
12 | String toJson() {
13 | return toString();
14 | }
15 |
16 | factory CIDR.fromString(String val) {
17 | final parts = val.split('/');
18 | if (parts.length != 2) {
19 | throw 'Invalid CIDR string';
20 | }
21 |
22 | return CIDR(ip: parts[0], bits: int.parse(parts[1]));
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchImage.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchImage@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "LaunchImage@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/.github/workflows/swiftfmt.yml:
--------------------------------------------------------------------------------
1 | name: Swift format
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | paths:
8 | - ".github/workflows/swiftfmt.yml"
9 | - "**.swift"
10 | jobs:
11 | swiftfmt:
12 | name: Run swift format
13 | runs-on: macos-26
14 | steps:
15 | - name: Check out code
16 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #4.2.2
17 | with:
18 | show-progress: false
19 |
20 | - name: Check formating
21 | run: ./swift-format.sh check
22 |
--------------------------------------------------------------------------------
/lib/validators/ipValidator.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | bool ipValidator(String? str, bool enableIPV6) {
4 | if (str == null) {
5 | return false;
6 | }
7 |
8 | final ia = InternetAddress.tryParse(str);
9 | if (ia == null) {
10 | return false;
11 | }
12 |
13 | switch (ia.type) {
14 | case InternetAddressType.IPv6:
15 | {
16 | if (enableIPV6) {
17 | return true;
18 | }
19 | }
20 | break;
21 |
22 | case InternetAddressType.IPv4:
23 | {
24 | return true;
25 | }
26 | }
27 |
28 | return false;
29 | }
30 |
--------------------------------------------------------------------------------
/ios/fastlane/Matchfile:
--------------------------------------------------------------------------------
1 | git_url("https://github.com/DefinedNet/mobile_nebula_match.git")
2 |
3 | storage_mode("git")
4 |
5 | type("appstore") # The default type, can be: appstore, adhoc, enterprise or development
6 |
7 | app_identifier(["net.defined.mobileNebula", "net.defined.mobileNebula.NebulaNetworkExtension"])
8 |
9 | # username("user@fastlane.tools") # Your Apple Developer Portal username
10 |
11 | # For all available options run `fastlane match --help`
12 | # Remove the # in the beginning of the line to enable the other options
13 |
14 | # The docs are available on https://docs.fastlane.tools/actions/match
15 |
--------------------------------------------------------------------------------
/nebula/Makefile:
--------------------------------------------------------------------------------
1 | 111MODULE = on
2 | export GO111MODULE
3 |
4 | unexport SWIFT_DEBUG_INFORMATION_VERSION
5 | unexport SWIFT_DEBUG_INFORMATION_FORMAT
6 |
7 | clean:
8 | rm -rf mobileNebula.aar MobileNebula.xcframework
9 |
10 | mobileNebula.aar: *.go go.sum
11 | go get -d golang.org/x/mobile/cmd/gomobile
12 | gomobile bind -trimpath -v --target=android -androidapi=26
13 |
14 | MobileNebula.xcframework: *.go go.sum
15 | go get -d golang.org/x/mobile/cmd/gomobile
16 | gomobile bind -trimpath -v -target=ios
17 |
18 | .DEFAULT_GOAL := mobileNebula.aar
19 |
20 | .PHONY: clean
21 | all: mobileNebula.aar MobileNebula.framework
22 |
--------------------------------------------------------------------------------
/ios/NebulaNetworkExtension/NebulaNetworkExtension.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.developer.networking.networkextension
6 |
7 | packet-tunnel-provider
8 |
9 | com.apple.security.application-groups
10 |
11 | group.net.defined.mobileNebula
12 |
13 | keychain-access-groups
14 |
15 | $(AppIdentifierPrefix)group.net.defined.mobileNebula
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/lib/models/IPAndPort.dart:
--------------------------------------------------------------------------------
1 | class IPAndPort {
2 | String? ip;
3 | int? port;
4 |
5 | IPAndPort({this.ip, this.port});
6 |
7 | @override
8 | String toString() {
9 | if (ip != null && ip!.contains(':')) {
10 | return '[$ip]:$port';
11 | }
12 |
13 | return '$ip:$port';
14 | }
15 |
16 | String toJson() {
17 | return toString();
18 | }
19 |
20 | factory IPAndPort.fromString(String val) {
21 | //TODO: Uri.parse is as close as I could get to parsing both ipv4 and v6 addresses with a port without bringing a whole mess of code into here
22 | final uri = Uri.parse("ugh://$val");
23 |
24 | return IPAndPort(ip: uri.host, port: uri.port);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/lib/models/StaticHosts.dart:
--------------------------------------------------------------------------------
1 | import 'IPAndPort.dart';
2 |
3 | class StaticHost {
4 | bool lighthouse;
5 | List destinations;
6 |
7 | StaticHost({required this.lighthouse, required this.destinations});
8 |
9 | factory StaticHost.fromJson(Map json) {
10 | var list = json['destinations'] as List;
11 | var result = [];
12 |
13 | for (var item in list) {
14 | result.add(IPAndPort.fromString(item));
15 | }
16 |
17 | return StaticHost(lighthouse: json['lighthouse'], destinations: result);
18 | }
19 |
20 | Map toJson() {
21 | return {'lighthouse': lighthouse, 'destinations': destinations};
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/components/config/ConfigTextItem.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 |
3 | class ConfigTextItem extends StatelessWidget {
4 | const ConfigTextItem({
5 | super.key,
6 | this.placeholder,
7 | this.controller,
8 | this.style = const TextStyle(fontFamily: 'RobotoMono'),
9 | });
10 |
11 | final String? placeholder;
12 | final TextEditingController? controller;
13 | final TextStyle style;
14 |
15 | @override
16 | Widget build(BuildContext context) {
17 | return CupertinoTextFormFieldRow(
18 | autocorrect: false,
19 | minLines: 3,
20 | maxLines: 10,
21 | placeholder: placeholder,
22 | style: style,
23 | controller: controller,
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | *.mode1v3
2 | *.mode2v3
3 | *.moved-aside
4 | *.pbxuser
5 | *.perspectivev3
6 | **/*sync/
7 | .sconsign.dblite
8 | .tags*
9 | **/.vagrant/
10 | **/DerivedData/
11 | Icon?
12 | **/Pods/
13 | **/.symlinks/
14 | profile
15 | xcuserdata
16 | **/.generated/
17 | Flutter/App.framework
18 | Flutter/Flutter.framework
19 | Flutter/Flutter.podspec
20 | Flutter/Generated.xcconfig
21 | Flutter/app.flx
22 | Flutter/app.zip
23 | Flutter/flutter_assets/
24 | Flutter/flutter_export_environment.sh
25 | ServiceDefinitions.json
26 | Runner/GeneratedPluginRegistrant.*
27 |
28 | # Exceptions to above rules.
29 | !default.mode1v3
30 | !default.mode2v3
31 | !default.pbxuser
32 | !default.perspectivev3
33 | /NebulaNetworkExtension/MobileNebula.framework/
34 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/net/defined/mobile_nebula/MyApplication.kt:
--------------------------------------------------------------------------------
1 | package net.defined.mobile_nebula
2 |
3 | import io.flutter.embedding.engine.loader.FlutterLoader
4 | import android.app.Application
5 | import androidx.work.Configuration
6 | import androidx.work.WorkManager
7 |
8 | class MyApplication : Application() {
9 | override fun onCreate() {
10 | super.onCreate()
11 |
12 | // In order to use the WorkManager from the nebulaVpnBg process (i.e. NebulaVpnService)
13 | // we must explicitly initialize this rather than using the default initializer.
14 | val myConfig = Configuration.Builder().build()
15 | WorkManager.initialize(this, myConfig)
16 |
17 | FlutterLoader().startInitialization(applicationContext)
18 | }
19 | }
--------------------------------------------------------------------------------
/ios/Runner/Runner.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.developer.associated-domains
6 |
7 | applinks:api.defined.net
8 |
9 | com.apple.developer.networking.networkextension
10 |
11 | packet-tunnel-provider
12 |
13 | com.apple.security.application-groups
14 |
15 | group.net.defined.mobileNebula
16 |
17 | keychain-access-groups
18 |
19 | $(AppIdentifierPrefix)group.net.defined.mobileNebula
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/ios/Runner/PackageInfo.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | class PackageInfo {
4 | func getVersion() -> String {
5 | let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
6 | let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String
7 |
8 | if buildNumber == nil {
9 | return version
10 | }
11 |
12 | return "\(version)-\(buildNumber!)"
13 | }
14 |
15 | func getName() -> String {
16 | return Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ?? Bundle.main
17 | .infoDictionary?["CFBundleName"] as? String ?? "Nebula"
18 | }
19 |
20 | func getSystemVersion() -> String {
21 | let osVersion = ProcessInfo.processInfo.operatingSystemVersion
22 | return "\(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | App
9 | CFBundleIdentifier
10 | io.flutter.flutter.app
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | App
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.0
23 | MinimumOSVersion
24 | 12.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | def flutterSdkPath = {
3 | def properties = new Properties()
4 | file("local.properties").withInputStream { properties.load(it) }
5 | def flutterSdkPath = properties.getProperty("flutter.sdk")
6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
7 | return flutterSdkPath
8 | }()
9 |
10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
11 |
12 | repositories {
13 | google()
14 | mavenCentral()
15 | gradlePluginPortal()
16 | }
17 | }
18 |
19 | plugins {
20 | id "org.gradle.toolchains.foojay-resolver-convention" version "0.8.0"
21 | id "dev.flutter.flutter-plugin-loader" version "1.0.0"
22 | id "com.android.application" version '8.13.2' apply false
23 | id "org.jetbrains.kotlin.android" version "2.0.20" apply false
24 | }
25 |
26 | include ':app', ':mobileNebula'
--------------------------------------------------------------------------------
/lib/components/config/ConfigButtonItem.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:mobile_nebula/components/SpecialButton.dart';
4 | import 'package:mobile_nebula/services/utils.dart';
5 |
6 | // A config item that detects tapping and calls back on a tap
7 | class ConfigButtonItem extends StatelessWidget {
8 | const ConfigButtonItem({super.key, this.content, this.onPressed});
9 |
10 | final Widget? content;
11 | final void Function()? onPressed;
12 |
13 | @override
14 | Widget build(BuildContext context) {
15 | return SpecialButton(
16 | color: Utils.configItemBackground(context),
17 | onPressed: onPressed,
18 | useButtonTheme: true,
19 | child: Container(
20 | constraints: BoxConstraints(minHeight: Utils.minInteractiveSize, minWidth: double.infinity),
21 | child: Center(child: content),
22 | ),
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/validators/dnsValidator.dart:
--------------------------------------------------------------------------------
1 | // Inspired by https://github.com/suragch/string_validator/blob/master/lib/src/validator.dart
2 |
3 | bool dnsValidator(str, {requireTld = true, allowUnderscore = false}) {
4 | if (str == null) {
5 | return false;
6 | }
7 |
8 | List parts = str.split('.');
9 | if (requireTld) {
10 | var tld = parts.removeLast();
11 | if (parts.isEmpty || !RegExp(r'^[a-z]{2,}$').hasMatch(tld)) {
12 | return false;
13 | }
14 | }
15 |
16 | for (var part, i = 0; i < parts.length; i++) {
17 | part = parts[i];
18 | if (allowUnderscore) {
19 | if (part.indexOf('__') >= 0) {
20 | return false;
21 | }
22 | }
23 |
24 | if (!RegExp(r'^[a-z\\u00a1-\\uffff0-9-]+$').hasMatch(part)) {
25 | return false;
26 | }
27 |
28 | if (part[0] == '-' || part[part.length - 1] == '-' || part.indexOf('---') >= 0) {
29 | return false;
30 | }
31 | }
32 |
33 | return true;
34 | }
35 |
--------------------------------------------------------------------------------
/lib/components/SiteTitle.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_svg/svg.dart';
3 |
4 | import '../models/Site.dart';
5 |
6 | class SiteTitle extends StatelessWidget {
7 | const SiteTitle({super.key, required this.site});
8 |
9 | final Site site;
10 |
11 | @override
12 | Widget build(BuildContext context) {
13 | final dnIcon =
14 | Theme.of(context).brightness == Brightness.dark ? 'images/dn-logo-dark.svg' : 'images/dn-logo-light.svg';
15 |
16 | return IntrinsicWidth(
17 | child: Padding(
18 | padding: EdgeInsets.symmetric(horizontal: 16),
19 | child: Row(
20 | children: [
21 | site.managed
22 | ? Padding(padding: EdgeInsets.only(right: 10), child: SvgPicture.asset(dnIcon, width: 12))
23 | : Container(),
24 | Expanded(child: Text(site.name, overflow: TextOverflow.ellipsis)),
25 | ],
26 | ),
27 | ),
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/components/config/ConfigHeader.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter/cupertino.dart';
4 | import 'package:flutter/material.dart';
5 |
6 | TextStyle basicTextStyle(BuildContext context) =>
7 | Platform.isIOS ? CupertinoTheme.of(context).textTheme.textStyle : Theme.of(context).textTheme.titleMedium!;
8 |
9 | const double _headerFontSize = 13.0;
10 |
11 | class ConfigHeader extends StatelessWidget {
12 | const ConfigHeader({super.key, required this.label, this.color});
13 |
14 | final String label;
15 | final Color? color;
16 |
17 | @override
18 | Widget build(BuildContext context) {
19 | return Container(
20 | padding: const EdgeInsets.only(left: 10.0, top: 30.0, bottom: 5.0, right: 10.0),
21 | child: Text(
22 | label,
23 | style: basicTextStyle(
24 | context,
25 | ).copyWith(color: color ?? CupertinoColors.secondaryLabel.resolveFrom(context), fontSize: _headerFontSize),
26 | ),
27 | );
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib/services/logs.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter/widgets.dart';
4 | import 'package:mobile_nebula/services/result.dart';
5 |
6 | class LogsNotFoundException implements Exception {
7 | String error() => 'No logs found. Logs will be available after starting the site for the first time.';
8 | }
9 |
10 | class LogsNotifier extends ChangeNotifier {
11 | Result? logsResult;
12 |
13 | LogsNotifier();
14 |
15 | loadLogs({required String logFile}) async {
16 | final file = File(logFile);
17 | try {
18 | logsResult = Result.ok(await file.readAsString());
19 | notifyListeners();
20 | } on FileSystemException {
21 | logsResult = Result.error(LogsNotFoundException());
22 | notifyListeners();
23 | } on Exception catch (err) {
24 | logsResult = Result.error(err);
25 | notifyListeners();
26 | } catch (err) {
27 | logsResult = Result.error(Exception(err));
28 | notifyListeners();
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/android/fastlane/README.md:
--------------------------------------------------------------------------------
1 | fastlane documentation
2 | ----
3 |
4 | # Installation
5 |
6 | Make sure you have the latest version of the Xcode command line tools installed:
7 |
8 | ```sh
9 | xcode-select --install
10 | ```
11 |
12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
13 |
14 | # Available Actions
15 |
16 | ## Android
17 |
18 | ### android release_build_number
19 |
20 | ```sh
21 | [bundle exec] fastlane android release_build_number
22 | ```
23 |
24 |
25 |
26 | ### android release
27 |
28 | ```sh
29 | [bundle exec] fastlane android release
30 | ```
31 |
32 | Deploy a new version to the Google Play
33 |
34 | ----
35 |
36 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
37 |
38 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
39 |
40 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
41 |
--------------------------------------------------------------------------------
/.github/workflows/gofmt.yml:
--------------------------------------------------------------------------------
1 | name: gofmt
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | paths:
8 | - '.github/workflows/gofmt.yml'
9 | - '.github/workflows/gofmt.sh'
10 | - '**.go'
11 | jobs:
12 |
13 | gofmt:
14 | name: Run gofmt
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Check out code into the Go module directory
18 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #4.2.2
19 | with:
20 | show-progress: false
21 |
22 | - name: Set up Go 1.22
23 | uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 #5.3.0
24 | with:
25 | go-version: '1.22'
26 | cache-dependency-path: nebula/go.sum
27 |
28 | - name: Install goimports
29 | working-directory: nebula
30 | run: |
31 | go get golang.org/x/tools/cmd/goimports
32 | go build golang.org/x/tools/cmd/goimports
33 |
34 | - name: gofmt
35 | run: $GITHUB_WORKSPACE/.github/workflows/gofmt.sh
36 |
--------------------------------------------------------------------------------
/lib/components/buttons/PrimaryButton.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter/cupertino.dart';
4 | import 'package:flutter/material.dart';
5 |
6 | class PrimaryButton extends StatelessWidget {
7 | const PrimaryButton({super.key, required this.child, this.onPressed});
8 |
9 | final Widget child;
10 | final GestureTapCallback? onPressed;
11 |
12 | @override
13 | Widget build(BuildContext context) {
14 | if (Platform.isAndroid) {
15 | return FilledButton(
16 | onPressed: onPressed,
17 | style: FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.primary),
18 | child: child,
19 | );
20 | } else {
21 | // Workaround for https://github.com/flutter/flutter/issues/161590
22 | final themeData = CupertinoTheme.of(context);
23 | return CupertinoTheme(
24 | data: themeData.copyWith(primaryColor: CupertinoColors.white),
25 | child: CupertinoButton(
26 | onPressed: onPressed,
27 | color: CupertinoColors.secondaryLabel.resolveFrom(context),
28 | child: child,
29 | ),
30 | );
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/components/DangerButton.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter/cupertino.dart';
4 | import 'package:flutter/material.dart';
5 |
6 | class DangerButton extends StatelessWidget {
7 | const DangerButton({super.key, required this.child, this.onPressed});
8 |
9 | final Widget child;
10 | final GestureTapCallback? onPressed;
11 |
12 | @override
13 | Widget build(BuildContext context) {
14 | if (Platform.isAndroid) {
15 | return FilledButton(
16 | onPressed: onPressed,
17 | style: FilledButton.styleFrom(
18 | backgroundColor: Theme.of(context).colorScheme.error,
19 | foregroundColor: Theme.of(context).colorScheme.onError,
20 | ),
21 | child: child,
22 | );
23 | } else {
24 | // Workaround for https://github.com/flutter/flutter/issues/161590
25 | final themeData = CupertinoTheme.of(context);
26 | return CupertinoTheme(
27 | data: themeData.copyWith(primaryColor: CupertinoColors.white),
28 | child: CupertinoButton(
29 | onPressed: onPressed,
30 | color: CupertinoColors.systemRed.resolveFrom(context),
31 | child: child,
32 | ),
33 | );
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/ios/NebulaNetworkExtension/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | NebulaNetworkExtension
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | NSExtension
24 |
25 | NSExtensionPointIdentifier
26 | com.apple.networkextension.packet-tunnel
27 | NSExtensionPrincipalClass
28 | $(PRODUCT_MODULE_NAME).PacketTunnelProvider
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 | android/app/.cxx
12 |
13 | # IntelliJ related
14 | *.iml
15 | *.ipr
16 | *.iws
17 | .idea/
18 |
19 | # The .vscode folder contains launch configuration and tasks you configure in
20 | # VS Code which you may wish to be included in version control, so this line
21 | # is commented out by default.
22 | #.vscode/
23 |
24 | # Flutter/Dart/Pub related
25 | **/doc/api/
26 | .dart_tool/
27 | .flutter-plugins
28 | .flutter-plugins-dependencies
29 | .packages
30 | .pub-cache/
31 | .pub/
32 | /build/
33 |
34 | # Web related
35 | lib/generated_plugin_registrant.dart
36 |
37 | # Exceptions to above rules.
38 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
39 | /nebula/local.settings
40 | /nebula/MobileNebula.framework/
41 | /nebula/mobileNebula-sources.jar
42 | /nebula/vendor/
43 | /android/app/src/main/libs/mobileNebula.aar
44 | /nebula/mobileNebula.aar
45 | /android/key.properties
46 | /env.sh
47 | /lib/gen.versions.dart
48 | /lib/.gen.versions.dart
49 | /lib/oss_licenses.dart
50 | /ios/Flutter/.last_build_id
51 | /local.properties
52 | /.gradle/
53 | *.keystore
54 | /nebula/MobileNebula.xcframework/
55 | /ios/MobileNebula.xcframework/
56 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [Unreleased]
9 |
10 | ## [0.0.41] - 2021-06-09
11 |
12 | ### Added
13 |
14 | - Added an option to wrap logs in the hamburger menu. (#10)
15 |
16 | - IPv6 and better roaming support. (#24)
17 |
18 | - Certificates can now be replaced. (#33)
19 |
20 | ### Changed
21 |
22 | - Upgraded to Flutter 2. (#26)
23 |
24 | - Upgraded core Nebula to 1.4.1. (#41)
25 |
26 | ### Fixed
27 |
28 | - iOS: Reworked vpn process IPC for more reliable communication. (#28)
29 |
30 | - Android: Detecting the active vpn site on app boot is now more reliable. (#29)
31 |
32 | - Android: Quickly toggling site connection status no longer presents an error. (#16)
33 |
34 | - Android: Better vpn shutdown support. (#34)
35 |
36 | - Android: System DNS will continue to work when moving between IPv4 only and IPv6 networks. (#40)
37 |
38 | ## [0.0.38] - 2020-09-25
39 |
40 | ### Added
41 |
42 | - Initial public release.
43 |
44 | [0.0.38]: https://github.com/DefinedNet/mobile_nebula/releases/tag/v0.0.38
45 | [0.0.41]: https://github.com/DefinedNet/mobile_nebula/releases/tag/v0.0.41
--------------------------------------------------------------------------------
/.github/workflows/fluttercheck.yml:
--------------------------------------------------------------------------------
1 | name: Flutter check
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | paths:
8 | - '.github/workflows/fluttercheck.yml'
9 | - '**.dart'
10 | jobs:
11 | flutterfmt:
12 | name: Run flutter format
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Install flutter
16 | uses: subosito/flutter-action@f2c4f6686ca8e8d6e6d0f28410eeef506ed66aff #v2.18.0
17 | with:
18 | flutter-version: '3.29.2'
19 |
20 | - name: Check out code
21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #4.2.2
22 | with:
23 | show-progress: false
24 |
25 | - name: Check formating
26 | run: dart format -l120 lib/ --set-exit-if-changed --suppress-analytics --output none
27 | flutterlint:
28 | name: Run flutter lint
29 | runs-on: ubuntu-latest
30 | steps:
31 | - name: Install flutter
32 | uses: subosito/flutter-action@f2c4f6686ca8e8d6e6d0f28410eeef506ed66aff #v2.18.0
33 | with:
34 | flutter-version: '3.29.2'
35 |
36 | - name: Check out code
37 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #4.2.2
38 | with:
39 | show-progress: false
40 |
41 | - name: Check linting
42 | run: dart fix --dry-run
43 |
--------------------------------------------------------------------------------
/lib/screens/siteConfig/RenderedConfigScreen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
3 | import 'package:mobile_nebula/components/SimplePage.dart';
4 | import 'package:mobile_nebula/services/share.dart';
5 |
6 | class RenderedConfigScreen extends StatelessWidget {
7 | final String config;
8 | final String name;
9 |
10 | const RenderedConfigScreen({super.key, required this.config, required this.name});
11 |
12 | @override
13 | Widget build(BuildContext context) {
14 | return SimplePage(
15 | title: Text('Rendered Site Config'),
16 | scrollable: SimpleScrollable.both,
17 | trailingActions: [
18 | Builder(
19 | builder: (BuildContext context) {
20 | return PlatformIconButton(
21 | padding: EdgeInsets.zero,
22 | icon: Icon(context.platformIcons.share, size: 28.0),
23 | onPressed: () => Share.share(context, title: '$name.yaml', text: config, filename: '$name.yaml'),
24 | );
25 | },
26 | ),
27 | ],
28 | child: Container(
29 | padding: EdgeInsets.all(5),
30 | constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width),
31 | child: SelectableText(config, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
32 | ),
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/net/defined/mobile_nebula/PackageInfo.kt:
--------------------------------------------------------------------------------
1 | package net.defined.mobile_nebula
2 |
3 | import android.content.Context
4 | import android.content.pm.ApplicationInfo
5 | import android.content.pm.PackageInfo
6 | import android.content.pm.PackageManager
7 | import android.os.Build
8 |
9 | class PackageInfo(private val context: Context) {
10 | private val pInfo: PackageInfo =
11 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
12 | context.packageManager.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(0))
13 | else
14 | @Suppress("DEPRECATION")
15 | context.packageManager.getPackageInfo(context.packageName, 0)
16 |
17 | private val appInfo: ApplicationInfo = context.applicationInfo
18 |
19 | fun getVersion(): String {
20 | val version: String? = pInfo.versionName
21 | val build: Long = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
22 | pInfo.longVersionCode
23 | else
24 | @Suppress("DEPRECATION")
25 | pInfo.versionCode.toLong()
26 | return "%s-%d".format(version, build)
27 | }
28 |
29 | fun getName(): String {
30 | val stringId = appInfo.labelRes
31 | return if (stringId == 0) appInfo.nonLocalizedLabel.toString() else context.getString(stringId)
32 | }
33 |
34 | fun getSystemVersion(): String {
35 | return Build.VERSION.RELEASE
36 | }
37 | }
--------------------------------------------------------------------------------
/lib/components/config/ConfigItem.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter/cupertino.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:mobile_nebula/services/utils.dart';
6 |
7 | class ConfigItem extends StatelessWidget {
8 | const ConfigItem({
9 | super.key,
10 | this.label,
11 | required this.content,
12 | this.labelWidth = 100,
13 | this.crossAxisAlignment = CrossAxisAlignment.center,
14 | });
15 |
16 | final Widget? label;
17 | final Widget content;
18 | final double labelWidth;
19 | final CrossAxisAlignment crossAxisAlignment;
20 |
21 | @override
22 | Widget build(BuildContext context) {
23 | TextStyle textStyle;
24 | if (Platform.isAndroid) {
25 | textStyle = Theme.of(context).textTheme.labelLarge!.copyWith(fontWeight: FontWeight.normal);
26 | } else {
27 | textStyle = CupertinoTheme.of(context).textTheme.textStyle;
28 | }
29 |
30 | return Container(
31 | color: Utils.configItemBackground(context),
32 | padding: EdgeInsets.symmetric(vertical: 6, horizontal: 15),
33 | constraints: BoxConstraints(minHeight: Utils.minInteractiveSize),
34 | child: Row(
35 | crossAxisAlignment: crossAxisAlignment,
36 | children: [
37 | SizedBox(width: labelWidth, child: DefaultTextStyle(style: textStyle, child: Container(child: label))),
38 | Expanded(child: DefaultTextStyle(style: textStyle, child: Container(child: content))),
39 | ],
40 | ),
41 | );
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/lib/services/result.dart:
--------------------------------------------------------------------------------
1 | /// Utility class that simplifies handling errors.
2 | ///
3 | /// Return a [Result] from a function to indicate success or failure.
4 | ///
5 | /// A [Result] is either an [Ok] with a value of type [T]
6 | /// or an [Error] with an [Exception].
7 | ///
8 | /// Use [Result.ok] to create a successful result with a value of type [T].
9 | /// Use [Result.error] to create an error result with an [Exception].
10 | ///
11 | /// Evaluate the result using a switch statement:
12 | /// ```dart
13 | /// switch (result) {
14 | /// case Ok(): {
15 | /// print(result.value);
16 | /// }
17 | /// case Error(): {
18 | /// print(result.error);
19 | /// }
20 | /// }
21 | /// ```
22 | sealed class Result {
23 | const Result();
24 |
25 | /// Creates a successful [Result], completed with the specified [value].
26 | const factory Result.ok(T value) = Ok._;
27 |
28 | /// Creates an error [Result], completed with the specified [error].
29 | const factory Result.error(Exception error) = Error._;
30 | }
31 |
32 | /// A successful [Result] with a returned [value].
33 | final class Ok extends Result {
34 | const Ok._(this.value);
35 |
36 | /// The returned value of this result.
37 | final T value;
38 |
39 | @override
40 | String toString() => 'Result<$T>.ok($value)';
41 | }
42 |
43 | /// An error [Result] with a resulting [error].
44 | final class Error extends Result {
45 | const Error._(this.error);
46 |
47 | /// The resulting error of this result.
48 | final Exception error;
49 |
50 | @override
51 | String toString() => 'Result<$T>.error($error)';
52 | }
53 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/net/defined/mobile_nebula/APIClient.kt:
--------------------------------------------------------------------------------
1 | package net.defined.mobile_nebula
2 |
3 | import android.content.Context
4 | import com.google.gson.Gson
5 |
6 | class InvalidCredentialsException: Exception("Invalid credentials")
7 |
8 | class APIClient(context: Context) {
9 | private val packageInfo = PackageInfo(context)
10 | private val client = mobileNebula.MobileNebula.newAPIClient(
11 | "MobileNebula/%s (Android %s)".format(
12 | packageInfo.getVersion(),
13 | packageInfo.getSystemVersion(),
14 | ))
15 | private val gson = Gson()
16 |
17 | fun enroll(code: String): IncomingSite {
18 | val res = client.enroll(code)
19 | return decodeIncomingSite(res.site)
20 | }
21 |
22 | fun tryUpdate(siteName: String, hostID: String, privateKey: String, counter: Long, trustedKeys: String): IncomingSite? {
23 | val res: mobileNebula.TryUpdateResult
24 | try {
25 | res = client.tryUpdate(siteName, hostID, privateKey, counter, trustedKeys)
26 | } catch (e: Exception) {
27 | // type information from Go is not available, use string matching instead
28 | if (e.message == "invalid credentials") {
29 | throw InvalidCredentialsException()
30 | }
31 |
32 | throw e
33 | }
34 |
35 | if (res.fetchedUpdate) {
36 | return decodeIncomingSite(res.site)
37 | }
38 |
39 | return null
40 | }
41 |
42 | private fun decodeIncomingSite(jsonSite: String): IncomingSite {
43 | return gson.fromJson(jsonSite, IncomingSite::class.java)
44 | }
45 | }
--------------------------------------------------------------------------------
/gen-artifacts.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 | . ./env.sh
6 |
7 | # Generate gomobile nebula bindings
8 | cd nebula
9 |
10 | if [ "$1" = "ios" ]; then
11 | # Build for nebula for iOS
12 | make MobileNebula.xcframework
13 | rm -rf ../ios/MobileNebula.xcframework
14 | cp -r MobileNebula.xcframework ../ios/
15 |
16 | elif [ "$1" = "android" ]; then
17 | # Build nebula for android
18 | make mobileNebula.aar
19 | mkdir -p ../android/mobileNebula
20 | rm -rf ../android/mobileNebula/mobileNebula.aar
21 | cp mobileNebula.aar ../android/mobileNebula/mobileNebula.aar
22 |
23 | else
24 | echo "Error: unsupported target os $1"
25 | exit 1
26 | fi
27 |
28 | cd ..
29 |
30 | # Generate version info to display in about
31 | {
32 | # Get the flutter and dart versions
33 | printf "const flutterVersion = "
34 | flutter --version --machine
35 | echo ";"
36 |
37 | # Get our current git sha
38 | git rev-parse --short HEAD | sed -e "s/\(.*\)/const gitSha = '\1';/"
39 |
40 | # Get the nebula version
41 | cd nebula
42 | NEBULA_VERSION="$(go list -m -f "{{.Version}}" github.com/slackhq/nebula | cut -f1 -d'-' | cut -c2-)"
43 | echo "const nebulaVersion = '$NEBULA_VERSION';"
44 | cd ..
45 |
46 | # Get our golang version
47 | echo "const goVersion = '$(go version | awk '{print $3}')';"
48 | } > lib/.gen.versions.dart
49 |
50 | # Try and avoid issues with building by moving into place after we are complete
51 | #TODO: this might be a parallel build of deps issue in kotlin, might need to solve there
52 | mv lib/.gen.versions.dart lib/gen.versions.dart
53 |
54 | # Generate licenses library
55 | flutter pub run flutter_oss_licenses:generate.dart
56 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints.
3 | #
4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6 | # invoked from the command line by running `flutter analyze`.
7 |
8 | # The following line activates a set of recommended lints for Flutter apps,
9 | # packages, and plugins designed to encourage good coding practices.
10 | include: package:flutter_lints/flutter.yaml
11 |
12 | linter:
13 | # The lint rules applied to this project can be customized in the
14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml`
15 | # included above or to enable additional rules. A list of all available lints
16 | # and their documentation is published at https://dart.dev/tools/linter-rules.
17 | #
18 | # Instead of disabling a lint rule for the entire project in the
19 | # section below, it can also be suppressed for a single line of code
20 | # or a specific dart file by using the `// ignore: name_of_lint` and
21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
22 | # producing the lint.
23 | rules:
24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
26 |
27 | # Additional information about this file can be found at
28 | # https://dart.dev/tools/analysis
29 | formatter:
30 | page_width: 120
31 |
32 | analyzer:
33 | exclude:
34 | # This is a generated file, let's ignore it.
35 | - lib/services/theme.dart
36 |
--------------------------------------------------------------------------------
/android/fastlane/Fastfile:
--------------------------------------------------------------------------------
1 | # This file contains the fastlane.tools configuration
2 | # You can find the documentation at https://docs.fastlane.tools
3 | #
4 | # For a list of all available actions, check out
5 | #
6 | # https://docs.fastlane.tools/actions
7 | #
8 | # For a list of all available plugins, check out
9 | #
10 | # https://docs.fastlane.tools/plugins/available-plugins
11 | #
12 |
13 | # Uncomment the line if you want fastlane to automatically update itself
14 | # update_fastlane
15 |
16 | default_platform(:android)
17 |
18 | platform :android do
19 | lane :release_build_number do
20 | nextCode = sprintf("%s", latest_googleplay_version_code + 1)
21 | File.write("../../release_build_number", nextCode)
22 | end
23 |
24 | desc "Deploy a new version to the Google Play"
25 | lane :release do
26 | upload_to_play_store(
27 | track: 'internal',
28 | aab: '../build/app/outputs/bundle/release/app-release.aab'
29 | )
30 | end
31 | end
32 |
33 | def latest_googleplay_version_code
34 | productionVersionCodes = google_play_track_version_codes(track: 'production')
35 | #NOTE: we do not have a beta track right now
36 | #betaVersionCodes = google_play_track_version_codes(track: 'beta')
37 | alphaVersionCodes = google_play_track_version_codes(track: 'alpha')
38 | internalVersionCodes = google_play_track_version_codes(track: 'internal')
39 |
40 | # puts version codes from all tracks into the same array
41 | versionCodes = [
42 | productionVersionCodes,
43 | #betaVersionCodes,
44 | alphaVersionCodes,
45 | internalVersionCodes
46 | ].reduce([], :concat)
47 |
48 | # returns the highest version code from array
49 | return versionCodes.max
50 | end
--------------------------------------------------------------------------------
/lib/components/config/ConfigCheckboxItem.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:mobile_nebula/components/SpecialButton.dart';
3 | import 'package:mobile_nebula/services/utils.dart';
4 |
5 | class ConfigCheckboxItem extends StatelessWidget {
6 | const ConfigCheckboxItem({
7 | super.key,
8 | this.label,
9 | this.content,
10 | this.labelWidth = 100,
11 | this.onChanged,
12 | this.checked = false,
13 | });
14 |
15 | final Widget? label;
16 | final Widget? content;
17 | final double labelWidth;
18 | final bool checked;
19 | final Function? onChanged;
20 |
21 | @override
22 | Widget build(BuildContext context) {
23 | Widget item = Container(
24 | padding: EdgeInsets.symmetric(horizontal: 15),
25 | constraints: BoxConstraints(minHeight: Utils.minInteractiveSize, minWidth: double.infinity),
26 | child: Row(
27 | crossAxisAlignment: CrossAxisAlignment.center,
28 | children: [
29 | label != null ? SizedBox(width: labelWidth, child: label) : Container(),
30 | Expanded(child: Container(padding: EdgeInsets.only(right: 10), child: content)),
31 | checked
32 | ? Icon(CupertinoIcons.check_mark, color: CupertinoColors.systemBlue.resolveFrom(context))
33 | : Container(),
34 | ],
35 | ),
36 | );
37 |
38 | if (onChanged != null) {
39 | return SpecialButton(
40 | color: Utils.configItemBackground(context),
41 | child: item,
42 | onPressed: () {
43 | if (onChanged != null) {
44 | onChanged!();
45 | }
46 | },
47 | );
48 | } else {
49 | return item;
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/ios/Runner/APIClient.swift:
--------------------------------------------------------------------------------
1 | import MobileNebula
2 |
3 | enum APIClientError: Error {
4 | case invalidCredentials
5 | }
6 |
7 | class APIClient {
8 | let apiClient: MobileNebulaAPIClient
9 | let json = JSONDecoder()
10 |
11 | init() {
12 | let packageInfo = PackageInfo()
13 | apiClient = MobileNebulaNewAPIClient(
14 | "MobileNebula/\(packageInfo.getVersion()) (iOS \(packageInfo.getSystemVersion()))")!
15 | }
16 |
17 | func enroll(code: String) throws -> IncomingSite {
18 | let res = try apiClient.enroll(code)
19 | return try decodeIncomingSite(jsonSite: res.site)
20 | }
21 |
22 | func tryUpdate(
23 | siteName: String, hostID: String, privateKey: String, counter: Int, trustedKeys: String
24 | ) throws -> IncomingSite? {
25 | let res: MobileNebulaTryUpdateResult
26 | do {
27 | res = try apiClient.tryUpdate(
28 | siteName,
29 | hostID: hostID,
30 | privateKey: privateKey,
31 | counter: counter,
32 | trustedKeys: trustedKeys)
33 | } catch {
34 | // type information from Go is not available, use string matching instead
35 | if error.localizedDescription == "invalid credentials" {
36 | throw APIClientError.invalidCredentials
37 | }
38 |
39 | throw error
40 | }
41 |
42 | if res.fetchedUpdate {
43 | return try decodeIncomingSite(jsonSite: res.site)
44 | }
45 |
46 | return nil
47 | }
48 |
49 | private func decodeIncomingSite(jsonSite: String) throws -> IncomingSite {
50 | do {
51 | return try json.decode(IncomingSite.self, from: jsonSite.data(using: .utf8)!)
52 | } catch {
53 | print("decodeIncomingSite: \(error)")
54 | throw error
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/lib/components/config/ConfigSection.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:mobile_nebula/services/utils.dart';
3 |
4 | import 'ConfigHeader.dart';
5 |
6 | class ConfigSection extends StatelessWidget {
7 | const ConfigSection({super.key, this.label, required this.children, this.borderColor, this.labelColor});
8 |
9 | final List children;
10 | final String? label;
11 | final Color? borderColor;
12 | final Color? labelColor;
13 |
14 | @override
15 | Widget build(BuildContext context) {
16 | final border = BorderSide(color: borderColor ?? Utils.configSectionBorder(context));
17 |
18 | List mappedChildren = [];
19 | final len = children.length;
20 |
21 | for (var i = 0; i < len; i++) {
22 | mappedChildren.add(children[i]);
23 |
24 | if (i < len - 1) {
25 | double pad = 15;
26 | if (children[i + 1].runtimeType.toString() == 'ConfigButtonItem') {
27 | pad = 0;
28 | }
29 | mappedChildren.add(
30 | Padding(
31 | padding: EdgeInsets.only(left: pad),
32 | child: Divider(height: 1, color: Utils.configSectionBorder(context)),
33 | ),
34 | );
35 | }
36 | }
37 |
38 | return Column(
39 | crossAxisAlignment: CrossAxisAlignment.start,
40 | children: [
41 | label != null ? ConfigHeader(label: label!, color: labelColor) : Container(height: 20),
42 | Container(
43 | decoration: BoxDecoration(
44 | border: Border(top: border, bottom: border),
45 | color: Utils.configItemBackground(context),
46 | ),
47 | child: Column(children: mappedChildren),
48 | ),
49 | ],
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/nebula/mobileNebula_test.go:
--------------------------------------------------------------------------------
1 | package mobileNebula
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/sirupsen/logrus"
7 | nebcfg "github.com/slackhq/nebula/config"
8 | )
9 |
10 | func TestParseCerts(t *testing.T) {
11 | jsonConfig := `{
12 | "name": "Debug Test - unsafe",
13 | "id": "be9d6756-4099-4b25-a901-9d3b773e7d1a",
14 | "staticHostmap": {
15 | "10.1.0.1": {
16 | "lighthouse": true,
17 | "destinations": [
18 | "10.1.1.53:4242"
19 | ]
20 | }
21 | },
22 | "unsafeRoutes": [
23 | {
24 | "route": "10.3.3.3/32",
25 | "via": "10.1.0.1",
26 | "mtu": null
27 | },
28 | {
29 | "route": "1.1.1.2/32",
30 | "via": "10.1.0.1",
31 | "mtu": null
32 | }
33 | ],
34 | "ca": "-----BEGIN NEBULA CERTIFICATE-----\nCpEBCg9EZWZpbmVkIHJvb3QgMDISE4CAhFCA/v//D4CCoIUMgID8/w8aE4CAgFCA\n/v//D4CAoIUMgID8/w8iBHRlc3QiBmxhcHRvcCIFcGhvbmUiCGVtcGxveWVlIgVh\nZG1pbiiI05z1BTCIuqGEBjogV/nxuQ1/kN12IrYs/H1cpZr3agQUnRs9FqWdJcOa\nJSlAARJA4H1wI3hdfVpIy8Y9IZHqIlMIFObCu5ceM4aELiTKsEGv+g7u8Dn1VY8g\nQPNsuOsqJB3ma8PntddPYn5QgH+qDA==\n-----END NEBULA CERTIFICATE-----\n",
35 | "cert": "-----BEGIN NEBULA CERTIFICATE-----\nCmcKCmNocm9tZWJvb2sSCYmAhFCA/v//DyiR1Zf2BTCHuqGEBjogqtoJL9WKGKLp\nb3BIgTEZnTTusSJOiswuf1DS7jPjMzFKIIstsyPnnccgEYkNflwrYBvZFMCOtgmN\nuc5Jpc5lbzM9EkBACYP3VMFYHk2h5AcpURcG6QwS4iYOgHET7lMbM7WSMj4ZnzLR\ni2HhX58vSTr6evgvKuSPaA23hLUqR65QNRQD\n-----END NEBULA CERTIFICATE-----\n",
36 | "key": null,
37 | "lhDuration": 7200,
38 | "port": 4242,
39 | "mtu": 1300,
40 | "cipher": "aes",
41 | "sortKey": 3,
42 | "logVerbosity": "info"
43 | }`
44 | s, err := RenderConfig(jsonConfig, "")
45 |
46 | config := nebcfg.NewC(logrus.New())
47 | err = config.LoadString(s)
48 |
49 | t.Log(err)
50 | return
51 | }
52 |
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/lib/services/storage.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:io';
3 |
4 | import 'package:path_provider/path_provider.dart';
5 | import 'package:path/path.dart' as p;
6 |
7 | class Storage {
8 | Future mkdir(String path) async {
9 | final parent = await localPath;
10 | return Directory(p.join(parent, path)).create(recursive: true);
11 | }
12 |
13 | Future> listDir(String path) async {
14 | List list = [];
15 | var parent = await localPath;
16 |
17 | if (path != '') {
18 | parent = p.join(parent, path);
19 | }
20 |
21 | var completer = Completer>();
22 |
23 | Directory(parent)
24 | .list()
25 | .listen((FileSystemEntity entity) {
26 | list.add(entity);
27 | })
28 | .onDone(() {
29 | completer.complete(list);
30 | });
31 |
32 | return completer.future;
33 | }
34 |
35 | Future get localPath async {
36 | final directory = await getApplicationDocumentsDirectory();
37 | return directory.path;
38 | }
39 |
40 | Future readFile(String path) async {
41 | try {
42 | final parent = await localPath;
43 | final file = File(p.join(parent, path));
44 |
45 | // Read the file
46 | return await file.readAsString();
47 | } catch (e) {
48 | return null;
49 | }
50 | }
51 |
52 | Future writeFile(String path, String contents) async {
53 | final parent = await localPath;
54 | final file = File(p.join(parent, path));
55 |
56 | // Write the file
57 | return file.writeAsString(contents);
58 | }
59 |
60 | Future delete(String path) async {
61 | var parent = await localPath;
62 | return File(p.join(parent, path)).delete(recursive: true);
63 | }
64 |
65 | Future getFullPath(String path) async {
66 | var parent = await localPath;
67 | return p.join(parent, path);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/lib/screens/siteConfig/LogVerbosityScreen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter/widgets.dart';
4 | import 'package:mobile_nebula/components/FormPage.dart';
5 | import 'package:mobile_nebula/components/config/ConfigCheckboxItem.dart';
6 | import 'package:mobile_nebula/components/config/ConfigSection.dart';
7 |
8 | class LogVerbosityScreen extends StatefulWidget {
9 | const LogVerbosityScreen({super.key, required this.verbosity, required this.onSave});
10 |
11 | final String verbosity;
12 | final ValueChanged onSave;
13 |
14 | @override
15 | _LogVerbosityScreenState createState() => _LogVerbosityScreenState();
16 | }
17 |
18 | class _LogVerbosityScreenState extends State {
19 | late String verbosity;
20 | bool changed = false;
21 |
22 | @override
23 | void initState() {
24 | verbosity = widget.verbosity;
25 | super.initState();
26 | }
27 |
28 | @override
29 | Widget build(BuildContext context) {
30 | return FormPage(
31 | title: 'Log Verbosity',
32 | changed: changed,
33 | onSave: () {
34 | Navigator.pop(context);
35 | widget.onSave(verbosity);
36 | },
37 | child: Column(
38 | children: [
39 | ConfigSection(
40 | children: [
41 | _buildEntry('debug'),
42 | _buildEntry('info'),
43 | _buildEntry('warning'),
44 | _buildEntry('error'),
45 | _buildEntry('fatal'),
46 | _buildEntry('panic'),
47 | ],
48 | ),
49 | ],
50 | ),
51 | );
52 | }
53 |
54 | Widget _buildEntry(String title) {
55 | return ConfigCheckboxItem(
56 | label: Text(title),
57 | labelWidth: 150,
58 | checked: verbosity == title,
59 | onChanged: () {
60 | setState(() {
61 | changed = true;
62 | verbosity = title;
63 | });
64 | },
65 | );
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | platform :ios, '14'
3 |
4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
6 |
7 | project 'Runner', {
8 | 'Debug' => :debug,
9 | 'Profile' => :release,
10 | 'Release' => :release,
11 | }
12 |
13 | def flutter_root
14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
15 | unless File.exist?(generated_xcode_build_settings_path)
16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
17 | end
18 |
19 | File.foreach(generated_xcode_build_settings_path) do |line|
20 | matches = line.match(/FLUTTER_ROOT\=(.*)/)
21 | return matches[1].strip if matches
22 | end
23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
24 | end
25 |
26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
27 |
28 | flutter_ios_podfile_setup
29 |
30 | Pod::PICKER_MEDIA = false
31 | Pod::PICKER_AUDIO = false
32 |
33 | target 'Runner' do
34 | use_frameworks!
35 | use_modular_headers!
36 |
37 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
38 | pod 'SwiftyJSON', '~> 5.0'
39 | end
40 |
41 | target 'NebulaNetworkExtension' do
42 | use_frameworks!
43 | pod 'SwiftyJSON', '~> 5.0'
44 | end
45 |
46 | post_install do |installer|
47 | installer.generated_projects.each do |project|
48 | project.targets.each do |target|
49 | target.build_configurations.each do |config|
50 | if Gem::Version.new('12.0') > Gem::Version.new(config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'])
51 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
52 | end
53 | end
54 | end
55 | end
56 |
57 | installer.pods_project.targets.each do |target|
58 | flutter_additional_ios_build_settings(target)
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/swift-format.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | ##===----------------------------------------------------------------------===##
3 | ##
4 | ## This source file is part of the Swift.org open source project
5 | ##
6 | ## Copyright (c) 2024 Apple Inc. and the Swift project authors
7 | ## Licensed under Apache License v2.0 with Runtime Library Exception
8 | ##
9 | ## See https://swift.org/LICENSE.txt for license information
10 | ## See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
11 | ##
12 | ##===----------------------------------------------------------------------===##
13 |
14 | # Vendored from while is open.
15 |
16 | # This file has been modified to only check formatting, with no linting, and to require a `check` command flag to fail when formatting was performed.
17 |
18 | set -euo pipefail
19 |
20 | log() { printf -- "** %s\n" "$*" >&2; }
21 | error() { printf -- "** ERROR: %s\n" "$*" >&2; }
22 | fatal() { error "$@"; exit 1; }
23 |
24 |
25 | if [[ -f .swiftformatignore ]]; then
26 | log "Found swiftformatignore file..."
27 |
28 | log "Running swift format format..."
29 | tr '\n' '\0' < .swiftformatignore| xargs -0 -I% printf '":(exclude)%" '| xargs git ls-files -z '*.swift' | xargs -0 xcrun swift-format --parallel --recursive --in-place
30 |
31 | # log "Running swift format lint..."
32 |
33 | # tr '\n' '\0' < .swiftformatignore | xargs -0 -I% printf '":(exclude)%" '| xargs git ls-files -z '*.swift' | xargs -0 swift format lint --strict --parallel
34 | else
35 | log "Running swift format format..."
36 | git ls-files -z '*.swift' | xargs -0 xcrun swift-format --parallel --recursive --in-place
37 |
38 | # log "Running swift format lint..."
39 |
40 | # git ls-files -z '*.swift' | xargs -0 swift format lint --strict --parallel
41 | fi
42 |
43 |
44 | if [ "${1-default}" = "check" ]; then
45 | log "Checking for modified files..."
46 |
47 | GIT_PAGER='' git diff --exit-code '*.swift'
48 |
49 | log "✅ Found no formatting issues."
50 | fi
--------------------------------------------------------------------------------
/lib/screens/siteConfig/CipherScreen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter/widgets.dart';
4 | import 'package:mobile_nebula/components/FormPage.dart';
5 | import 'package:mobile_nebula/components/config/ConfigCheckboxItem.dart';
6 | import 'package:mobile_nebula/components/config/ConfigSection.dart';
7 |
8 | class CipherScreen extends StatefulWidget {
9 | const CipherScreen({super.key, required this.cipher, required this.onSave});
10 |
11 | final String cipher;
12 | final ValueChanged onSave;
13 |
14 | @override
15 | _CipherScreenState createState() => _CipherScreenState();
16 | }
17 |
18 | class _CipherScreenState extends State {
19 | late String cipher;
20 | bool changed = false;
21 |
22 | @override
23 | void initState() {
24 | cipher = widget.cipher;
25 | super.initState();
26 | }
27 |
28 | @override
29 | Widget build(BuildContext context) {
30 | return FormPage(
31 | title: 'Cipher Selection',
32 | changed: changed,
33 | onSave: () {
34 | Navigator.pop(context);
35 | widget.onSave(cipher);
36 | },
37 | child: Column(
38 | children: [
39 | ConfigSection(
40 | children: [
41 | ConfigCheckboxItem(
42 | label: Text("aes"),
43 | labelWidth: 150,
44 | checked: cipher == "aes",
45 | onChanged: () {
46 | setState(() {
47 | changed = true;
48 | cipher = "aes";
49 | });
50 | },
51 | ),
52 | ConfigCheckboxItem(
53 | label: Text("chachapoly"),
54 | labelWidth: 150,
55 | checked: cipher == "chachapoly",
56 | onChanged: () {
57 | setState(() {
58 | changed = true;
59 | cipher = "chachapoly";
60 | });
61 | },
62 | ),
63 | ],
64 | ),
65 | ],
66 | ),
67 | );
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/ios/NebulaNetworkExtension/Keychain.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let groupName = "group.net.defined.mobileNebula"
4 |
5 | class KeyChain {
6 | class func save(key: String, data: Data, managed: Bool) -> Bool {
7 | var query: [String: Any] = [
8 | kSecClass as String: kSecClassGenericPassword as String,
9 | kSecAttrAccount as String: key,
10 | kSecValueData as String: data,
11 | kSecAttrAccessGroup as String: groupName,
12 | ]
13 |
14 | if managed {
15 | query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
16 | }
17 |
18 | // Attempt to delete an existing key to allow for an overwrite
19 | _ = self.delete(key: key)
20 | return SecItemAdd(query as CFDictionary, nil) == 0
21 | }
22 |
23 | class func load(key: String) -> Data? {
24 | let query: [String: Any] = [
25 | kSecClass as String: kSecClassGenericPassword,
26 | kSecAttrAccount as String: key,
27 | kSecReturnData as String: kCFBooleanTrue!,
28 | kSecMatchLimit as String: kSecMatchLimitOne,
29 | kSecAttrAccessGroup as String: groupName,
30 | ]
31 |
32 | var dataTypeRef: AnyObject? = nil
33 |
34 | let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
35 |
36 | if status == noErr {
37 | return dataTypeRef as! Data?
38 | } else {
39 | return nil
40 | }
41 | }
42 |
43 | class func delete(key: String) -> Bool {
44 | let query: [String: Any] = [
45 | kSecClass as String: kSecClassGenericPassword as String,
46 | kSecAttrAccount as String: key,
47 | kSecAttrAccessGroup as String: groupName,
48 | ]
49 |
50 | return SecItemDelete(query as CFDictionary) == 0
51 | }
52 | }
53 |
54 | extension Data {
55 |
56 | init(from value: T) {
57 | var value = value
58 | var data = Data()
59 | withUnsafePointer(
60 | to: &value,
61 | { (ptr: UnsafePointer) -> Void in
62 | data = Data(buffer: UnsafeBufferPointer(start: ptr, count: 1))
63 | })
64 | self.init(data)
65 | }
66 |
67 | func to(type: T.Type) -> T {
68 | return self.withUnsafeBytes { $0.load(as: T.self) }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/net/defined/mobile_nebula/EncFile.kt:
--------------------------------------------------------------------------------
1 | package net.defined.mobile_nebula
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import androidx.security.crypto.EncryptedFile
6 | import androidx.security.crypto.MasterKeys
7 | import java.io.*
8 | import java.security.KeyStore
9 |
10 | class EncFile(private val context: Context) {
11 | companion object {
12 | // Borrowed from androidx.security.crypto.MasterKeys
13 | private const val ANDROID_KEYSTORE = "AndroidKeyStore"
14 |
15 | // Borrowed from androidx.security.crypto.EncryptedFile
16 | private const val KEYSET_PREF_NAME = "__androidx_security_crypto_encrypted_file_pref__"
17 | }
18 |
19 | private val scheme = EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
20 | private val spec = MasterKeys.AES256_GCM_SPEC
21 | private var master: String = MasterKeys.getOrCreate(spec)
22 |
23 | fun openRead(file: File): BufferedReader {
24 | // We may fail to decrypt the file, in which case we'll raise an exception.
25 | // Callers should handle this exception by deleting the invalid file.
26 | return build(file).openFileInput().bufferedReader()
27 | }
28 |
29 | fun openWrite(file: File): BufferedWriter {
30 | return try {
31 | build(file).openFileOutput().bufferedWriter()
32 | } catch (e: Exception) {
33 | // If we fail to open the file, it's likely because the master key no longer works.
34 | // We'll try to reset the master key and try again.
35 | resetMasterKey()
36 |
37 | build(file).openFileOutput().bufferedWriter()
38 | }
39 | }
40 |
41 | private fun build(file: File): EncryptedFile {
42 | return EncryptedFile.Builder(file, context, master, scheme).build()
43 | }
44 |
45 | fun resetMasterKey() {
46 | // Reset the master key
47 | KeyStore.getInstance(ANDROID_KEYSTORE).apply {
48 | load(null)
49 | deleteEntry(master)
50 | }
51 | // And reset the shared preference containing the file encryption key
52 | context.deleteSharedPreferences(KEYSET_PREF_NAME)
53 |
54 | // Re-create the master key now so future calls don't fail
55 | master = MasterKeys.getOrCreate(spec)
56 | }
57 | }
--------------------------------------------------------------------------------
/lib/screens/LicensesScreen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
4 | import 'package:mobile_nebula/components/SimplePage.dart';
5 | import 'package:mobile_nebula/services/utils.dart';
6 | import '../../oss_licenses.dart';
7 |
8 | String capitalize(String input) {
9 | return input[0].toUpperCase() + input.substring(1);
10 | }
11 |
12 | class LicensesScreen extends StatelessWidget {
13 | const LicensesScreen({super.key});
14 |
15 | @override
16 | Widget build(BuildContext context) {
17 | return SimplePage(
18 | title: const Text("Licences"),
19 | scrollable: SimpleScrollable.none,
20 | child: ListView.builder(
21 | itemCount: allDependencies.length,
22 | itemBuilder: (_, index) {
23 | var dep = allDependencies[index];
24 | return Padding(
25 | padding: const EdgeInsets.all(8),
26 | child: PlatformListTile(
27 | onTap: () {
28 | Utils.openPage(context, (_) => LicenceDetailPage(title: capitalize(dep.name), licence: dep.license!));
29 | },
30 | title: Text(capitalize(dep.name)),
31 | subtitle: Text(dep.description),
32 | trailing: Icon(context.platformIcons.forward, size: 18),
33 | ),
34 | );
35 | },
36 | ),
37 | );
38 | }
39 | }
40 |
41 | //detail page for the licence
42 | class LicenceDetailPage extends StatelessWidget {
43 | final String title, licence;
44 | const LicenceDetailPage({super.key, required this.title, required this.licence});
45 |
46 | @override
47 | Widget build(BuildContext context) {
48 | return SimplePage(
49 | title: Text(title),
50 | scrollable: SimpleScrollable.none,
51 | child: Padding(
52 | padding: const EdgeInsets.all(8.0),
53 | child: Container(
54 | padding: const EdgeInsets.all(5),
55 | decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
56 | child: SingleChildScrollView(
57 | physics: const BouncingScrollPhysics(),
58 | child: Column(children: [Text(licence, style: const TextStyle(fontSize: 15))]),
59 | ),
60 | ),
61 | ),
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CADisableMinimumFrameDurationOnPhone
6 |
7 | CFBundleDevelopmentRegion
8 | $(DEVELOPMENT_LANGUAGE)
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | Nebula
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleSignature
22 | ????
23 | CFBundleURLTypes
24 |
25 |
26 | CFBundleTypeRole
27 | Viewer
28 | CFBundleURLName
29 | mailto
30 | CFBundleURLSchemes
31 |
32 | mailto
33 |
34 |
35 |
36 | CFBundleVersion
37 | $(CURRENT_PROJECT_VERSION)
38 | ITSAppUsesNonExemptEncryption
39 |
40 | LSRequiresIPhoneOS
41 |
42 | NSCameraUsageDescription
43 | Camera permission is required for qr code scanning.
44 | UILaunchStoryboardName
45 | LaunchScreen
46 | UIMainStoryboardFile
47 | Main
48 | UISupportedInterfaceOrientations
49 |
50 | UIInterfaceOrientationPortrait
51 | UIInterfaceOrientationLandscapeLeft
52 | UIInterfaceOrientationLandscapeRight
53 |
54 | UISupportedInterfaceOrientations~ipad
55 |
56 | UIInterfaceOrientationPortrait
57 | UIInterfaceOrientationPortraitUpsideDown
58 | UIInterfaceOrientationLandscapeLeft
59 | UIInterfaceOrientationLandscapeRight
60 |
61 | UIViewControllerBasedStatusBarAppearance
62 |
63 | UIApplicationSupportsIndirectInputEvents
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/lib/services/share.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:io';
3 |
4 | import 'package:flutter/widgets.dart';
5 | import 'package:path_provider/path_provider.dart';
6 | import 'package:share_plus/share_plus.dart' as sp;
7 | import 'package:path/path.dart' as p;
8 |
9 | class Share {
10 | /// Transforms a string of text into a file and shares that file
11 | /// - title: Title of message or subject if sending an email
12 | /// - text: The text to share
13 | /// - filename: The filename to use if sending over airdrop for example
14 | static Future share(
15 | BuildContext context, {
16 | required String title,
17 | required String text,
18 | required String filename,
19 | }) async {
20 | assert(title.isNotEmpty);
21 | assert(text.isNotEmpty);
22 | assert(filename.isNotEmpty);
23 |
24 | final tmpDir = await getTemporaryDirectory();
25 | final file = File(p.join(tmpDir.path, filename));
26 | var res = false;
27 |
28 | try {
29 | file.writeAsStringSync(text, flush: true);
30 | res = await Share.shareFile(context, title: title, filePath: file.path);
31 | } catch (err) {
32 | // Ignoring file write errors
33 | }
34 |
35 | file.delete();
36 | return res;
37 | }
38 |
39 | /// Shares a local file
40 | /// - title: Title of message or subject if sending an email
41 | /// - filePath: Path to the file to share
42 | /// - filename: An optional filename to override the existing file
43 | static Future shareFile(
44 | BuildContext context, {
45 | required String title,
46 | required String filePath,
47 | String? filename,
48 | }) async {
49 | assert(title.isNotEmpty);
50 | assert(filePath.isNotEmpty);
51 |
52 | final box = context.findRenderObject() as RenderBox?;
53 |
54 | //NOTE: the filename used to specify the name of the file in gmail/slack/etc but no longer works that way
55 | // If we want to support that again we will need to save the file to a temporary directory, share that,
56 | // and then delete it
57 | final xFile = sp.XFile(filePath, name: filename);
58 | final result = await sp.Share.shareXFiles(
59 | [xFile],
60 | subject: title,
61 | sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
62 | );
63 | return result.status == sp.ShareResultStatus.success;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/lib/components/SiteItem.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter_svg/svg.dart';
4 | import 'package:mobile_nebula/components/SpecialButton.dart';
5 | import 'package:mobile_nebula/models/Site.dart';
6 | import 'package:mobile_nebula/services/utils.dart';
7 |
8 | class SiteItem extends StatelessWidget {
9 | const SiteItem({super.key, required this.site, this.onPressed});
10 |
11 | final Site site;
12 | final void Function()? onPressed;
13 |
14 | @override
15 | Widget build(BuildContext context) {
16 | final borderColor =
17 | site.errors.isNotEmpty
18 | ? CupertinoColors.systemRed.resolveFrom(context)
19 | : site.connected
20 | ? CupertinoColors.systemGreen.resolveFrom(context)
21 | : CupertinoColors.systemGrey2.resolveFrom(context);
22 | final border = BorderSide(color: borderColor, width: 10);
23 |
24 | return Container(
25 | margin: EdgeInsets.symmetric(vertical: 6),
26 | decoration: BoxDecoration(border: Border(left: border)),
27 | child: _buildContent(context),
28 | );
29 | }
30 |
31 | Widget _buildContent(BuildContext context) {
32 | final border = BorderSide(color: Utils.configSectionBorder(context));
33 | final dnIcon =
34 | Theme.of(context).brightness == Brightness.dark ? 'images/dn-logo-dark.svg' : 'images/dn-logo-light.svg';
35 |
36 | return SpecialButton(
37 | decoration: BoxDecoration(
38 | border: Border(top: border, bottom: border),
39 | color: Utils.configItemBackground(context),
40 | ),
41 | onPressed: onPressed,
42 | child: Padding(
43 | padding: EdgeInsets.fromLTRB(10, 10, 5, 10),
44 | child: Row(
45 | crossAxisAlignment: CrossAxisAlignment.center,
46 | children: [
47 | site.managed
48 | ? Padding(padding: EdgeInsets.only(right: 10), child: SvgPicture.asset(dnIcon, width: 12))
49 | : Container(),
50 | Expanded(child: Text(site.name, style: TextStyle(fontWeight: FontWeight.bold))),
51 | Padding(padding: EdgeInsets.only(right: 10)),
52 | Icon(CupertinoIcons.forward, color: CupertinoColors.placeholderText.resolveFrom(context), size: 18),
53 | ],
54 | ),
55 | ),
56 | );
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/lib/components/config/ConfigPageItem.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter/cupertino.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:mobile_nebula/components/SpecialButton.dart';
6 | import 'package:mobile_nebula/services/utils.dart';
7 |
8 | class ConfigPageItem extends StatelessWidget {
9 | const ConfigPageItem({
10 | super.key,
11 | this.label,
12 | this.content,
13 | this.labelWidth = 100,
14 | this.onPressed,
15 | this.disabled = false,
16 | this.crossAxisAlignment = CrossAxisAlignment.center,
17 | });
18 |
19 | final Widget? label;
20 | final Widget? content;
21 | final double labelWidth;
22 | final CrossAxisAlignment crossAxisAlignment;
23 | final void Function()? onPressed;
24 | final bool disabled;
25 |
26 | @override
27 | Widget build(BuildContext context) {
28 | dynamic theme;
29 |
30 | if (Platform.isAndroid) {
31 | final origTheme = Theme.of(context);
32 | theme = origTheme.copyWith(
33 | textTheme: origTheme.textTheme.copyWith(
34 | labelLarge: origTheme.textTheme.labelLarge!.copyWith(fontWeight: FontWeight.normal),
35 | ),
36 | );
37 | return Theme(data: theme, child: _buildContent(context));
38 | } else {
39 | final origTheme = CupertinoTheme.of(context);
40 | theme = origTheme.copyWith(primaryColor: CupertinoColors.label.resolveFrom(context));
41 | return CupertinoTheme(data: theme, child: _buildContent(context));
42 | }
43 | }
44 |
45 | Widget _buildContent(BuildContext context) {
46 | return SpecialButton(
47 | onPressed: disabled ? null : onPressed,
48 | color: Utils.configItemBackground(context),
49 | child: Container(
50 | padding: EdgeInsets.symmetric(vertical: 6, horizontal: 15),
51 | constraints: BoxConstraints(minHeight: Utils.minInteractiveSize, minWidth: double.infinity),
52 | child: Row(
53 | crossAxisAlignment: crossAxisAlignment,
54 | children: [
55 | label != null ? SizedBox(width: labelWidth, child: label) : Container(),
56 | Expanded(child: Container(padding: EdgeInsets.only(right: 10), child: content)),
57 | disabled
58 | ? Container()
59 | : Icon(CupertinoIcons.forward, color: CupertinoColors.placeholderText.resolveFrom(context), size: 18),
60 | ],
61 | ),
62 | ),
63 | );
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/lib/models/HostInfo.dart:
--------------------------------------------------------------------------------
1 | import 'package:mobile_nebula/models/Certificate.dart';
2 |
3 | class HostInfo {
4 | List vpnAddrs;
5 | int localIndex;
6 | int remoteIndex;
7 | List remoteAddresses;
8 | Certificate? cert;
9 | UDPAddress? currentRemote;
10 | int messageCounter;
11 |
12 | HostInfo({
13 | required this.vpnAddrs,
14 | required this.localIndex,
15 | required this.remoteIndex,
16 | required this.remoteAddresses,
17 | required this.messageCounter,
18 | this.cert,
19 | this.currentRemote,
20 | });
21 |
22 | factory HostInfo.fromJson(Map json) {
23 | UDPAddress? currentRemote;
24 | if (json['currentRemote'] != "") {
25 | currentRemote = UDPAddress.fromJson(json['currentRemote']);
26 | }
27 |
28 | Certificate? cert;
29 | if (json['cert'] != null) {
30 | cert = Certificate.fromJson(json['cert']);
31 | }
32 |
33 | List? addrs = json['remoteAddrs'];
34 | List remoteAddresses = [];
35 | addrs?.forEach((val) {
36 | remoteAddresses.add(UDPAddress.fromJson(val));
37 | });
38 |
39 | addrs = json['vpnAddrs'];
40 | List vpnAddrs = [];
41 | addrs?.forEach((val) {
42 | if (val is String) {
43 | vpnAddrs.add(val);
44 | }
45 | });
46 |
47 | return HostInfo(
48 | vpnAddrs: vpnAddrs,
49 | localIndex: json['localIndex'],
50 | remoteIndex: json['remoteIndex'],
51 | remoteAddresses: remoteAddresses,
52 | messageCounter: json['messageCounter'],
53 | cert: cert,
54 | currentRemote: currentRemote,
55 | );
56 | }
57 | }
58 |
59 | class UDPAddress {
60 | String ip;
61 | int port;
62 |
63 | UDPAddress({required this.ip, required this.port});
64 |
65 | @override
66 | String toString() {
67 | // Simple check on if nebula told us about a v4 or v6 ip address
68 | if (ip.contains(':')) {
69 | return '[$ip]:$port';
70 | }
71 |
72 | return '$ip:$port';
73 | }
74 |
75 | factory UDPAddress.fromJson(String json) {
76 | // IPv4 Address
77 | if (json.contains('.')) {
78 | var ip = json.split(':')[0];
79 | var port = int.parse(json.split(':')[1]);
80 | return UDPAddress(ip: ip, port: port);
81 | }
82 |
83 | // IPv6 Address
84 | var ip = json.split(']')[0].substring(1);
85 | var port = int.parse(json.split(']')[1].split(':')[1]);
86 | return UDPAddress(ip: ip, port: port);
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/nebula/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/DefinedNet/mobile_nebula/nebula
2 |
3 | go 1.25
4 |
5 | // replace github.com/slackhq/nebula => /Volumes/T7/nate/src/github.com/slackhq/nebula
6 |
7 | require (
8 | github.com/DefinedNet/dnapi v0.0.0-20251210211559-8ae1e6743199
9 | github.com/sirupsen/logrus v1.9.3
10 | github.com/slackhq/nebula v1.10.1-0.20251210163936-3ec527e42cec
11 | golang.org/x/crypto v0.46.0
12 | gopkg.in/yaml.v2 v2.4.0
13 | )
14 |
15 | require (
16 | dario.cat/mergo v1.0.2 // indirect
17 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
18 | github.com/armon/go-radix v1.0.0 // indirect
19 | github.com/beorn7/perks v1.0.1 // indirect
20 | github.com/cespare/xxhash/v2 v2.3.0 // indirect
21 | github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432 // indirect
22 | github.com/flynn/noise v1.1.0 // indirect
23 | github.com/gaissmai/bart v0.26.0 // indirect
24 | github.com/gogo/protobuf v1.3.2 // indirect
25 | github.com/google/gopacket v1.1.19 // indirect
26 | github.com/miekg/dns v1.1.68 // indirect
27 | github.com/miekg/pkcs11 v1.1.2-0.20231115102856-9078ad6b9d4b // indirect
28 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
29 | github.com/nbrownus/go-metrics-prometheus v0.0.0-20210712211119-974a6260965f // indirect
30 | github.com/prometheus/client_golang v1.23.2 // indirect
31 | github.com/prometheus/client_model v0.6.2 // indirect
32 | github.com/prometheus/common v0.67.2 // indirect
33 | github.com/prometheus/procfs v0.19.1 // indirect
34 | github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
35 | github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect
36 | github.com/vishvananda/netlink v1.3.1 // indirect
37 | github.com/vishvananda/netns v0.0.5 // indirect
38 | go.yaml.in/yaml/v2 v2.4.3 // indirect
39 | go.yaml.in/yaml/v3 v3.0.4 // indirect
40 | golang.org/x/mobile v0.0.0-20251209145715-2553ed8ce294 // indirect
41 | golang.org/x/mod v0.31.0 // indirect
42 | golang.org/x/net v0.48.0 // indirect
43 | golang.org/x/sync v0.19.0 // indirect
44 | golang.org/x/sys v0.39.0 // indirect
45 | golang.org/x/term v0.38.0 // indirect
46 | golang.org/x/tools v0.40.0 // indirect
47 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
48 | golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
49 | golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
50 | google.golang.org/protobuf v1.36.10 // indirect
51 | )
52 |
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Mobile Nebula
2 |
3 | [Play Store](https://play.google.com/store/apps/details?id=net.defined.mobile_nebula&hl=en_US&gl=US) | [App Store](https://apps.apple.com/us/app/mobile-nebula/id1509587936)
4 |
5 | ## Setting up dev environment
6 |
7 | Install all of the following things:
8 |
9 | - [`xcode`](https://apps.apple.com/us/app/xcode/) - use the version specified by `xcode_select` in `/ios/fastlane/Fastfile`
10 | - [`android-studio`](https://developer.android.com/studio)
11 | - [`flutter` 3.29.2](https://docs.flutter.dev/get-started/install)
12 | - [`gomobile`](https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile)
13 | - [Flutter Android Studio Extension](https://docs.flutter.dev/get-started/editor?tab=androidstudio)
14 |
15 | Ensure your path is set up correctly to execute flutter
16 |
17 | Run `flutter doctor` and fix everything it complains before proceeding
18 |
19 | *NOTE* on iOS, always open `Runner.xcworkspace` and NOT the `Runner.xccodeproj`
20 |
21 | ### Before first compile
22 |
23 | - Copy `env.sh.example` and set it up for your machine
24 | - Ensure you have run `gomobile init`
25 | - In Android Studio, make sure you have the current ndk installed by going to Tools -> SDK Manager, go to the SDK Tools tab, check the `Show package details` box, expand the NDK section and select `28.2.13676358` version.
26 | - Ensure you have downloaded an ndk via android studio, this is likely not the default one and you need to check the
27 | `Show package details` box to select the correct version. The correct version comes from the error when you try and compile
28 | - Make sure you have `gem` installed with `sudo gem install`
29 | - If on MacOS arm, `sudo gem install ffi -- --enable-libffi-alloc`
30 |
31 | If you are having issues with iOS pods, try blowing it all away! `cd ios && rm -rf Pods/ Podfile.lock && pod install --repo-update`
32 |
33 | # Formatting
34 |
35 | `dart format` can be used to format the code in `lib` and `test`. We use a line-length of 120 characters.
36 |
37 | Use:
38 | ```sh
39 | dart format lib/ test/ -l 120
40 | ```
41 |
42 | In Android Studio, set the line length using Preferences -> Editor -> Code Style -> Dart -> Line length, set it to 120. Enable auto-format with Preferences -> Languages & Frameworks -> Flutter -> Format code on save.
43 |
44 | `./swift-format.sh` can be used to format Swift code in the repo.
45 |
46 | Once `swift-format` supports ignoring directories (), we can move to a method of running it more like what describes.
--------------------------------------------------------------------------------
/ios/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - file_picker (0.0.1):
3 | - Flutter
4 | - Flutter (1.0.0)
5 | - mobile_scanner (7.0.0):
6 | - Flutter
7 | - FlutterMacOS
8 | - package_info_plus (0.4.5):
9 | - Flutter
10 | - path_provider_foundation (0.0.1):
11 | - Flutter
12 | - FlutterMacOS
13 | - Sentry/HybridSDK (8.46.0)
14 | - sentry_flutter (8.14.2):
15 | - Flutter
16 | - FlutterMacOS
17 | - Sentry/HybridSDK (= 8.46.0)
18 | - share_plus (0.0.1):
19 | - Flutter
20 | - SwiftyJSON (5.0.2)
21 | - url_launcher_ios (0.0.1):
22 | - Flutter
23 |
24 | DEPENDENCIES:
25 | - file_picker (from `.symlinks/plugins/file_picker/ios`)
26 | - Flutter (from `Flutter`)
27 | - mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
28 | - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
29 | - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
30 | - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
31 | - share_plus (from `.symlinks/plugins/share_plus/ios`)
32 | - SwiftyJSON (~> 5.0)
33 | - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
34 |
35 | SPEC REPOS:
36 | trunk:
37 | - Sentry
38 | - SwiftyJSON
39 |
40 | EXTERNAL SOURCES:
41 | file_picker:
42 | :path: ".symlinks/plugins/file_picker/ios"
43 | Flutter:
44 | :path: Flutter
45 | mobile_scanner:
46 | :path: ".symlinks/plugins/mobile_scanner/darwin"
47 | package_info_plus:
48 | :path: ".symlinks/plugins/package_info_plus/ios"
49 | path_provider_foundation:
50 | :path: ".symlinks/plugins/path_provider_foundation/darwin"
51 | sentry_flutter:
52 | :path: ".symlinks/plugins/sentry_flutter/ios"
53 | share_plus:
54 | :path: ".symlinks/plugins/share_plus/ios"
55 | url_launcher_ios:
56 | :path: ".symlinks/plugins/url_launcher_ios/ios"
57 |
58 | SPEC CHECKSUMS:
59 | file_picker: c79185e70b9b45728cde2a8d8da454e0cb43f287
60 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
61 | mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e
62 | package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
63 | path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
64 | Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854
65 | sentry_flutter: 2df8b0aab7e4aba81261c230cbea31c82a62dd1b
66 | share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
67 | SwiftyJSON: f5b1bf1cd8dd53cd25887ac0eabcfd92301c6a5a
68 | url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
69 |
70 | PODFILE CHECKSUM: b44d9de9944d89118a4ff4bfffe1c2dab91de156
71 |
72 | COCOAPODS: 1.16.2
73 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id "com.android.application"
3 | id "kotlin-android"
4 | id "dev.flutter.flutter-gradle-plugin"
5 | }
6 |
7 | def localProperties = new Properties()
8 | def localPropertiesFile = rootProject.file('local.properties')
9 | if (localPropertiesFile.exists()) {
10 | localPropertiesFile.withReader('UTF-8') { reader ->
11 | localProperties.load(reader)
12 | }
13 | }
14 |
15 |
16 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
17 | if (flutterVersionCode == null) {
18 | flutterVersionCode = '1'
19 | }
20 |
21 | def flutterVersionName = localProperties.getProperty('flutter.versionName')
22 | if (flutterVersionName == null) {
23 | flutterVersionName = '1.0'
24 | }
25 |
26 | android {
27 | namespace "net.defined.mobile_nebula"
28 |
29 | compileSdkVersion 36
30 |
31 | // default ndk version for AGP 8.7: https://developer.android.com/build/releases/past-releases/agp-8-7-0-release-notes
32 | ndkVersion "28.2.13676358"
33 |
34 | sourceSets {
35 | main.java.srcDirs += 'src/main/kotlin'
36 | }
37 |
38 | defaultConfig {
39 | applicationId "net.defined.mobile_nebula"
40 | minSdkVersion 26 //flutter.minSdkVersion
41 | targetSdkVersion 36 //flutter.targetSdkVersion
42 | versionCode flutterVersionCode.toInteger()
43 | versionName flutterVersionName
44 | }
45 |
46 | signingConfigs {
47 | release {
48 | keyAlias 'key'
49 | storeFile System.getenv('GOOGLE_PLAY_KEYSTORE_PATH') ? file(System.getenv('GOOGLE_PLAY_KEYSTORE_PATH')) : null
50 | keyPassword System.getenv('GOOGLE_PLAY_KEYSTORE_PASSWORD')
51 | storePassword System.getenv('GOOGLE_PLAY_KEYSTORE_PASSWORD')
52 | }
53 | }
54 |
55 | buildTypes {
56 | release {
57 | signingConfig signingConfigs.release
58 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
59 | resValue 'string', 'app_name', '"Nebula"'
60 | }
61 |
62 | debug {
63 | resValue 'string', 'app_name', '"Nebula-DEBUG"'
64 | applicationIdSuffix '.debug'
65 | }
66 | }
67 | }
68 |
69 | java {
70 | toolchain {
71 | languageVersion = JavaLanguageVersion.of(17)
72 | }
73 | }
74 |
75 | flutter {
76 | source '../..'
77 | }
78 |
79 | dependencies {
80 | def workVersion = "2.9.1"
81 | implementation "androidx.security:security-crypto:1.0.0"
82 | implementation "androidx.work:work-runtime-ktx:$workVersion"
83 | implementation 'com.google.code.gson:gson:2.11.0'
84 | implementation "com.google.guava:guava:31.0.1-android"
85 | implementation project(':mobileNebula')
86 |
87 | }
88 |
89 |
--------------------------------------------------------------------------------
/lib/services/settings.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:convert';
3 | import 'package:flutter/cupertino.dart';
4 | import 'package:flutter/scheduler.dart';
5 | import 'package:mobile_nebula/services/storage.dart';
6 | import 'package:sentry_flutter/sentry_flutter.dart';
7 |
8 | bool DEFAULT_LOG_WRAP = false;
9 | bool DEFAULT_TRACK_ERRORS = true;
10 |
11 | class Settings {
12 | final _storage = Storage();
13 | final StreamController _change = StreamController.broadcast();
14 | var _settings = {};
15 |
16 | bool get useSystemColors {
17 | return _getBool('systemDarkMode', true);
18 | }
19 |
20 | set useSystemColors(bool enabled) {
21 | if (!enabled) {
22 | // Clear the dark mode to let the default system config take over, user can override from there
23 | _settings.remove('darkMode');
24 | }
25 | _set('systemDarkMode', enabled);
26 | }
27 |
28 | bool get darkMode {
29 | return _getBool('darkMode', SchedulerBinding.instance.platformDispatcher.platformBrightness == Brightness.dark);
30 | }
31 |
32 | set darkMode(bool enabled) {
33 | _set('darkMode', enabled);
34 | }
35 |
36 | bool get logWrap {
37 | return _getBool('logWrap', DEFAULT_LOG_WRAP);
38 | }
39 |
40 | set logWrap(bool enabled) {
41 | _set('logWrap', enabled);
42 | }
43 |
44 | bool get trackErrors {
45 | return _getBool('trackErrors', DEFAULT_TRACK_ERRORS);
46 | }
47 |
48 | set trackErrors(bool enabled) {
49 | _set('trackErrors', enabled);
50 |
51 | // Side-effect: Disable Sentry immediately
52 | if (!enabled) {
53 | Sentry.close();
54 | }
55 | }
56 |
57 | String _getString(String key, String defaultValue) {
58 | final val = _settings[key];
59 | if (val is String) {
60 | return val;
61 | }
62 | return defaultValue;
63 | }
64 |
65 | bool _getBool(String key, bool defaultValue) {
66 | final val = _settings[key];
67 | if (val is bool) {
68 | return val;
69 | }
70 | return defaultValue;
71 | }
72 |
73 | void _set(String key, dynamic value) {
74 | _settings[key] = value;
75 | _save();
76 | }
77 |
78 | Stream onChange() {
79 | return _change.stream;
80 | }
81 |
82 | void _save() {
83 | final content = jsonEncode(_settings);
84 | //TODO: handle errors
85 | _storage.writeFile("config.json", content).then((_) {
86 | _change.add(null);
87 | });
88 | }
89 |
90 | static final Settings _instance = Settings._internal();
91 |
92 | factory Settings() {
93 | return _instance;
94 | }
95 |
96 | Settings._internal() {
97 | _storage.readFile("config.json").then((rawConfig) {
98 | if (rawConfig != null) {
99 | _settings = jsonDecode(rawConfig);
100 | }
101 |
102 | _change.add(null);
103 | });
104 | }
105 |
106 | void dispose() {
107 | _change.close();
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/ios/fastlane/Fastfile:
--------------------------------------------------------------------------------
1 | # This file contains the fastlane.tools configuration
2 | # You can find the documentation at https://docs.fastlane.tools
3 | #
4 | # For a list of all available actions, check out
5 | #
6 | # https://docs.fastlane.tools/actions
7 | #
8 | # For a list of all available plugins, check out
9 | #
10 | # https://docs.fastlane.tools/plugins/available-plugins
11 | #
12 |
13 | # Uncomment the line if you want fastlane to automatically update itself
14 | # update_fastlane
15 |
16 | default_platform(:ios)
17 |
18 | platform :ios do
19 | desc "Push a new beta build to TestFlight"
20 |
21 | before_all do
22 | xcode_select("/Applications/Xcode_26.0.1.app")
23 | end
24 |
25 |
26 | lane :build do
27 | # Do some things like setting up a temporary keystore to host secrets in CI
28 | setup_ci
29 |
30 | # # Authenticate with Apple app store connect
31 | # app_store_connect_api_key
32 |
33 | # Change signing behavior to work in CI
34 | update_code_signing_settings(
35 | # Automatic signing seems to be a good thing to have on in dev but will not work in CI
36 | use_automatic_signing: false,
37 | # The default value for this is iOS Development which is not appropriate for release
38 | code_sign_identity: "Apple Distribution",
39 | )
40 |
41 | # Find our signing certs and profiles, these come from a private repository and managed by `fastlane match`
42 | match(type: 'appstore', app_identifier: ["net.defined.mobileNebula","net.defined.mobileNebula.NebulaNetworkExtension"], readonly: true)
43 |
44 | # Update our main program to have the correct provisioning profile from Apple
45 | update_project_provisioning(
46 | xcodeproj: "Runner.xcodeproj",
47 | target_filter: "Runner",
48 | # This comes from match() above
49 | profile:ENV["sigh_net.defined.mobileNebula_appstore_profile-path"],
50 | build_configuration: "Release"
51 | )
52 |
53 | # Update our network extension to have the correct provisioning profile from Apple
54 | update_project_provisioning(
55 | xcodeproj: "Runner.xcodeproj",
56 | target_filter: "NebulaNetworkExtension",
57 | # This comes from match() above
58 | profile:ENV["sigh_net.defined.mobileNebula.NebulaNetworkExtension_appstore_profile-path"],
59 | build_configuration: "Release"
60 | )
61 |
62 | increment_build_number(
63 | xcodeproj: "Runner.xcodeproj",
64 | build_number: ENV['BUILD_NUMBER']
65 | )
66 |
67 | increment_version_number(
68 | xcodeproj: "Runner.xcodeproj",
69 | version_number: ENV['BUILD_NAME']
70 | )
71 |
72 | build_app(
73 | output_name: "MobileNebula.ipa",
74 | workspace: "Runner.xcworkspace",
75 | scheme: "Runner",
76 | export_method: "app-store",
77 | )
78 | end
79 |
80 | lane :release do
81 | # Do some things like setting up a temporary keystore to host secrets in CI
82 | setup_ci
83 |
84 | # Authenticate with Apple app store connect
85 | app_store_connect_api_key
86 |
87 | upload_to_testflight(skip_waiting_for_build_processing: true)
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "nebula icon white@2x-20@2x.png",
5 | "idiom" : "iphone",
6 | "scale" : "2x",
7 | "size" : "20x20"
8 | },
9 | {
10 | "filename" : "nebula icon white@2x-20@3x.png",
11 | "idiom" : "iphone",
12 | "scale" : "3x",
13 | "size" : "20x20"
14 | },
15 | {
16 | "filename" : "nebula icon white@2x-29@2x.png",
17 | "idiom" : "iphone",
18 | "scale" : "2x",
19 | "size" : "29x29"
20 | },
21 | {
22 | "filename" : "nebula icon white@2x-29@3x.png",
23 | "idiom" : "iphone",
24 | "scale" : "3x",
25 | "size" : "29x29"
26 | },
27 | {
28 | "filename" : "nebula icon white@2x-40@2x.png",
29 | "idiom" : "iphone",
30 | "scale" : "2x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "filename" : "nebula icon white@2x-40@3x.png",
35 | "idiom" : "iphone",
36 | "scale" : "3x",
37 | "size" : "40x40"
38 | },
39 | {
40 | "filename" : "nebula icon white@2x-60@2x.png",
41 | "idiom" : "iphone",
42 | "scale" : "2x",
43 | "size" : "60x60"
44 | },
45 | {
46 | "filename" : "nebula icon white@2x-60@3x.png",
47 | "idiom" : "iphone",
48 | "scale" : "3x",
49 | "size" : "60x60"
50 | },
51 | {
52 | "filename" : "nebula icon white@2x-20.png",
53 | "idiom" : "ipad",
54 | "scale" : "1x",
55 | "size" : "20x20"
56 | },
57 | {
58 | "filename" : "nebula icon white@2x-20@2x.png",
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "20x20"
62 | },
63 | {
64 | "filename" : "nebula icon white@2x-29.png",
65 | "idiom" : "ipad",
66 | "scale" : "1x",
67 | "size" : "29x29"
68 | },
69 | {
70 | "filename" : "nebula icon white@2x-29@2x.png",
71 | "idiom" : "ipad",
72 | "scale" : "2x",
73 | "size" : "29x29"
74 | },
75 | {
76 | "filename" : "nebula icon white@2x-40.png",
77 | "idiom" : "ipad",
78 | "scale" : "1x",
79 | "size" : "40x40"
80 | },
81 | {
82 | "filename" : "nebula icon white@2x-40@2x.png",
83 | "idiom" : "ipad",
84 | "scale" : "2x",
85 | "size" : "40x40"
86 | },
87 | {
88 | "filename" : "nebula icon white@2x-76.png",
89 | "idiom" : "ipad",
90 | "scale" : "1x",
91 | "size" : "76x76"
92 | },
93 | {
94 | "filename" : "nebula icon white@2x-76@2x.png",
95 | "idiom" : "ipad",
96 | "scale" : "2x",
97 | "size" : "76x76"
98 | },
99 | {
100 | "filename" : "nebula icon white@2x-83.5@2x.png",
101 | "idiom" : "ipad",
102 | "scale" : "2x",
103 | "size" : "83.5x83.5"
104 | },
105 | {
106 | "filename" : "nebula icon white@2x-1024.png",
107 | "idiom" : "ios-marketing",
108 | "scale" : "1x",
109 | "size" : "1024x1024"
110 | }
111 | ],
112 | "info" : {
113 | "author" : "xcode",
114 | "version" : 1
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/lib/components/FormPage.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/widgets.dart';
3 | import 'package:mobile_nebula/components/SimplePage.dart';
4 | import 'package:mobile_nebula/services/utils.dart';
5 |
6 | /// SimplePage with a form and built in validation and confirmation to discard changes if any are made
7 | class FormPage extends StatefulWidget {
8 | const FormPage({
9 | super.key,
10 | required this.title,
11 | required this.child,
12 | required this.onSave,
13 | required this.changed,
14 | this.hideSave = false,
15 | this.scrollController,
16 | });
17 |
18 | final String title;
19 | final Function onSave;
20 | final Widget child;
21 | final ScrollController? scrollController;
22 |
23 | /// If you need the page to progress to a certain point before saving, control it here
24 | final bool hideSave;
25 |
26 | /// Useful if you have a non form field that can change, overrides the internal changed state if true
27 | final bool changed;
28 |
29 | @override
30 | _FormPageState createState() => _FormPageState();
31 | }
32 |
33 | class _FormPageState extends State {
34 | var changed = false;
35 | final _formKey = GlobalKey();
36 |
37 | @override
38 | Widget build(BuildContext context) {
39 | changed = widget.changed || changed;
40 |
41 | return PopScope