mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
outbound gateway support (#3120)
* Multiple (#3111) * fix alerts i18n, fix status display, better, remove usb media, hide shutdown for install complete * trigger chnage detection for localize pipe and round out implementing localize pipe for consistency even though not needed * Fix PackageInfoShort to handle LocaleString on releaseNotes (#3112) * Fix PackageInfoShort to handle LocaleString on releaseNotes * fix: filter by target_version in get_matching_models and pass otherVersions from install * chore: add exver documentation for ai agents * frontend plus some be types --------- Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
This commit is contained in:
@@ -2253,6 +2253,7 @@ export namespace Mock {
|
||||
},
|
||||
},
|
||||
storeExposedDependents: [],
|
||||
outboundGateway: null,
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
tasks: {
|
||||
@@ -2321,6 +2322,7 @@ export namespace Mock {
|
||||
},
|
||||
hosts: {},
|
||||
storeExposedDependents: [],
|
||||
outboundGateway: null,
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
tasks: {},
|
||||
@@ -2427,6 +2429,7 @@ export namespace Mock {
|
||||
},
|
||||
hosts: {},
|
||||
storeExposedDependents: [],
|
||||
outboundGateway: null,
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
tasks: {
|
||||
|
||||
@@ -252,10 +252,13 @@ export namespace RR {
|
||||
|
||||
// network
|
||||
|
||||
export type GatewayType = 'inbound-outbound' | 'outbound-only'
|
||||
|
||||
export type AddTunnelReq = {
|
||||
name: string
|
||||
config: string // file contents
|
||||
public: boolean
|
||||
type: GatewayType
|
||||
setAsDefaultOutbound?: boolean
|
||||
} // net.tunnel.add
|
||||
export type AddTunnelRes = {
|
||||
id: string
|
||||
@@ -270,6 +273,17 @@ export namespace RR {
|
||||
export type RemoveTunnelReq = { id: string } // net.tunnel.remove
|
||||
export type RemoveTunnelRes = null
|
||||
|
||||
// Set default outbound gateway
|
||||
export type SetDefaultOutboundReq = { gateway: string | null } // net.gateway.set-default-outbound
|
||||
export type SetDefaultOutboundRes = null
|
||||
|
||||
// Set service outbound gateway
|
||||
export type SetServiceOutboundReq = {
|
||||
packageId: string
|
||||
gateway: string | null
|
||||
} // package.set-outbound-gateway
|
||||
export type SetServiceOutboundRes = null
|
||||
|
||||
export type InitAcmeReq = {
|
||||
provider: string
|
||||
contact: string[]
|
||||
|
||||
@@ -175,6 +175,14 @@ export abstract class ApiService {
|
||||
|
||||
abstract removeTunnel(params: RR.RemoveTunnelReq): Promise<RR.RemoveTunnelRes>
|
||||
|
||||
abstract setDefaultOutbound(
|
||||
params: RR.SetDefaultOutboundReq,
|
||||
): Promise<RR.SetDefaultOutboundRes>
|
||||
|
||||
abstract setServiceOutbound(
|
||||
params: RR.SetServiceOutboundReq,
|
||||
): Promise<RR.SetServiceOutboundRes>
|
||||
|
||||
// ** domains **
|
||||
|
||||
// wifi
|
||||
|
||||
@@ -355,6 +355,18 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'net.tunnel.remove', params })
|
||||
}
|
||||
|
||||
async setDefaultOutbound(
|
||||
params: RR.SetDefaultOutboundReq,
|
||||
): Promise<RR.SetDefaultOutboundRes> {
|
||||
return this.rpcRequest({ method: 'net.gateway.set-default-outbound', params })
|
||||
}
|
||||
|
||||
async setServiceOutbound(
|
||||
params: RR.SetServiceOutboundReq,
|
||||
): Promise<RR.SetServiceOutboundRes> {
|
||||
return this.rpcRequest({ method: 'package.set-outbound-gateway', params })
|
||||
}
|
||||
|
||||
// wifi
|
||||
|
||||
async enableWifi(params: RR.EnabledWifiReq): Promise<RR.EnabledWifiRes> {
|
||||
|
||||
@@ -566,13 +566,12 @@ export class MockApiService extends ApiService {
|
||||
|
||||
const id = `wg${this.proxyId++}`
|
||||
|
||||
const patch: AddOperation<T.NetworkInterfaceInfo>[] = [
|
||||
const patch: AddOperation<any>[] = [
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/network/gateways/${id}`,
|
||||
value: {
|
||||
name: params.name,
|
||||
public: params.public,
|
||||
secure: false,
|
||||
ipInfo: {
|
||||
name: id,
|
||||
@@ -584,9 +583,19 @@ export class MockApiService extends ApiService {
|
||||
lanIp: ['192.168.1.10'],
|
||||
dnsServers: [],
|
||||
},
|
||||
type: params.type,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
if (params.setAsDefaultOutbound) {
|
||||
;(patch as any[]).push({
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/serverInfo/network/defaultOutbound',
|
||||
value: id,
|
||||
})
|
||||
}
|
||||
|
||||
this.mockRevision(patch)
|
||||
|
||||
return { id }
|
||||
@@ -620,6 +629,38 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async setDefaultOutbound(
|
||||
params: RR.SetDefaultOutboundReq,
|
||||
): Promise<RR.SetDefaultOutboundRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/serverInfo/network/defaultOutbound',
|
||||
value: params.gateway,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async setServiceOutbound(
|
||||
params: RR.SetServiceOutboundReq,
|
||||
): Promise<RR.SetServiceOutboundRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/packageData/${params.packageId}/outboundGateway`,
|
||||
value: params.gateway,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// wifi
|
||||
|
||||
async enableWifi(params: RR.EnabledWifiReq): Promise<RR.EnabledWifiRes> {
|
||||
|
||||
@@ -124,8 +124,8 @@ export const mockPatchData: DataModel = {
|
||||
gateways: {
|
||||
eth0: {
|
||||
name: null,
|
||||
public: null,
|
||||
secure: null,
|
||||
type: null,
|
||||
ipInfo: {
|
||||
name: 'Wired Connection 1',
|
||||
scopeId: 1,
|
||||
@@ -139,8 +139,8 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
wlan0: {
|
||||
name: null,
|
||||
public: null,
|
||||
secure: null,
|
||||
type: null,
|
||||
ipInfo: {
|
||||
name: 'Wireless Connection 1',
|
||||
scopeId: 2,
|
||||
@@ -157,8 +157,8 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
wireguard1: {
|
||||
name: 'StartTunnel',
|
||||
public: null,
|
||||
secure: null,
|
||||
type: 'inbound-outbound',
|
||||
ipInfo: {
|
||||
name: 'wireguard1',
|
||||
scopeId: 2,
|
||||
@@ -173,7 +173,23 @@ export const mockPatchData: DataModel = {
|
||||
dnsServers: ['1.1.1.1'],
|
||||
},
|
||||
},
|
||||
wireguard2: {
|
||||
name: 'Mullvad VPN',
|
||||
secure: null,
|
||||
type: 'outbound-only',
|
||||
ipInfo: {
|
||||
name: 'wireguard2',
|
||||
scopeId: 4,
|
||||
deviceType: 'wireguard',
|
||||
subnets: [],
|
||||
wanIp: '198.51.100.77',
|
||||
ntpServers: [],
|
||||
lanIp: [],
|
||||
dnsServers: ['10.64.0.1'],
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultOutbound: 'eth0',
|
||||
dns: {
|
||||
dhcpServers: ['1.1.1.1', '8.8.8.8'],
|
||||
staticServers: null,
|
||||
@@ -320,6 +336,7 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
hosts: {},
|
||||
storeExposedDependents: [],
|
||||
outboundGateway: null,
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
tasks: {
|
||||
@@ -624,6 +641,7 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
},
|
||||
storeExposedDependents: [],
|
||||
outboundGateway: null,
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
tasks: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ErrorService,
|
||||
i18nKey,
|
||||
i18nPipe,
|
||||
i18nService,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
@@ -24,6 +25,7 @@ export class ControlsService {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
private readonly i18nService = inject(i18nService)
|
||||
|
||||
async start({ title, alerts, id }: T.Manifest, unmet: boolean) {
|
||||
const deps =
|
||||
@@ -31,7 +33,7 @@ export class ControlsService {
|
||||
|
||||
if (
|
||||
(unmet && !(await this.alert(deps))) ||
|
||||
(alerts.start && !(await this.alert(alerts.start as i18nKey)))
|
||||
(alerts.start && !(await this.alert(alerts.start)))
|
||||
) {
|
||||
return
|
||||
}
|
||||
@@ -49,7 +51,7 @@ export class ControlsService {
|
||||
|
||||
async stop({ id, title, alerts }: T.Manifest) {
|
||||
const depMessage = `${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}`
|
||||
let content = alerts.stop || ''
|
||||
let content = alerts.stop ? this.i18nService.localize(alerts.stop) : ''
|
||||
|
||||
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
|
||||
content = content ? `${content}.\n\n${depMessage}` : depMessage
|
||||
@@ -113,14 +115,14 @@ export class ControlsService {
|
||||
})
|
||||
}
|
||||
|
||||
private alert(content: i18nKey): Promise<boolean> {
|
||||
private alert(content: T.LocaleString): Promise<boolean> {
|
||||
return firstValueFrom(
|
||||
this.dialog
|
||||
.openConfirm({
|
||||
label: 'Warning',
|
||||
size: 's',
|
||||
data: {
|
||||
content,
|
||||
content: this.i18nService.localize(content),
|
||||
yes: 'Continue',
|
||||
no: 'Cancel',
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { T, utils } from '@start9labs/start-sdk'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { map } from 'rxjs'
|
||||
import { DataModel } from './patch-db/data-model'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
|
||||
@@ -12,39 +12,47 @@ export type GatewayPlus = T.NetworkInterfaceInfo & {
|
||||
subnets: utils.IpNet[]
|
||||
lanIpv4: string[]
|
||||
wanIp?: utils.IpAddress
|
||||
public: boolean
|
||||
isDefaultOutbound: boolean
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class GatewayService {
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
|
||||
private readonly network$ = this.patch.watch$('serverInfo', 'network')
|
||||
|
||||
readonly defaultOutbound = toSignal(
|
||||
this.network$.pipe(map(n => n.defaultOutbound)),
|
||||
)
|
||||
|
||||
readonly gateways = toSignal(
|
||||
inject<PatchDB<DataModel>>(PatchDB)
|
||||
.watch$('serverInfo', 'network', 'gateways')
|
||||
.pipe(
|
||||
map(gateways =>
|
||||
Object.entries(gateways)
|
||||
.filter(([_, val]) => !!val?.ipInfo)
|
||||
.filter(
|
||||
([_, val]) =>
|
||||
val?.ipInfo?.deviceType !== 'bridge' &&
|
||||
val?.ipInfo?.deviceType !== 'loopback',
|
||||
)
|
||||
.map(([id, val]) => {
|
||||
const subnets =
|
||||
val.ipInfo?.subnets.map(s => utils.IpNet.parse(s)) ?? []
|
||||
const name = val.name ?? val.ipInfo!.name
|
||||
return {
|
||||
...val,
|
||||
id,
|
||||
name,
|
||||
subnets,
|
||||
lanIpv4: subnets.filter(s => s.isIpv4()).map(s => s.address),
|
||||
public: val.public ?? subnets.some(s => s.isPublic()),
|
||||
wanIp:
|
||||
val.ipInfo?.wanIp && utils.IpAddress.parse(val.ipInfo?.wanIp),
|
||||
} as GatewayPlus
|
||||
}),
|
||||
),
|
||||
),
|
||||
this.network$.pipe(
|
||||
map(network => {
|
||||
const gateways = network.gateways
|
||||
const defaultOutbound = network.defaultOutbound
|
||||
return Object.entries(gateways)
|
||||
.filter(([_, val]) => !!val?.ipInfo)
|
||||
.filter(
|
||||
([_, val]) =>
|
||||
val?.ipInfo?.deviceType !== 'bridge' &&
|
||||
val?.ipInfo?.deviceType !== 'loopback',
|
||||
)
|
||||
.map(([id, val]) => {
|
||||
const subnets =
|
||||
val.ipInfo?.subnets.map(s => utils.IpNet.parse(s)) ?? []
|
||||
const name = val.name ?? val.ipInfo!.name
|
||||
return {
|
||||
...val,
|
||||
id,
|
||||
name,
|
||||
subnets,
|
||||
lanIpv4: subnets.filter(s => s.isIpv4()).map(s => s.address),
|
||||
wanIp:
|
||||
val.ipInfo?.wanIp && utils.IpAddress.parse(val.ipInfo?.wanIp),
|
||||
isDefaultOutbound: id === defaultOutbound,
|
||||
} as GatewayPlus
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export function getInstalledBaseStatus(statusInfo: T.StatusInfo): BaseStatus {
|
||||
(!statusInfo.started ||
|
||||
Object.values(statusInfo.health)
|
||||
.filter(h => !!h)
|
||||
.some(h => h.result === 'starting'))
|
||||
.some(h => h.result === 'starting' || h.result === 'waiting'))
|
||||
) {
|
||||
return 'starting'
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ErrorService,
|
||||
i18nKey,
|
||||
i18nPipe,
|
||||
i18nService,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
@@ -27,6 +28,7 @@ export class StandardActionsService {
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly router = inject(Router)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
private readonly i18nService = inject(i18nService)
|
||||
|
||||
async rebuild(id: string) {
|
||||
const loader = this.loader.open('Rebuilding container').subscribe()
|
||||
@@ -50,11 +52,12 @@ export class StandardActionsService {
|
||||
): Promise<void> {
|
||||
let content = soft
|
||||
? ''
|
||||
: alerts.uninstall ||
|
||||
`${this.i18n.transform('Uninstalling')} ${title} ${this.i18n.transform('will permanently delete its data.')}`
|
||||
: alerts.uninstall
|
||||
? this.i18nService.localize(alerts.uninstall)
|
||||
: `${this.i18n.transform('Uninstalling')} ${title} ${this.i18n.transform('will permanently delete its data.')}`
|
||||
|
||||
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
|
||||
content = `${content}${content ? ' ' : ''}${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}`
|
||||
content = `${content ? `${content} ` : ''}${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}`
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
|
||||
Reference in New Issue
Block a user