limit adding gateway to StartTunnel, better copy around Tor SSL (#3033)

* limit adding gateway to StartTunnel, better copy around Tor SSL

* properly differentiate ssl

* exclude disconnected gateways

* better error handling

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Matt Hill
2025-09-24 13:22:26 -06:00
committed by GitHub
parent bc62de795e
commit 6f1900f3bb
9 changed files with 91 additions and 83 deletions

View File

@@ -135,11 +135,12 @@ impl BindInfo {
}
impl InterfaceFilter for NetInfo {
fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool {
if info.public() {
self.public_enabled.contains(id)
} else {
!self.private_disabled.contains(id)
}
info.ip_info.is_some()
&& if info.public() {
self.public_enabled.contains(id)
} else {
!self.private_disabled.contains(id)
}
}
}

View File

@@ -87,7 +87,10 @@ type FormatReturnTy<
export type Filled = {
hostnames: HostnameInfo[]
toUrl: (h: HostnameInfo) => UrlString[]
toUrls: (h: HostnameInfo) => {
url: UrlString | null
sslUrl: UrlString | null
}
filter: <F extends Filter, Format extends Formats = "urlstring">(
filter: F,
@@ -139,7 +142,7 @@ const unique = <A>(values: A[]) => Array.from(new Set(values))
export const addressHostToUrl = (
{ scheme, sslScheme, username, suffix }: AddressInfo,
hostname: HostnameInfo,
): UrlString[] => {
): { url: UrlString | null; sslUrl: UrlString | null } => {
const res = []
const fmt = (scheme: string | null, host: HostnameInfo, port: number) => {
const excludePort =
@@ -164,14 +167,16 @@ export const addressHostToUrl = (
username ? `${username}@` : ""
}${hostname}${excludePort ? "" : `:${port}`}${suffix}`
}
let url = null
if (hostname.hostname.sslPort !== null) {
res.push(fmt(sslScheme, hostname, hostname.hostname.sslPort))
url = fmt(sslScheme, hostname, hostname.hostname.sslPort)
}
let sslUrl = null
if (hostname.hostname.port !== null) {
res.push(fmt(scheme, hostname, hostname.hostname.port))
sslUrl = fmt(scheme, hostname, hostname.hostname.port)
}
return res
return { url, sslUrl }
}
function filterRec(
@@ -223,13 +228,17 @@ export const filledAddress = (
host: Host,
addressInfo: AddressInfo,
): FilledAddressInfo => {
const toUrl = addressHostToUrl.bind(null, addressInfo)
const toUrls = addressHostToUrl.bind(null, addressInfo)
const toUrlArray = (h: HostnameInfo) => {
const u = toUrls(h)
return [u.url, u.sslUrl].filter((u) => u !== null)
}
const hostnames = host.hostnameInfo[addressInfo.internalPort] ?? []
return {
...addressInfo,
hostnames,
toUrl,
toUrls,
filter: <F extends Filter, Format extends Formats = "urlstring">(
filter: F,
format?: Format,
@@ -237,7 +246,7 @@ export const filledAddress = (
const filtered = filterRec(hostnames, filter, false)
let res: FormatReturnTy<F, Format>[] = filtered as any
if (format === "hostname-info") return res
const urls = filtered.flatMap(toUrl)
const urls = filtered.flatMap(toUrlArray)
if (format === "url") res = urls.map((u) => new URL(u)) as any
else res = urls as any
return res
@@ -279,28 +288,28 @@ export const filledAddress = (
)
},
get urls() {
return this.hostnames.flatMap(toUrl)
return this.hostnames.flatMap(toUrlArray)
},
get publicUrls() {
return this.publicHostnames.flatMap(toUrl)
return this.publicHostnames.flatMap(toUrlArray)
},
get onionUrls() {
return this.onionHostnames.flatMap(toUrl)
return this.onionHostnames.flatMap(toUrlArray)
},
get localUrls() {
return this.localHostnames.flatMap(toUrl)
return this.localHostnames.flatMap(toUrlArray)
},
get ipUrls() {
return this.ipHostnames.flatMap(toUrl)
return this.ipHostnames.flatMap(toUrlArray)
},
get ipv4Urls() {
return this.ipv4Hostnames.flatMap(toUrl)
return this.ipv4Hostnames.flatMap(toUrlArray)
},
get ipv6Urls() {
return this.ipv6Hostnames.flatMap(toUrl)
return this.ipv6Hostnames.flatMap(toUrlArray)
},
get nonIpUrls() {
return this.nonIpHostnames.flatMap(toUrl)
return this.nonIpHostnames.flatMap(toUrlArray)
},
}
}

View File

@@ -502,7 +502,7 @@ export default {
531: 'Fehler beim Initialisieren des Servers',
532: 'Abgeschlossen',
533: 'Gateways',
535: 'Gateway hinzufügen',
535: 'StartTunnel-Gateway hinzufügen',
536: 'Umbenennen',
537: 'Zugriff',
538: 'Öffentliche Domains',
@@ -535,10 +535,8 @@ export default {
569: 'Wählen Sie eine Zertifizierungsstelle aus, um SSL/TLS-Zertifikate für diese Domain auszustellen.',
570: 'Andere',
571: 'Ein Name zur einfachen Identifizierung des Gateways',
572: 'Wählen Sie diese Option, wenn das Gateway für den privaten Zugriff nur für autorisierte Clients konfiguriert ist. StartTunnel ist ein privates Gateway.',
573: 'Wählen Sie diese Option, wenn das Gateway für uneingeschränkten öffentlichen Zugriff konfiguriert ist.',
574: 'Datei',
575: 'Wireguard-Konfigurationsdatei',
575: 'StartTunnel-Konfigurationsdatei',
576: 'Kopieren/Einfügen',
577: 'Dateiinhalt',
578: 'Öffentlicher Schlüssel',
@@ -550,7 +548,7 @@ export default {
584: 'Verbindungen können manchmal langsam oder unzuverlässig sein',
585: 'Öffentlich, wenn Sie die Adresse öffentlich teilen, andernfalls privat',
586: 'Erfordert ein Tor-fähiges Gerät oder einen Browser',
587: 'Nur nützlich für Clients, die HTTPS erzwingen',
587: 'Nur nützlich für Clients, die SSL erzwingen',
588: 'Ideal für anonyme, zensurresistente Bereitstellung und Fernzugriff',
589: 'Ideal für lokalen Zugriff',
590: 'Erfordert die Verbindung mit demselben lokalen Netzwerk (LAN) wie Ihr Server, entweder physisch oder über VPN',
@@ -589,4 +587,5 @@ export default {
623: 'Alternative Implementierungen',
624: 'Versionen',
625: 'Eine andere Version auswählen',
626: 'Hochladen',
} satisfies i18n

View File

@@ -501,7 +501,7 @@ export const ENGLISH = {
'Error initializing server': 531,
'Finished': 532, // an in, complete
'Gateways': 533, // as in, a device or software that connects two different networks
'Add gateway': 535, // as in, add a new network gateway to StartOS
'Add StartTunnel Gateway': 535, // as in, add a new StartTunnel network gateway to StartOS
'Rename': 536,
'Access': 537, // as in, public or private access, almost "permission"
'Public Domains': 538, // as in, internet domains
@@ -534,10 +534,8 @@ export const ENGLISH = {
'Select a Certificate Authority to issue SSL/TLS certificates for this domain': 569,
'Other': 570, // as in, a list option to indicate none of the options listed
'A name to easily identify the gateway': 571,
'select this option if the gateway is configured for private access to authorized clients only. StartTunnel is a private gateway.': 572,
'select this option if the gateway is configured for unfettered public access.': 573,
'File': 574, // as in, a computer file
'Wireguard Config File': 575,
'StartTunnel Config File': 575,
'Copy/Paste': 576,
'File Contents': 577,
'Public Key': 578, // as in, a cryptographic public key
@@ -549,7 +547,7 @@ export const ENGLISH = {
'Connections can be slow or unreliable at times': 584,
'Public if you share the address publicly, otherwise private': 585,
'Requires using a Tor-enabled device or browser': 586,
'Only useful for clients that enforce HTTPS': 587,
'Only useful for clients that require SSL': 587,
'Ideal for anonymous, censorship-resistant hosting and remote access': 588,
'Ideal for local access': 589,
'Requires being connected to the same Local Area Network (LAN) as your server, either physically or via VPN': 590,
@@ -588,4 +586,5 @@ export const ENGLISH = {
'Alternative Implementations': 623,
'Versions': 624,
'Select another version': 625,
'Upload': 626, // as in, upload a file
} as const

View File

@@ -502,7 +502,7 @@ export default {
531: 'Error al inicializar el servidor',
532: 'Finalizado',
533: 'Puertas de enlace',
535: 'Agregar puerta de enlace',
535: 'Agregar puerta de enlace StartTunnel',
536: 'Renombrar',
537: 'Acceso',
538: 'Dominios públicos',
@@ -535,10 +535,8 @@ export default {
569: 'Selecciona una Autoridad Certificadora para emitir certificados SSL/TLS para este dominio.',
570: 'Otro',
571: 'Un nombre para identificar fácilmente la puerta de enlace',
572: 'Selecciona esta opción si la puerta de enlace está configurada para acceso privado solo a clientes autorizados. StartTunnel es una puerta de enlace privada.',
573: 'Selecciona esta opción si la puerta de enlace está configurada para acceso público sin restricciones.',
574: 'Archivo',
575: 'Archivo de configuración de Wireguard',
575: 'Archivo de configuración de StartTunnel',
576: 'Copiar/Pegar',
577: 'Contenido del archivo',
578: 'Clave pública',
@@ -550,7 +548,7 @@ export default {
584: 'Las conexiones pueden ser lentas o poco confiables a veces',
585: 'Público si compartes la dirección públicamente, de lo contrario privado',
586: 'Requiere un dispositivo o navegador habilitado para Tor',
587: 'Solo útil para clientes que imponen HTTPS',
587: 'Solo útil para clientes que imponen SSL',
588: 'Ideal para alojamiento y acceso remoto anónimo y resistente a la censura',
589: 'Ideal para acceso local',
590: 'Requiere estar conectado a la misma red de área local (LAN) que tu servidor, ya sea físicamente o mediante VPN',
@@ -589,4 +587,5 @@ export default {
623: 'Implementaciones alternativas',
624: 'Versiones',
625: 'Seleccionar otra versión',
626: 'Subir',
} satisfies i18n

View File

@@ -502,7 +502,7 @@ export default {
531: "Erreur lors de l'initialisation du serveur",
532: 'Terminé',
533: 'Passerelles',
535: 'Ajouter une passerelle',
535: 'Ajouter une passerelle StartTunnel',
536: 'Renommer',
537: 'Accès',
538: 'Domaines publics',
@@ -535,10 +535,8 @@ export default {
569: 'Sélectionnez une Autorité de Certification pour émettre des certificats SSL/TLS pour ce domaine.',
570: 'Autre',
571: 'Un nom pour identifier facilement la passerelle',
572: 'Sélectionnez cette option si la passerelle est configurée pour un accès privé uniquement aux clients autorisés. StartTunnel est une passerelle privée.',
573: 'Sélectionnez cette option si la passerelle est configurée pour un accès public illimité.',
574: 'Fichier',
575: 'Fichier de configuration Wireguard',
575: 'Fichier de configuration StartTunnel',
576: 'Copier/Coller',
577: 'Contenu du fichier',
578: 'Clé publique',
@@ -550,7 +548,7 @@ export default {
584: 'Les connexions peuvent parfois être lentes ou peu fiables',
585: 'Public si vous partagez ladresse publiquement, sinon privé',
586: 'Nécessite un appareil ou un navigateur compatible Tor',
587: 'Utile uniquement pour les clients qui imposent HTTPS',
587: 'Utile uniquement pour les clients qui imposent SSL',
588: 'Idéal pour lhébergement et laccès à distance anonymes et résistants à la censure',
589: 'Idéal pour un accès local',
590: 'Nécessite dêtre connecté au même réseau local (LAN) que votre serveur, soit physiquement, soit via VPN',
@@ -589,4 +587,5 @@ export default {
623: 'Implémentations alternatives',
624: 'Versions',
625: 'Sélectionner une autre version',
626: 'Téléverser',
} satisfies i18n

View File

@@ -502,7 +502,7 @@ export default {
531: 'Błąd inicjalizacji serwera',
532: 'Zakończono',
533: 'Bramy sieciowe',
535: 'Dodaj bramę',
535: 'Dodaj bramę StartTunnel',
536: 'Zmień nazwę',
537: 'Dostęp',
538: 'Domeny publiczne',
@@ -535,10 +535,8 @@ export default {
569: 'Wybierz Urząd Certyfikacji, aby wystawić certyfikaty SSL/TLS dla tej domeny.',
570: 'Inne',
571: 'Nazwa ułatwiająca identyfikację bramy',
572: 'Wybierz tę opcję, jeśli brama jest skonfigurowana do prywatnego dostępu tylko dla autoryzowanych klientów. StartTunnel to prywatna brama.',
573: 'Wybierz tę opcję, jeśli brama jest skonfigurowana do nieograniczonego publicznego dostępu.',
574: 'Plik',
575: 'Plik konfiguracyjny Wireguard',
575: 'Plik konfiguracyjny StartTunnel',
576: 'Kopiuj/Wklej',
577: 'Zawartość pliku',
578: 'Klucz publiczny',
@@ -550,7 +548,7 @@ export default {
584: 'Połączenia mogą być czasami wolne lub niestabilne',
585: 'Publiczne, jeśli udostępniasz adres publicznie, w przeciwnym razie prywatne',
586: 'Wymaga urządzenia lub przeglądarki obsługującej Tor',
587: 'Przydatne tylko dla klientów wymuszających HTTPS',
587: 'Przydatne tylko dla klientów wymuszających SSL',
588: 'Idealne do anonimowego, odpornego na cenzurę hostingu i zdalnego dostępu',
589: 'Idealne do dostępu lokalnego',
590: 'Wymaga połączenia z tą samą siecią lokalną (LAN) co serwer, fizycznie lub przez VPN',
@@ -589,4 +587,5 @@ export default {
623: 'Alternatywne implementacje',
624: 'Wersje',
625: 'Wybierz inną wersję',
626: 'Prześlij',
} satisfies i18n

View File

@@ -7,6 +7,7 @@ import { i18nKey, i18nPipe } from '@start9labs/shared'
type AddressWithInfo = {
url: string
ssl: boolean
info: T.HostnameInfo
gateway?: GatewayPlus
}
@@ -132,12 +133,26 @@ export class InterfaceService {
if (!hostnamesInfos.length) return addresses
const allAddressesWithInfo: AddressWithInfo[] = hostnamesInfos.flatMap(h =>
utils.addressHostToUrl(serviceInterface.addressInfo, h).map(url => ({
url,
info: h,
gateway: gateways.find(g => h.kind === 'ip' && h.gateway.id === g.id),
})),
const allAddressesWithInfo: AddressWithInfo[] = hostnamesInfos.flatMap(
h => {
const { url, sslUrl } = utils.addressHostToUrl(
serviceInterface.addressInfo,
h,
)
const info = h
const gateway =
h.kind === 'ip'
? gateways.find(g => h.gateway.id === g.id)
: undefined
const res = []
if (url) {
res.push({ url, ssl: false, info, gateway })
}
if (sslUrl) {
res.push({ url: sslUrl, ssl: true, info, gateway })
}
return res
},
)
const torAddrs = allAddressesWithInfo.filter(filterTor).sort(cmpTor)
@@ -311,7 +326,7 @@ export class InterfaceService {
}
private toDisplayAddress(
{ info, url, gateway }: AddressWithInfo,
{ info, ssl, url, gateway }: AddressWithInfo,
publicDomains: Record<string, T.PublicDomainConfig>,
): DisplayAddress {
let access: DisplayAddress['access']
@@ -335,33 +350,29 @@ export class InterfaceService {
),
this.i18n.transform('Requires using a Tor-enabled device or browser'),
]
// Tor (HTTPS)
if (url.startsWith('https:')) {
type = `${type} (HTTPS)`
// Tor (SSL)
if (ssl) {
type = `${type} (SSL)`
bullets = [
this.i18n.transform('Only useful for clients that enforce HTTPS'),
this.i18n.transform('Only useful for clients that require SSL'),
rootCaRequired,
...bullets,
]
// Tor (HTTP)
// Tor (NON-SSL)
} else {
bullets.unshift(
this.i18n.transform(
'Ideal for anonymous, censorship-resistant hosting and remote access',
),
)
if (url.startsWith('http:')) {
type = `${type} (HTTP)`
}
}
// ** Not Tor **
} else {
const port = info.hostname.sslPort || info.hostname.port
const g = gateway!
gatewayName = g.name
gatewayName = info.gateway.name
const gatewayLanIpv4 = g.lanIpv4[0]
const isWireguard = g.ipInfo.deviceType === 'wireguard'
const gatewayLanIpv4 = gateway?.lanIpv4[0]
const isWireguard = gateway?.ipInfo.deviceType === 'wireguard'
const localIdeal = this.i18n.transform('Ideal for local access')
const lanRequired = this.i18n.transform(
@@ -402,9 +413,9 @@ export class InterfaceService {
),
rootCaRequired,
]
if (!g.public) {
if (!info.gateway.public) {
bullets.push(
`${portForwarding} "${gatewayName}": ${port} -> ${g.subnets.find(s => s.isIpv4())?.address}:${port}`,
`${portForwarding} "${gatewayName}": ${port} -> ${gateway?.subnets.find(s => s.isIpv4())?.address}:${port}`,
)
}
} else {
@@ -436,12 +447,12 @@ export class InterfaceService {
if (info.public) {
access = 'public'
bullets = [
`${dnsFor} ${info.hostname.value} ${resolvesTo} ${g.ipInfo.wanIp}`,
`${dnsFor} ${info.hostname.value} ${resolvesTo} ${gateway?.ipInfo.wanIp}`,
]
if (!g.public) {
if (!info.gateway.public) {
bullets.push(
`${portForwarding} "${gatewayName}": ${port} -> ${g.subnets.find(s => s.isIpv4())?.address}:${port === 443 ? 5443 : port}`,
`${portForwarding} "${gatewayName}": ${port} -> ${gateway?.subnets.find(s => s.isIpv4())?.address}:${port === 443 ? 5443 : port}`,
)
}

View File

@@ -83,18 +83,10 @@ export default class GatewaysComponent {
),
required: true,
default: null,
}),
type: ISB.Value.select({
name: this.i18n.transform('Type'),
description: `-**${this.i18n.transform('private')}**: ${this.i18n.transform('select this option if the gateway is configured for private access to authorized clients only. StartTunnel is a private gateway.')}\n-**${this.i18n.transform('public')}**: ${this.i18n.transform('select this option if the gateway is configured for unfettered public access.')}`,
default: 'private',
values: {
private: this.i18n.transform('private'),
public: this.i18n.transform('public'),
},
placeholder: 'StartTunnel 1',
}),
config: ISB.Value.union({
name: this.i18n.transform('Wireguard Config File'),
name: this.i18n.transform('StartTunnel Config File'),
default: 'paste',
variants: ISB.Variants.of({
paste: {
@@ -108,7 +100,7 @@ export default class GatewaysComponent {
}),
},
select: {
name: this.i18n.transform('Select'),
name: this.i18n.transform('Upload'),
spec: ISB.InputSpec.of({
file: ISB.Value.file({
name: this.i18n.transform('File'),
@@ -122,7 +114,7 @@ export default class GatewaysComponent {
})
this.formDialog.open(FormComponent, {
label: 'Add gateway',
label: 'Add StartTunnel Gateway',
data: {
spec: await configBuilderToSpec(spec),
buttons: [
@@ -138,7 +130,7 @@ export default class GatewaysComponent {
input.config.selection === 'paste'
? input.config.value.file
: await (input.config.value.file as any as File).text(),
public: input.type === 'public',
public: false,
})
return true
} catch (e: any) {