Files
op-packages/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js
T

1532 lines
46 KiB
JavaScript

'use strict';
'require view';
'require form';
'require rpc';
'require tools.widgets as widgets';
/*
* Important:
*
* This LuCI page writes UCI options into /etc/config/frpc.
* UCI option names must not contain ".".
*
* Therefore we keep UCI-safe option names here, then let init/generator script
* convert them to frp TOML keys, for example:
*
* server_addr -> serverAddr
* server_port -> serverPort
* protocol -> transport.protocol
* wire_protocol -> transport.wireProtocol
* tls_enable -> transport.tls.enable
* token -> auth.token
* admin_addr -> webServer.addr
* use_encryption -> transport.useEncryption
* use_compression -> transport.useCompression
* sk -> secretKey
*
* Do NOT use option names like "transport.protocol" in LuCI form options.
*/
/*
* Debug-only TOML mapping. Keep these TOML: hints in the JS source for
* troubleshooting generated configs, but do not show them in LuCI form text
* and do not wrap them in _().
*
* common.client_id -> TOML: clientID
* common.user -> TOML: user
* common.server_addr -> TOML: serverAddr
* common.server_port -> TOML: serverPort
* common.nat_hole_stun_server -> TOML: natHoleStunServer
* common.login_fail_exit -> TOML: loginFailExit
* common.dns_server -> TOML: dnsServer
* common.start -> TOML: start
* common.udp_packet_size -> TOML: udpPacketSize
* common.includes -> TOML: includes
* common.authentication_method -> TOML: auth.method
* common.token -> TOML: auth.token
* common.auth_additional_scopes -> TOML: auth.additionalScopes
* common.token_source_type -> TOML: auth.tokenSource.type
* common.token_source_file_path -> TOML: auth.tokenSource.file.path
* common.token_source_exec_command -> TOML: auth.tokenSource.exec.command
* common.token_source_exec_args -> TOML: auth.tokenSource.exec.args
* common.token_source_exec_env -> TOML: auth.tokenSource.exec.env
* common.oidc_client_id -> TOML: auth.oidc.clientID
* common.oidc_client_secret -> TOML: auth.oidc.clientSecret
* common.oidc_audience -> TOML: auth.oidc.audience
* common.oidc_scope -> TOML: auth.oidc.scope
* common.oidc_token_endpoint_url -> TOML: auth.oidc.tokenEndpointURL
* common.oidc_additional_endpoint_params -> TOML: auth.oidc.additionalEndpointParams
* common.oidc_trusted_ca_file -> TOML: auth.oidc.trustedCaFile
* common.oidc_insecure_skip_verify -> TOML: auth.oidc.insecureSkipVerify
* common.oidc_proxy_url -> TOML: auth.oidc.proxyURL
* common.dial_server_timeout -> TOML: transport.dialServerTimeout
* common.dial_server_keepalive -> TOML: transport.dialServerKeepalive
* common.http_proxy -> TOML: transport.proxyURL
* common.pool_count -> TOML: transport.poolCount
* common.tcp_mux -> TOML: transport.tcpMux
* common.tcp_mux_keepalive_interval -> TOML: transport.tcpMuxKeepaliveInterval
* common.protocol -> TOML: transport.protocol
* common.wire_protocol -> TOML: transport.wireProtocol
* common.connect_server_local_ip -> TOML: transport.connectServerLocalIP
* common.heartbeat_interval -> TOML: transport.heartbeatInterval
* common.heartbeat_timeout -> TOML: transport.heartbeatTimeout
* common.tls_enable -> TOML: transport.tls.enable
* common.tls_cert_file -> TOML: transport.tls.certFile
* common.tls_key_file -> TOML: transport.tls.keyFile
* common.tls_trusted_ca_file -> TOML: transport.tls.trustedCaFile
* common.tls_server_name -> TOML: transport.tls.serverName
* common.disable_custom_tls_first_byte -> TOML: transport.tls.disableCustomTLSFirstByte
* common.quic_keepalive_period -> TOML: transport.quic.keepalivePeriod
* common.quic_max_idle_timeout -> TOML: transport.quic.maxIdleTimeout
* common.quic_max_incoming_streams -> TOML: transport.quic.maxIncomingStreams
* common.admin_addr -> TOML: webServer.addr
* common.admin_port -> TOML: webServer.port
* common.admin_user -> TOML: webServer.user
* common.admin_pwd -> TOML: webServer.password
* common.admin_tls_cert_file -> TOML: webServer.tls.certFile
* common.admin_tls_key_file -> TOML: webServer.tls.keyFile
* common.assets_dir -> TOML: webServer.assetsDir
* common.pprof_enable -> TOML: webServer.pprofEnable
* common.feature_gates -> TOML: featureGates
* common.virtual_net_address -> TOML: virtualNet.address
* common.store_path -> TOML: store.path
* common.metadatas -> TOML: metadatas
* common.log_file -> TOML: log.to
* common.log_level -> TOML: log.level
* common.log_max_days -> TOML: log.maxDays
* common.disable_log_color -> TOML: log.disablePrintColor
* conf.name -> TOML: proxies[].name / visitors[].name
* conf.type -> TOML: proxies[].type / visitors[].type
* conf.enabled -> TOML: proxies[].enabled / visitors[].enabled
* conf.local_ip -> TOML: proxies[].localIP
* conf.local_port -> TOML: proxies[].localPort
* conf.remote_port -> TOML: proxies[].remotePort
* conf.custom_domains -> TOML: proxies[].customDomains
* conf.subdomain -> TOML: proxies[].subdomain
* conf.locations -> TOML: proxies[].locations
* conf.http_user -> TOML: proxies[].httpUser
* conf.http_pwd -> TOML: proxies[].httpPassword
* conf.route_by_http_user -> TOML: proxies[].routeByHTTPUser
* conf.host_header_rewrite -> TOML: proxies[].hostHeaderRewrite
* conf.request_headers -> TOML: proxies[].requestHeaders.set
* conf.response_headers -> TOML: proxies[].responseHeaders.set
* conf.multiplexer -> TOML: proxies[].multiplexer
* conf.group -> TOML: proxies[].loadBalancer.group
* conf.group_key -> TOML: proxies[].loadBalancer.groupKey
* conf.health_check_type -> TOML: proxies[].healthCheck.type
* conf.health_check_url -> TOML: proxies[].healthCheck.path
* conf.health_check_headers -> TOML: proxies[].healthCheck.httpHeaders
* conf.sk -> TOML: proxies[].secretKey / visitors[].secretKey
* conf.allow_users -> TOML: proxies[].allowUsers
* conf.server_user -> TOML: visitors[].serverUser
* conf.server_name -> TOML: visitors[].serverName
* conf.bind_addr -> TOML: visitors[].bindAddr
* conf.bind_port -> TOML: visitors[].bindPort
* conf.visitor_protocol -> TOML: visitors[].protocol
* conf.keep_tunnel_open -> TOML: visitors[].keepTunnelOpen
* conf.fallback_to -> TOML: visitors[].fallbackTo
* conf.plugin -> TOML: proxies[].plugin.type / visitors[].plugin.type
*/
const startupConf = [
[form.Flag, 'stdout', _('Log stdout'), null,
{
enabled: '1',
disabled: '0',
default: '1',
rmempty: false,
retain: true,
remove: writeFlagDisabled
}],
[form.Flag, 'stderr', _('Log stderr'), null,
{
enabled: '1',
disabled: '0',
default: '1',
rmempty: false,
retain: true,
remove: writeFlagDisabled
}],
[widgets.UserSelect, 'user', _('Run daemon as user')],
[widgets.GroupSelect, 'group', _('Run daemon as group')],
[form.Flag, 'respawn', _('Respawn when crashed'), null,
{
enabled: '1',
disabled: '0',
default: '1',
rmempty: false,
retain: true,
remove: writeFlagDisabled
}],
[form.DynamicList, 'env', _('Environment variable'),
_('OS environments passed to frp for config file template, see %s.').format('<a href="https://github.com/fatedier/frp#configuration-file-template">frp README</a>'),
{
placeholder: 'ENV_NAME=value',
validate: function (section_id, value) {
return validateEnv(value);
}
}],
[form.DynamicList, 'conf_inc', _('Additional configs'),
_('Extra root-level configuration fragments included before generated proxy and visitor sections. Use frp includes or per-proxy raw settings for proxy/visitor tables.'),
{
datatype: 'file',
placeholder: '/etc/frp/frpc.d/frpc_extra.toml'
}]
];
const commonBaseConf = [
[form.Value, 'client_id', _('Client ID'),
_('Optional unique identifier for this frpc instance.')],
[form.Value, 'server_addr', _('Server address'),
_('Address of the frps server.'),
{ datatype: 'host', rmempty: false, placeholder: '127.0.0.1' }],
[form.Value, 'server_port', _('Server port'),
_('Port of the frps server. Default is 7000.'),
{ datatype: 'port', rmempty: false, placeholder: '7000' }],
[form.Value, 'user', _('Proxy name prefix'),
_('Prefix for proxy names. If set, proxy names become {user}.{proxy}.')],
[form.Value, 'nat_hole_stun_server', _('NAT hole STUN server'),
_('STUN server used for NAT traversal.'),
{ placeholder: 'stun.easyvoip.com:3478' }],
[form.Flag, 'login_fail_exit', _('Exit when login fails'),
_('Exit frpc when the first login fails.'),
{
enabled: 'true',
disabled: 'false',
default: 'true',
optional: false,
rmempty: false,
retain: true,
remove: writeFlagDisabled
}],
[form.Value, 'dns_server', _('DNS server'),
_('Custom DNS server used by frpc.'),
{ datatype: 'ipaddr', placeholder: '8.8.8.8' }],
[form.DynamicList, 'start', _('Start proxies / visitors'),
_('Only start specified proxies or visitors. Empty means start all.'),
{ placeholder: 'ssh' }],
[form.Value, 'udp_packet_size', _('UDP packet size'),
_('UDP packet size in bytes. Should match frps. Default is 1500.'),
{ datatype: 'uinteger', placeholder: '1500' }],
[form.DynamicList, 'includes', _('Include config files'),
_('Additional config files included by frpc.'),
{ placeholder: './confd/*.toml' }]
];
const commonAuthConf = [
[form.ListValue, 'authentication_method', _('Authentication method'),
_('Authentication method.'),
{ values: [['token', 'token'], ['oidc', 'oidc']], default: 'token' }],
[form.Value, 'token', _('Token'),
_('Token used for authentication.'),
{
depends: { authentication_method: 'token' },
password: true,
validate: function (section_id, value) {
const tokenSourceType = this.section.getOption('token_source_type');
if (value && tokenSourceType && tokenSourceType.formvalue(section_id))
return _('Token and token source are mutually exclusive.');
return true;
}
}],
[form.MultiValue, 'auth_additional_scopes', _('Additional auth scopes'),
_('Additional scopes to include authentication information.'),
{ values: ['HeartBeats', 'NewWorkConns'] }],
[form.ListValue, 'token_source_type', _('Token source type'),
_('Load token from an external source. File reads the token from a local file; exec runs a command and requires frp unsafe permission. Mutually exclusive with auth.token.'),
{
depends: { authentication_method: 'token' },
values: [['', _('Disabled')], ['file', 'file'], ['exec', 'exec']],
rmempty: true,
validate: function (section_id, value) {
const token = this.section.getOption('token');
if (value && token && token.formvalue(section_id))
return _('Token and token source are mutually exclusive.');
return true;
}
}],
[form.Value, 'token_source_file_path', _('Token source file'),
_('Path of token file.'),
{
depends: {
authentication_method: 'token',
token_source_type: 'file'
},
datatype: 'file',
rmempty: false,
validate: function (section_id, value) {
const tokenSourceType = this.section.getOption('token_source_type');
if (
tokenSourceType &&
tokenSourceType.formvalue(section_id) === 'file' &&
!String(value || '').trim()
)
return _('Token source file path is required when token source type is file.');
return true;
}
}],
[form.Value, 'token_source_exec_command', _('Token source command'),
_('Command used to obtain the token. The init script adds --allow-unsafe=TokenSourceExec when this source type is used.'),
{
depends: {
authentication_method: 'token',
token_source_type: 'exec'
},
datatype: 'file',
rmempty: false,
validate: function (section_id, value) {
const tokenSourceType = this.section.getOption('token_source_type');
if (
tokenSourceType &&
tokenSourceType.formvalue(section_id) === 'exec' &&
!String(value || '').trim()
)
return _('Token source command is required when token source type is exec.');
return true;
}
}],
[form.DynamicList, 'token_source_exec_args', _('Token source command arguments'),
_('Command arguments passed to token source exec.'),
{
depends: {
authentication_method: 'token',
token_source_type: 'exec'
},
placeholder: '--format'
}],
[form.DynamicList, 'token_source_exec_env', _('Token source environment'),
_('Environment variables passed to token source exec. Use KEY=value.'),
{
depends: {
authentication_method: 'token',
token_source_type: 'exec'
},
placeholder: 'TOKEN_SERVICE=production',
validate: function (section_id, value) {
return validateEnv(value);
}
}],
[form.Value, 'oidc_client_id', _('OIDC client ID'),
_('Configuration key: auth.oidc.clientID.'),
{ depends: { authentication_method: 'oidc' } }],
[form.Value, 'oidc_client_secret', _('OIDC client secret'),
_('Configuration key: auth.oidc.clientSecret.'),
{ depends: { authentication_method: 'oidc' }, password: true }],
[form.Value, 'oidc_audience', _('OIDC audience'),
_('Configuration key: auth.oidc.audience.'),
{ depends: { authentication_method: 'oidc' } }],
[form.Value, 'oidc_scope', _('OIDC scope'),
_('Configuration key: auth.oidc.scope.'),
{ depends: { authentication_method: 'oidc' } }],
[form.Value, 'oidc_token_endpoint_url', _('OIDC token endpoint URL'),
_('Configuration key: auth.oidc.tokenEndpointURL.'),
{ depends: { authentication_method: 'oidc' }, datatype: 'url' }],
[form.DynamicList, 'oidc_additional_endpoint_params', _('OIDC additional endpoint params'),
_('Additional OIDC token endpoint params. Use key=value.'),
{
depends: { authentication_method: 'oidc' },
placeholder: 'audience=https://dev.auth.com/api/v2/',
validate: function (section_id, value) {
return validateKeyValue(value);
}
}],
[form.Value, 'oidc_trusted_ca_file', _('OIDC trusted CA file'),
_('Custom CA certificate file for OIDC endpoint.'),
{ depends: { authentication_method: 'oidc' }, datatype: 'file' }],
[form.Flag, 'oidc_insecure_skip_verify', _('OIDC insecure skip verify'),
_('Skip TLS verification for OIDC endpoint. Not recommended for production.'),
{ depends: { authentication_method: 'oidc' }, datatype: 'bool', default: 'false' }],
[form.Value, 'oidc_proxy_url', _('OIDC proxy URL'),
_('Proxy URL for OIDC token endpoint.'),
{ depends: { authentication_method: 'oidc' }, datatype: 'url', placeholder: 'http://proxy.example.com:8080' }]
];
const commonLogConf = [
[form.Value, 'log_file', _('Log output'),
_('Log output path or console.'),
{ placeholder: 'console' }],
[form.ListValue, 'log_level', _('Log level'),
_('Configuration key: log.level.'),
{ values: ['trace', 'debug', 'info', 'warn', 'error'], default: 'info' }],
[form.Value, 'log_max_days', _('Log max days'),
_('Maximum number of days to keep logs.'),
{ datatype: 'uinteger', placeholder: '3' }],
[form.Flag, 'disable_log_color', _('Disable log color'),
_('Disable log colors when log output is console.'),
{ datatype: 'bool', default: 'false' }]
];
const commonWebConf = [
[form.Value, 'admin_addr', _('Web server address'),
_('Web server bind address.'),
{ datatype: 'host', placeholder: '127.0.0.1' }],
[form.Value, 'admin_port', _('Web server port'),
_('Web server port. Leave empty or set to 0 to disable the web server.'),
{ validate: validatePortOrZero, placeholder: '7400' }],
[form.Value, 'admin_user', _('Web server user'),
_('Web server username.'),
{ placeholder: 'admin' }],
[form.Value, 'admin_pwd', _('Web server password'),
_('Web server password.'),
{ password: true, placeholder: 'change_me_to_a_strong_password' }],
[form.Flag, 'admin_tls_enable', _('Enable web HTTPS'),
_('Enable HTTPS for the frpc web server. This switch controls whether webServer.tls.certFile and webServer.tls.keyFile are emitted.'),
{
enabled: 'true',
disabled: 'false',
default: 'false',
rmempty: false,
retain: true,
remove: writeFlagDisabled
}],
[form.Value, 'admin_tls_cert_file', _('TLS certificate path'),
_('Path to the certificate file used by the HTTPS web server.'),
{
datatype: 'file',
placeholder: '/etc/ssl/acme/example.com.fullchain.crt',
retain: true,
validate: function (section_id, value) {
if (
adminWebEnabled(this.section, section_id) &&
adminHttpsEnabled(this.section, section_id) &&
!String(value || '').trim()
)
return _('TLS certificate path is required when web HTTPS is enabled.');
return true;
}
}],
[form.Value, 'admin_tls_key_file', _('TLS private key path'),
_('Path to the private key file used by the HTTPS web server.'),
{
datatype: 'file',
placeholder: '/etc/ssl/acme/example.com.key',
retain: true,
validate: function (section_id, value) {
if (
adminWebEnabled(this.section, section_id) &&
adminHttpsEnabled(this.section, section_id) &&
!String(value || '').trim()
)
return _('TLS private key path is required when web HTTPS is enabled.');
return true;
}
}],
[form.Value, 'assets_dir', _('Assets dir'),
_('Web server assets directory.'),
{ datatype: 'directory' }],
[form.Flag, 'pprof_enable', _('Enable pprof'),
_('Enable Go pprof handlers in web server.'),
{
datatype: 'bool',
enabled: 'true',
disabled: 'false',
default: 'false',
rmempty: false,
retain: true,
remove: writeFlagDisabled
}]
];
const commonTransportConf = [
[form.Value, 'dial_server_timeout', _('Dial server timeout'),
_('Timeout in seconds for connecting to frps.'),
{ datatype: 'uinteger', placeholder: '10' }],
[form.Value, 'dial_server_keepalive', _('Dial server keepalive'),
_('TCP keepalive interval in seconds. Negative value disables keepalive.'),
{ datatype: 'integer', placeholder: '7200' }],
[form.Value, 'http_proxy', _('Proxy URL'),
_('Proxy URL used to connect to frps. Supports http, socks5 and ntlm.'),
{ placeholder: 'http://user:passwd@192.168.1.128:8080' }],
[form.Value, 'pool_count', _('Connection pool count'),
_('Connections established in advance.'),
{ datatype: 'uinteger', placeholder: '1' }],
[form.Flag, 'tcp_mux', _('TCP mux'),
_('Enable TCP stream multiplexing.'),
{
enabled: 'true',
disabled: 'false',
default: 'true',
optional: false,
rmempty: false,
retain: true,
remove: writeFlagDisabled
}],
[form.Value, 'tcp_mux_keepalive_interval', _('TCP mux keepalive interval'),
_('Keepalive interval for TCP mux.'),
{ datatype: 'uinteger', placeholder: '30' }],
[form.ListValue, 'protocol', _('Transport protocol'),
_('Transport protocol used to connect to frps.'),
{ values: ['tcp', 'kcp', 'quic', 'websocket', 'wss'], default: 'tcp' }],
[form.ListValue, 'wire_protocol', _('Wire protocol'),
_('frp internal wire protocol. v2 requires frps support and must be enabled explicitly.'),
{ values: ['v1', 'v2'], default: 'v1' }],
[form.Value, 'connect_server_local_ip', _('Connect server local IP'),
_('Local IP bound when connecting to frps. Only valid for tcp, websocket or wss.'),
{
datatype: 'ipaddr',
depends: [{ protocol: 'tcp' }, { protocol: 'websocket' }, { protocol: 'wss' }],
placeholder: '0.0.0.0'
}],
[form.Value, 'heartbeat_interval', _('Heartbeat interval'),
_('Heartbeat interval in seconds.'),
{ datatype: 'integer', placeholder: '30' }],
[form.Value, 'heartbeat_timeout', _('Heartbeat timeout'),
_('Heartbeat timeout in seconds.'),
{ datatype: 'integer', placeholder: '90' }]
];
const commonTlsQuicConf = [
[form.Flag, 'tls_enable', _('TLS'),
_('Enable TLS when communicating with frps. Since frp v0.50.0, default is true.'),
{
enabled: 'true',
disabled: 'false',
default: 'true',
optional: false,
rmempty: false,
retain: true,
remove: writeFlagDisabled
}],
[form.Value, 'tls_cert_file', _('TLS certificate path'),
_('Path to the TLS certificate file.'),
{ datatype: 'file', placeholder: '/etc/ssl/frp/client.crt' }],
[form.Value, 'tls_key_file', _('TLS private key path'),
_('Path to the TLS private key file.'),
{ datatype: 'file', placeholder: '/etc/ssl/frp/client.key' }],
[form.Value, 'tls_trusted_ca_file', _('TLS trusted CA path'),
_('Path to the trusted CA certificate file.'),
{ datatype: 'file', placeholder: '/etc/ssl/frp/ca.crt' }],
[form.Value, 'tls_server_name', _('TLS server name'),
_('Server name used for TLS verification.'),
{ placeholder: 'example.com' }],
[form.Flag, 'disable_custom_tls_first_byte', _('Disable custom TLS first byte'),
_('Since frp v0.50.0, default is true.'),
{
enabled: 'true',
disabled: 'false',
default: 'true',
optional: false,
rmempty: false,
retain: true,
remove: writeFlagDisabled
}],
[form.Value, 'quic_keepalive_period', _('QUIC keepalive period'),
_('Configuration key: transport.quic.keepalivePeriod.'),
{ datatype: 'uinteger', depends: { protocol: 'quic' }, placeholder: '10' }],
[form.Value, 'quic_max_idle_timeout', _('QUIC max idle timeout'),
_('Configuration key: transport.quic.maxIdleTimeout.'),
{ datatype: 'uinteger', depends: { protocol: 'quic' }, placeholder: '30' }],
[form.Value, 'quic_max_incoming_streams', _('QUIC max incoming streams'),
_('Configuration key: transport.quic.maxIncomingStreams.'),
{ datatype: 'uinteger', depends: { protocol: 'quic' }, placeholder: '100000' }]
];
const commonAdvancedConf = [
[form.DynamicList, 'feature_gates', _('Feature gates'),
_('Experimental feature gates. Use key=value, for example VirtualNet=true.'),
{
placeholder: 'VirtualNet=true',
validate: function (section_id, value) {
return validateKeyValue(value);
}
}],
[form.Value, 'virtual_net_address', _('VirtualNet address'),
_('Virtual network address. Requires VirtualNet feature gate.'),
{ placeholder: '100.86.1.1/24' }],
[form.Value, 'store_path', _('Store file path'),
_('Persist runtime proxy and visitor configuration for Web UI or API management.'),
{ datatype: 'file', placeholder: '/etc/frp/frpc_store.json' }],
[form.DynamicList, 'metadatas', _('Client metadata'),
_('Additional client metadata. Use key=value.'),
{
placeholder: 'var1=abc',
validate: function (section_id, value) {
return validateKeyValue(value);
}
}]
];
const proxyOnlyDepends = [
{ type: 'tcp' },
{ type: 'udp' },
{ type: 'http' },
{ type: 'https' },
{ type: 'tcpmux' },
{ type: 'stcp', role: 'server' },
{ type: 'xtcp', role: 'server' },
{ type: 'sudp', role: 'server' }
];
const healthCheckDepends = [
{ type: 'tcp' },
{ type: 'http' },
{ type: 'https' },
{ type: 'tcpmux' }
];
const baseProxyConf = [
[form.Value, 'name', _('Proxy name'), undefined,
{ rmempty: false, optional: false }],
[form.ListValue, 'type', _('Proxy type'),
_('Proxy type.'),
{ values: ['tcp', 'udp', 'http', 'https', 'stcp', 'xtcp', 'sudp', 'tcpmux'], default: 'tcp' }],
[form.Flag, 'enabled', _('Enabled'),
_('Enable or disable this proxy.'),
{
enabled: 'true',
disabled: 'false',
default: 'true',
optional: false,
rmempty: false,
retain: true,
remove: writeFlagDisabled
}],
[form.Value, 'local_ip', _('Local IP'),
_('Local service IP or host.'),
{ datatype: 'host', depends: proxyOnlyDepends }],
[form.Value, 'local_port', _('Local port'),
_('Local service port.'),
{ datatype: 'port', depends: proxyOnlyDepends }],
[form.Value, 'remote_port', _('Remote port'),
_('Remote port listened by frps. If 0, frps assigns a random port.'),
{
validate: validatePortOrZero,
depends: [{ type: 'tcp' }, { type: 'udp' }],
textvalue: function (section_id) {
const v = this.cfgvalue(section_id);
return v && v !== '0' ? v : '#';
}
}]
];
const proxyTransportConf = [
[form.Value, 'bandwidth_limit', _('Bandwidth limit'),
_('Bandwidth limit, for example 1MB.'),
{ placeholder: '1MB', depends: proxyOnlyDepends }],
[form.ListValue, 'bandwidth_limit_mode', _('Bandwidth limit mode'),
_('Where to limit bandwidth.'),
{ values: ['client', 'server'], default: 'client', depends: proxyOnlyDepends }],
[form.Flag, 'use_encryption', _('Encryption'),
_('Encrypt proxy traffic.'),
{ datatype: 'bool', default: 'false' }],
[form.Flag, 'use_compression', _('Compression'),
_('Compress proxy traffic.'),
{ datatype: 'bool', default: 'false' }],
[form.ListValue, 'proxy_protocol_version', _('Proxy protocol version'),
_('Use proxy protocol to transfer connection info to local service.'),
{ values: [['', _('Disabled')], ['v1', 'v1'], ['v2', 'v2']], rmempty: true, depends: proxyOnlyDepends }]
];
const domainConf = [
[form.DynamicList, 'custom_domains', _('Custom domains'),
_('Configuration key: proxies[].customDomains.'),
{ placeholder: 'web01.yourdomain.com' }],
[form.Value, 'subdomain', _('Subdomain'),
_('Configuration key: proxies[].subdomain.')]
];
const httpProxyConf = [
[form.DynamicList, 'locations', _('Locations'),
_('Only valid for HTTP proxy.'),
{
placeholder: '/',
validate: function (section_id, value) {
return validateHttpPath(value);
}
}],
[form.Value, 'host_header_rewrite', _('Host header rewrite'),
_('Rewrite Host header.')],
[form.DynamicList, 'request_headers', _('Request headers'),
_('Set request headers. Use Header=Value.'),
{
placeholder: 'x-from-where=frp',
validate: function (section_id, value) {
return validateKeyValue(value);
}
}],
[form.DynamicList, 'response_headers', _('Response headers'),
_('Set response headers. Use Header=Value.'),
{
placeholder: 'foo=bar',
validate: function (section_id, value) {
return validateKeyValue(value);
}
}]
];
const httpAuthConf = [
[form.Value, 'http_user', _('HTTP user'),
_('HTTP or TCPMUX basic auth username.')],
[form.Value, 'http_pwd', _('HTTP password'),
_('HTTP or TCPMUX basic auth password.'),
{ password: true }]
];
const routeByHTTPUserConf = [
[form.Value, 'route_by_http_user', _('Route by HTTP user'),
_('Route HTTP or TCPMUX requests by HTTP basic auth user.')]
];
const tcpmuxConf = [
[form.ListValue, 'multiplexer', _('Multiplexer'),
_('TCP multiplexer.'),
{ values: ['httpconnect'], depends: { type: 'tcpmux' }, default: 'httpconnect' }]
];
const loadBalancerConf = [
[form.Value, 'group', _('Load balancer group'),
_('Configuration key: proxies[].loadBalancer.group.'),
{ depends: proxyOnlyDepends }],
[form.Value, 'group_key', _('Load balancer group key'),
_('Configuration key: proxies[].loadBalancer.groupKey.'),
{
password: true,
depends: proxyOnlyDepends,
validate: function (section_id, value) {
const group = this.section.getOption('group');
if (String(value || '').trim() && group && !String(group.formvalue(section_id) || '').trim())
return _('Load balancer group is required when group key is set.');
return true;
}
}]
];
const healthCheckConf = [
[form.ListValue, 'health_check_type', _('Health check type'),
_('Health check type.'),
{ values: [['', _('Disabled')], ['tcp', 'tcp'], ['http', 'http']], rmempty: true, depends: healthCheckDepends }],
[form.Value, 'health_check_timeout_s', _('Health check timeout'),
_('Health check timeout in seconds.'),
{ datatype: 'uinteger', depends: [{ health_check_type: 'tcp' }, { health_check_type: 'http' }], placeholder: '3' }],
[form.Value, 'health_check_max_failed', _('Health check max failed'),
_('Remove proxy from frps after continuous failures.'),
{ datatype: 'uinteger', depends: [{ health_check_type: 'tcp' }, { health_check_type: 'http' }], placeholder: '3' }],
[form.Value, 'health_check_interval_s', _('Health check interval'),
_('Health check interval in seconds.'),
{ datatype: 'uinteger', depends: [{ health_check_type: 'tcp' }, { health_check_type: 'http' }], placeholder: '10' }],
[form.Value, 'health_check_url', _('Health check path'),
_('HTTP health check path.'),
{
depends: { health_check_type: 'http' },
placeholder: '/status',
validate: function (section_id, value) {
const healthCheckType = this.section.getOption('health_check_type');
if (
healthCheckType &&
healthCheckType.formvalue(section_id) === 'http' &&
!String(value || '').trim()
)
return _('Health check path is required when health check type is http.');
return validateHttpPath(value);
}
}],
[form.DynamicList, 'health_check_headers', _('Health check headers'),
_('HTTP health check headers. Use Header=Value.'),
{
depends: { health_check_type: 'http' },
placeholder: 'x-from-where=frp',
validate: function (section_id, value) {
return validateKeyValue(value);
}
}]
];
const stcpXtcpConf = [
[form.ListValue, 'role', _('Role'),
_('Compatibility field for UCI. Server sections are emitted as proxy entries; visitor sections are emitted as visitor entries.'),
{ values: ['server', 'visitor'], default: 'server' }],
[form.Value, 'sk', _('Secret key'),
_('Secret key used by STCP/XTCP/SUDP proxies and visitors.'),
{ password: true }],
[form.DynamicList, 'allow_users', _('Allow users'),
_('Only visitors from specified users can connect. Use * to allow all users.'),
{ depends: { role: 'server' }, placeholder: '*' }],
[form.Value, 'server_user', _('Server user'),
_('Server user for visitor.'),
{ depends: { role: 'visitor' } }],
[form.Value, 'server_name', _('Server name'),
_('Server proxy name to visit.'),
{
depends: { role: 'visitor' },
validate: function (section_id, value) {
const role = this.section.getOption('role');
if (role && role.formvalue(section_id) === 'visitor' && !String(value || '').trim())
return _('Server name is required for visitors.');
return true;
}
}],
[form.Value, 'bind_addr', _('Bind address'),
_('Local bind address for visitor.'),
{ depends: { role: 'visitor' }, datatype: 'ipaddr', placeholder: '127.0.0.1' }],
[form.Value, 'bind_port', _('Bind port'),
_('Local bind port for visitor. Use a negative value to avoid binding.'),
{
depends: { role: 'visitor' },
validate: function (section_id, value) {
const role = this.section.getOption('role');
if (role && role.formvalue(section_id) === 'visitor' && !String(value || '').trim())
return _('Bind port is required for visitors.');
return validateVisitorBindPort(section_id, value);
}
}],
[form.ListValue, 'visitor_protocol', _('XTCP protocol'),
_('XTCP visitor tunnel protocol.'),
{ depends: { role: 'visitor', type: 'xtcp' }, values: ['quic', 'kcp'], default: 'quic' }],
[form.Flag, 'keep_tunnel_open', _('Keep tunnel open'),
_('Keep XTCP tunnel open.'),
{ depends: { role: 'visitor', type: 'xtcp' }, datatype: 'bool', default: 'false' }],
[form.Value, 'max_retries_an_hour', _('Max retries per hour'),
_('Effective when XTCP keep tunnel open is enabled.'),
{ depends: { role: 'visitor', type: 'xtcp' }, datatype: 'uinteger', placeholder: '8' }],
[form.Value, 'min_retry_interval', _('Min retry interval'),
_('Minimum XTCP retry interval.'),
{ depends: { role: 'visitor', type: 'xtcp' }, datatype: 'uinteger', placeholder: '90' }],
[form.Value, 'fallback_to', _('Fallback to'),
_('Fallback visitor name for XTCP.'),
{ depends: { role: 'visitor', type: 'xtcp' } }],
[form.Value, 'fallback_timeout_ms', _('Fallback timeout ms'),
_('Fallback timeout in milliseconds for XTCP.'),
{ depends: { role: 'visitor', type: 'xtcp' }, datatype: 'uinteger', placeholder: '500' }],
[form.Flag, 'nat_disable_assisted_addrs', _('Disable NAT assisted addresses'),
_('Disable local interface assisted addresses for XTCP NAT traversal.'),
{ depends: { type: 'xtcp' }, datatype: 'bool', default: 'false' }]
];
const pluginConf = [
[form.ListValue, 'plugin', _('Plugin'),
_('Plugin type.'),
{
values: [
['', _('Disabled')],
['http_proxy', 'http_proxy'],
['socks5', 'socks5'],
['unix_domain_socket', 'unix_domain_socket'],
['static_file', 'static_file'],
['https2http', 'https2http'],
['https2https', 'https2https'],
['http2https', 'http2https'],
['http2http', 'http2http'],
['tls2raw', 'tls2raw'],
['virtual_net', 'virtual_net']
],
rmempty: true,
validate: function (section_id, value) {
return validatePlugin(this.section, section_id, value);
}
}],
[form.Value, 'plugin_http_user', _('HTTP user'),
_('Configuration key: plugin.httpUser.'),
{ depends: [{ plugin: 'http_proxy' }, { plugin: 'static_file' }] }],
[form.Value, 'plugin_http_passwd', _('HTTP password'),
_('Configuration key: plugin.httpPassword.'),
{ depends: [{ plugin: 'http_proxy' }, { plugin: 'static_file' }], password: true }],
[form.Value, 'plugin_user', _('SOCKS5 user'),
_('Configuration key: plugin.username.'),
{ depends: { plugin: 'socks5' } }],
[form.Value, 'plugin_passwd', _('SOCKS5 password'),
_('Configuration key: plugin.password.'),
{ depends: { plugin: 'socks5' }, password: true }],
[form.Value, 'plugin_unix_path', _('Unix domain socket path'),
_('Configuration key: plugin.unixPath.'),
{
depends: { plugin: 'unix_domain_socket' },
optional: false,
rmempty: false,
datatype: 'file',
placeholder: '/var/run/docker.sock',
default: '/var/run/docker.sock'
}],
[form.Value, 'plugin_local_path', _('Static file local path'),
_('Configuration key: plugin.localPath.'),
{ depends: { plugin: 'static_file' }, datatype: 'directory', placeholder: '/var/www/blog' }],
[form.Value, 'plugin_strip_prefix', _('Static file strip prefix'),
_('Configuration key: plugin.stripPrefix.'),
{ depends: { plugin: 'static_file' }, placeholder: 'static' }],
[form.Value, 'plugin_local_addr', _('Plugin local address'),
_('Local backend address used by bridge plugins. For https2http, https2https and tls2raw, this is the local service address reached after frp terminates external TLS; for http2https/http2http, it is the local upstream address.'),
{
depends: [
{ plugin: 'https2http' },
{ plugin: 'https2https' },
{ plugin: 'http2https' },
{ plugin: 'http2http' },
{ plugin: 'tls2raw' }
],
placeholder: '127.0.0.1:80'
}],
[form.Value, 'plugin_crt_path', _('Plugin certificate path'),
_('Configuration key: plugin.crtPath.'),
{
depends: [
{ plugin: 'https2http' },
{ plugin: 'https2https' },
{ plugin: 'tls2raw' }
],
datatype: 'file',
placeholder: './server.crt'
}],
[form.Value, 'plugin_key_path', _('Plugin key path'),
_('Configuration key: plugin.keyPath.'),
{
depends: [
{ plugin: 'https2http' },
{ plugin: 'https2https' },
{ plugin: 'tls2raw' }
],
datatype: 'file',
placeholder: './server.key'
}],
[form.Value, 'plugin_host_header_rewrite', _('Plugin host header rewrite'),
_('Configuration key: plugin.hostHeaderRewrite.'),
{
depends: [
{ plugin: 'https2http' },
{ plugin: 'https2https' },
{ plugin: 'http2https' },
{ plugin: 'http2http' }
],
placeholder: '127.0.0.1'
}],
[form.Flag, 'plugin_enable_http2', _('Plugin HTTP/2'),
_('Enable HTTP/2 for HTTPS bridge plugins.'),
{
depends: [
{ plugin: 'https2http' },
{ plugin: 'https2https' }
],
enabled: 'true',
disabled: 'false',
default: 'true',
optional: false,
rmempty: false,
retain: true,
remove: writeFlagDisabled
}],
[form.DynamicList, 'plugin_request_headers', _('Plugin request headers'),
_('Use Header=Value.'),
{
depends: [
{ plugin: 'https2http' },
{ plugin: 'https2https' },
{ plugin: 'http2https' },
{ plugin: 'http2http' }
],
placeholder: 'x-from-where=frp',
validate: function (section_id, value) {
return validateKeyValue(value);
}
}],
[form.Value, 'plugin_destination_ip', _('VirtualNet destination IP'),
_('For virtual_net visitor plugin.'),
{
depends: { plugin: 'virtual_net', role: 'visitor' },
datatype: 'ipaddr',
placeholder: '100.86.0.1',
validate: function (section_id, value) {
return validateVirtualNetDestination(this.section, section_id, value);
}
}]
];
const metadataConf = [
[form.DynamicList, 'metadatas', _('Proxy metadatas'),
_('Additional proxy metadata. Use key=value.'),
{
depends: proxyOnlyDepends,
placeholder: 'var1=abc',
validate: function (section_id, value) {
return validateKeyValue(value);
}
}],
[form.DynamicList, 'annotations', _('Annotations'),
_('Annotations displayed on frps dashboard. Use key=value.'),
{
depends: proxyOnlyDepends,
placeholder: 'key1=value1',
validate: function (section_id, value) {
return validateKeyValue(value);
}
}]
];
function writeFlagDisabled(section_id) {
return this.write(section_id, this.disabled || 'false');
}
function removeIfPresent(section_id) {
const this_cfg = this.uciconfig || this.section.uciconfig || this.map.config;
const this_sid = this.ucisection || section_id;
const this_opt = this.ucioption || this.option;
for (let i = 0; i < this.section.children.length; i++) {
const sibling = this.section.children[i];
if (sibling === this || sibling.ucioption == null)
continue;
const sibling_cfg = sibling.uciconfig || sibling.section.uciconfig || sibling.map.config;
const sibling_sid = sibling.ucisection || section_id;
const sibling_opt = sibling.ucioption || sibling.option;
if (this_cfg != sibling_cfg || this_sid != sibling_sid || this_opt != sibling_opt)
continue;
if (typeof sibling.isActive === 'function' && sibling.isActive(section_id))
return Promise.resolve();
}
if (this.map.data.get(this_cfg, this_sid, this_opt) == null)
return Promise.resolve();
return this.map.data.unset(this_cfg, this_sid, this_opt);
}
function isUciDeleteNotFoundError(err) {
const message = err && err.message ? err.message : String(err);
return /uci\/delete/.test(message) && /ubus code 4/.test(message);
}
function guardUciDeleteNotFound(data, config) {
data._frpIgnoreMissingDeleteConfigs ??= {};
data._frpIgnoreMissingDeleteConfigs[config] = true;
if (data._frpIgnoreMissingDeleteInstalled)
return;
const callDelete = data.callDelete;
data.callDelete = function(conf, sid, options) {
const guarded = this._frpIgnoreMissingDeleteConfigs && this._frpIgnoreMissingDeleteConfigs[conf];
return callDelete.apply(this, arguments).catch(L.bind(function(err) {
if (!guarded || !isUciDeleteNotFoundError(err))
return Promise.reject(err);
if (!Array.isArray(options) || options.length <= 1)
return null;
return Promise.all(options.map(L.bind(function(opt) {
return callDelete.call(this, conf, sid, [ opt ]).catch(function(e) {
return isUciDeleteNotFoundError(e) ? null : Promise.reject(e);
});
}, this)));
}, this));
};
data._frpIgnoreMissingDeleteInstalled = true;
}
function adminWebEnabled(section, section_id) {
const port = section.getOption('admin_port');
const value = port ? port.formvalue(section_id) : null;
return !!(value && value !== '0');
}
function adminHttpsEnabled(section, section_id) {
const enable = section.getOption('admin_tls_enable');
return !!(enable && enable.formvalue(section_id) === 'true');
}
function validatePortOrZero(section_id, value) {
if (!value)
return true;
value = String(value).trim();
if (!/^\d+$/.test(value))
return _('Port must be a number between 0 and 65535.');
const port = Number(value);
if (port < 0 || port > 65535)
return _('Port must be between 0 and 65535.');
return true;
}
function validateVisitorBindPort(section_id, value) {
if (!value)
return true;
value = String(value).trim();
if (/^-\d+$/.test(value) && Number(value) < 0)
return true;
if (!/^\d+$/.test(value))
return _('Port must be negative or a number between 1 and 65535.');
const port = Number(value);
if (port < 1 || port > 65535)
return _('Port must be negative or between 1 and 65535.');
return true;
}
function validateEnv(value) {
if (!value)
return true;
if (/\r|\n/.test(String(value)) || !/^[A-Za-z_][A-Za-z0-9_]*=.*$/.test(String(value)))
return _('Environment variable must use KEY=value format.');
return true;
}
function validateHttpPath(value) {
if (!value)
return true;
if (/\r|\n/.test(String(value)) || String(value).charAt(0) !== '/')
return _('HTTP path must start with /.');
return true;
}
function validateKeyValue(value) {
if (!value)
return true;
if (/[\r\n]/.test(String(value)) || !/^[^=\s][^=]*=.*$/.test(String(value)))
return _('Please use KEY=value format.');
return true;
}
function validatePlugin(section, section_id, value) {
const role = section.getOption('role');
const roleValue = role ? role.formvalue(section_id) : null;
if (roleValue === 'visitor' && value && value !== 'virtual_net')
return _('Only the virtual_net plugin is valid for visitors.');
return true;
}
function validateVirtualNetDestination(section, section_id, value) {
const plugin = section.getOption('plugin');
const role = section.getOption('role');
if (
plugin && plugin.formvalue(section_id) === 'virtual_net' &&
role && role.formvalue(section_id) === 'visitor' &&
!String(value || '').trim()
)
return _('VirtualNet destination IP is required for virtual_net visitors.');
return true;
}
function normalizeDepends(depends) {
if (depends == null)
return [];
return Array.isArray(depends) ? depends : [ depends ];
}
function mergeDepends(existing, next) {
const current = normalizeDepends(existing);
const incoming = normalizeDepends(next);
if (current.length === 0)
return incoming;
if (incoming.length === 0)
return current;
const merged = [];
for (let oldDep of current) {
for (let newDep of incoming) {
const dep = {};
let conflict = false;
for (let key in oldDep)
dep[key] = oldDep[key];
for (let key in newDep) {
if (Object.prototype.hasOwnProperty.call(dep, key) && dep[key] !== newDep[key]) {
conflict = true;
break;
}
dep[key] = newDep[key];
}
if (!conflict)
merged.push(dep);
}
}
return merged;
}
function setParams(o, params) {
if (!params)
return;
for (let key in params) {
let val = params[key];
if (key === 'values') {
for (let v of val) {
let args = v;
if (!Array.isArray(args))
args = [args];
o.value.apply(o, args);
}
} else if (key === 'depends') {
o.deps = mergeDepends(o.deps, val);
} else {
o[key] = params[key];
}
}
if (params['datatype'] === 'bool') {
o.enabled = 'true';
o.disabled = 'false';
}
}
function defTabOpts(s, t, opts, params) {
for (let opt of opts) {
const o = s.taboption(t, opt[0], opt[1], opt[2], opt[3]);
setParams(o, opt[4]);
setParams(o, params);
/*
* Per-option optional must win over tab-wide optional.
* This is important for form.Flag with default='true',
* otherwise LuCI may treat checked state as default and call remove().
*/
if (opt[4] && Object.prototype.hasOwnProperty.call(opt[4], 'optional'))
o.optional = opt[4].optional;
if (
!(opt[4] && Object.prototype.hasOwnProperty.call(opt[4], 'remove')) &&
!(params && Object.prototype.hasOwnProperty.call(params, 'remove'))
)
o.remove = removeIfPresent;
}
}
function defOpts(s, opts, params) {
for (let opt of opts) {
const o = s.option(opt[0], opt[1], opt[2], opt[3]);
setParams(o, opt[4]);
setParams(o, params);
if (opt[4] && Object.prototype.hasOwnProperty.call(opt[4], 'optional'))
o.optional = opt[4].optional;
if (
!(opt[4] && Object.prototype.hasOwnProperty.call(opt[4], 'remove')) &&
!(params && Object.prototype.hasOwnProperty.call(params, 'remove'))
)
o.remove = removeIfPresent;
}
}
const callServiceList = rpc.declare({
object: 'service',
method: 'list',
params: ['name'],
expect: { '': {} }
});
function getServiceStatus() {
return L.resolveDefault(callServiceList('frpc'), {}).then(function (res) {
const instances = res.frpc && res.frpc.instances;
if (!instances)
return false;
for (let name in instances) {
if (instances[name] && instances[name].running)
return true;
}
return false;
});
}
function renderStatus(isRunning) {
const color = isRunning ? 'green' : 'red';
const status = isRunning ? _('RUNNING') : _('NOT RUNNING');
return String.format('<em><span style="color:%s"><strong>%s %s</strong></span></em>',
color, _('frp Client'), status);
}
return view.extend({
render() {
let m, s, o;
m = new form.Map('frpc', _('frp Client'));
guardUciDeleteNotFound(m.data, 'frpc');
s = m.section(form.NamedSection, '_status');
s.anonymous = true;
s.render = function (section_id) {
L.Poll.add(function () {
return L.resolveDefault(getServiceStatus()).then(function (res) {
const view = document.getElementById('service_status');
if (view)
view.innerHTML = renderStatus(res);
});
});
return E('div', { class: 'cbi-map' },
E('fieldset', { class: 'cbi-section' }, [
E('p', { id: 'service_status' }, _('Collecting data ...'))
])
);
};
s = m.section(form.NamedSection, 'common', 'conf');
s.dynamic = true;
s.tab('common', _('Common Settings'));
s.tab('auth', _('Authentication'));
s.tab('transport', _('Transport Settings'));
s.tab('tls_quic', _('TLS / QUIC'));
s.tab('web', _('Web Server'));
s.tab('advanced', _('Advanced'));
s.tab('log', _('Log Settings'));
s.tab('init', _('Startup Settings'));
defTabOpts(s, 'common', commonBaseConf, { optional: true });
defTabOpts(s, 'auth', commonAuthConf, { optional: true });
defTabOpts(s, 'transport', commonTransportConf, { optional: true });
defTabOpts(s, 'tls_quic', commonTlsQuicConf, { optional: true });
defTabOpts(s, 'web', commonWebConf, { optional: true });
defTabOpts(s, 'advanced', commonAdvancedConf, { optional: true });
defTabOpts(s, 'log', commonLogConf, { optional: true });
o = s.taboption('init', form.SectionValue, 'init', form.TypedSection, 'init', _('Startup Settings'));
s = o.subsection;
s.anonymous = true;
s.dynamic = true;
defOpts(s, startupConf);
s = m.section(form.GridSection, 'conf', _('Proxy / Visitor Settings'));
s.anonymous = true;
s.addremove = true;
s.sortable = true;
s.nodescriptions = true;
s.addbtntitle = _('Add new proxy or visitor...');
s.filter = function (section_id) {
return section_id !== 'common';
};
s.tab('general', _('General Settings'));
s.tab('transport', _('Transport'));
s.tab('http', _('HTTP / Domain'));
s.tab('plugin', _('Plugin'));
s.tab('stcp_xtcp', _('STCP / XTCP / SUDP'));
s.tab('health', _('Health Check'));
s.tab('lb', _('Load Balancer'));
s.tab('metadata', _('Metadata'));
defTabOpts(s, 'general', baseProxyConf, { modalonly: null });
defTabOpts(s, 'transport', proxyTransportConf, { optional: true, modalonly: true });
defTabOpts(s, 'http', domainConf, {
optional: true,
modalonly: true,
depends: [{ type: 'http' }, { type: 'https' }, { type: 'tcpmux' }]
});
defTabOpts(s, 'http', httpProxyConf, {
optional: true,
modalonly: true,
depends: { type: 'http' }
});
defTabOpts(s, 'http', httpAuthConf, {
optional: true,
modalonly: true,
depends: [{ type: 'http' }, { type: 'tcpmux' }]
});
defTabOpts(s, 'http', routeByHTTPUserConf, {
optional: true,
modalonly: true,
depends: [{ type: 'http' }, { type: 'tcpmux' }]
});
defTabOpts(s, 'http', tcpmuxConf, {
optional: true,
modalonly: true,
depends: { type: 'tcpmux' }
});
defTabOpts(s, 'plugin', pluginConf, {
optional: true,
modalonly: true
});
defTabOpts(s, 'stcp_xtcp', stcpXtcpConf, {
optional: true,
modalonly: true,
depends: [{ type: 'stcp' }, { type: 'xtcp' }, { type: 'sudp' }]
});
defTabOpts(s, 'health', healthCheckConf, {
optional: true,
modalonly: true,
depends: healthCheckDepends
});
defTabOpts(s, 'lb', loadBalancerConf, {
optional: true,
modalonly: true
});
defTabOpts(s, 'metadata', metadataConf, {
optional: true,
modalonly: true
});
return m.render();
}
});