/* * SPDX-License-Identifier: GPL-2.0-only * * Copyright (C) 2022-2023 ImmortalWrt.org */ 'use strict'; 'require form'; 'require poll'; 'require rpc'; 'require uci'; 'require view'; 'require homeproxy as hp'; var callServiceList = rpc.declare({ object: 'service', method: 'list', params: ['name'], expect: { '': {} } }); function getServiceStatus() { return L.resolveDefault(callServiceList('homeproxy'), {}).then((res) => { var isRunning = false; try { isRunning = res['homeproxy']['instances']['sing-box-s']['running']; } catch (e) { } return isRunning; }); } function renderStatus(isRunning) { var spanTemp = '%s %s'; var renderHTML; if (isRunning) renderHTML = spanTemp.format('green', _('HomeProxy Server'), _('RUNNING')); else renderHTML = spanTemp.format('red', _('HomeProxy Server'), _('NOT RUNNING')); return renderHTML; } return view.extend({ load: function() { return Promise.all([ uci.load('homeproxy'), hp.getBuiltinFeatures() ]); }, render: function(data) { var m, s, o; var features = data[1]; m = new form.Map('homeproxy', _('HomeProxy Server'), _('The modern ImmortalWrt proxy platform for ARM64/AMD64.')); s = m.section(form.TypedSection); s.render = function () { poll.add(function () { return L.resolveDefault(getServiceStatus()).then((res) => { var view = document.getElementById('service_status'); view.innerHTML = renderStatus(res); }); }); return E('div', { class: 'cbi-section', id: 'status_bar' }, [ E('p', { id: 'service_status' }, _('Collecting data...')) ]); } s = m.section(form.NamedSection, 'server', 'homeproxy', _('Global settings')); o = s.option(form.Flag, 'enabled', _('Enable')); o.default = o.disabled; o.rmempty = false; o = s.option(form.Flag, 'auto_firewall', _('Auto configure firewall')); o.default = o.disabled; o.rmempty = false; s = m.section(form.GridSection, 'server', _('Server settings')); s.addremove = true; s.rowcolors = true; s.sortable = true; s.nodescriptions = true; s.modaltitle = L.bind(hp.loadModalTitle, this, _('Server'), _('Add a server'), data[0]); s.sectiontitle = L.bind(hp.loadDefaultLabel, this, data[0]); s.renderSectionAdd = L.bind(hp.renderSectionAdd, this, s); o = s.option(form.Value, 'label', _('Label')); o.load = L.bind(hp.loadDefaultLabel, this, data[0]); o.validate = L.bind(hp.validateUniqueValue, this, data[0], 'server', 'label'); o.rmempty = false; o.modalonly = true; o = s.option(form.Flag, 'enabled', _('Enable')); o.default = o.enabled; o.rmempty = false; o.editable = true; o = s.option(form.ListValue, 'type', _('Type')); o.value('http', _('HTTP')); if (features.with_quic) { o.value('hysteria', _('Hysteria')); o.value('hysteria2', _('Hysteria2')); o.value('naive', _('NaïveProxy')); } o.value('shadowsocks', _('Shadowsocks')); o.value('socks', _('Socks')); o.value('trojan', _('Trojan')); if (features.with_quic) o.value('tuic', _('Tuic')); o.value('vless', _('VLESS')); o.value('vmess', _('VMess')); o.rmempty = false; o = s.option(form.Value, 'address', _('Listen address')); o.placeholder = '::'; o.datatype = 'ipaddr'; o.modalonly = true; o = s.option(form.Value, 'port', _('Listen port'), _('The port must be unique.')); o.datatype = 'port'; o.validate = L.bind(hp.validateUniqueValue, this, data[0], 'server', 'port'); o = s.option(form.Value, 'username', _('Username')); o.depends('type', 'http'); o.depends('type', 'naive'); o.depends('type', 'socks'); o.modalonly = true; o = s.option(form.Value, 'password', _('Password')); o.password = true; o.depends({'type': /^(http|naive|socks)$/, 'username': /[\s\S]/}); o.depends('type', 'hysteria2'); o.depends('type', 'shadowsocks'); o.depends('type', 'trojan'); o.depends('type', 'tuic'); o.validate = function(section_id, value) { if (section_id) { var type = this.map.lookupOption('type', section_id)[0].formvalue(section_id); var required_type = [ 'http', 'naive', 'socks', 'shadowsocks' ]; if (required_type.includes(type)) { if (type === 'shadowsocks') { var encmode = this.map.lookupOption('shadowsocks_encrypt_method', section_id)[0].formvalue(section_id); if (encmode === 'none') return true; else if (encmode === '2022-blake3-aes-128-gcm') return hp.validateBase64Key(24, section_id, value); else if (['2022-blake3-aes-256-gcm', '2022-blake3-chacha20-poly1305'].includes(encmode)) return hp.validateBase64Key(44, section_id, value); } if (!value) return _('Expecting: %s').format(_('non-empty value')); } } return true; } o.modalonly = true; /* Hysteria (2) config start */ o = s.option(form.ListValue, 'hysteria_protocol', _('Protocol')); o.value('udp'); /* WeChat-Video / FakeTCP are unsupported by sing-box currently o.value('wechat-video'); o.value('faketcp'); */ o.default = 'udp'; o.depends('type', 'hysteria'); o.rmempty = false; o.modalonly = true; o = s.option(form.Value, 'hysteria_down_mbps', _('Max download speed'), _('Max download speed in Mbps.')); o.datatype = 'uinteger'; o.depends('type', 'hysteria'); o.depends('type', 'hysteria2'); o.modalonly = true; o = s.option(form.Value, 'hysteria_up_mbps', _('Max upload speed'), _('Max upload speed in Mbps.')); o.datatype = 'uinteger'; o.depends('type', 'hysteria'); o.depends('type', 'hysteria2'); o.modalonly = true; o = s.option(form.ListValue, 'hysteria_auth_type', _('Authentication type')); o.value('', _('Disable')); o.value('base64', _('Base64')); o.value('string', _('String')); o.depends('type', 'hysteria'); o.modalonly = true; o = s.option(form.Value, 'hysteria_auth_payload', _('Authentication payload')); o.depends({'type': 'hysteria', 'hysteria_auth_type': /[\s\S]/}); o.rmempty = false; o.modalonly = true; o = s.option(form.ListValue, 'hysteria_obfs_type', _('Obfuscate type')); o.value('', _('Disable')); o.value('salamander', _('Salamander')); o.depends('type', 'hysteria2'); o.modalonly = true; o = s.option(form.Value, 'hysteria_obfs_password', _('Obfuscate password')); o.depends('type', 'hysteria'); o.depends({'type': 'hysteria2', 'hysteria_obfs_type': /[\s\S]/}); o.modalonly = true; o = s.option(form.Value, 'hysteria_recv_window_conn', _('QUIC stream receive window'), _('The QUIC stream-level flow control window for receiving data.')); o.datatype = 'uinteger'; o.default = '67108864'; o.depends('type', 'hysteria'); o.modalonly = true; o = s.option(form.Value, 'hysteria_recv_window_client', _('QUIC connection receive window'), _('The QUIC connection-level flow control window for receiving data.')); o.datatype = 'uinteger'; o.default = '15728640'; o.depends('type', 'hysteria'); o.modalonly = true; o = s.option(form.Value, 'hysteria_max_conn_client', _('QUIC maximum concurrent bidirectional streams'), _('The maximum number of QUIC concurrent bidirectional streams that a peer is allowed to open.')); o.datatype = 'uinteger'; o.default = '1024'; o.depends('type', 'hysteria'); o.modalonly = true; o = s.option(form.Flag, 'hysteria_disable_mtu_discovery', _('Disable Path MTU discovery'), _('Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.')); o.default = o.disabled; o.depends('type', 'hysteria'); o.modalonly = true; o = s.option(form.Flag, 'hysteria_ignore_client_bandwidth', _('Ignore client bandwidth'), _('Tell the client to use the BBR flow control algorithm instead of Hysteria CC.')); o.default = o.disabled; o.depends({'type': 'hysteria2', 'hysteria_down_mbps': '', 'hysteria_up_mbps': ''}); o.modalonly = true; o = s.option(form.Value, 'hysteria_masquerade', _('Masquerade'), _('HTTP3 server behavior when authentication fails.
A 404 page will be returned if empty.')); o.depends('type', 'hysteria2'); o.modalonly = true; /* Hysteria (2) config end */ /* Shadowsocks config */ o = s.option(form.ListValue, 'shadowsocks_encrypt_method', _('Encrypt method')); for (var i of hp.shadowsocks_encrypt_methods) o.value(i); o.default = 'aes-128-gcm'; o.depends('type', 'shadowsocks'); o.modalonly = true; /* Tuic config start */ o = s.option(form.Value, 'uuid', _('UUID')); o.depends('type', 'tuic'); o.depends('type', 'vless'); o.depends('type', 'vmess'); o.validate = hp.validateUUID; o.modalonly = true; o = s.option(form.ListValue, 'tuic_congestion_control', _('Congestion control algorithm'), _('QUIC congestion control algorithm.')); o.value('cubic'); o.value('new_reno'); o.value('bbr'); o.default = 'cubic'; o.depends('type', 'tuic'); o.modalonly = true; o = s.option(form.ListValue, 'tuic_auth_timeout', _('Auth timeout'), _('How long the server should wait for the client to send the authentication command (in seconds).')); o.datatype = 'uinteger'; o.default = '3'; o.depends('type', 'tuic'); o.modalonly = true; o = s.option(form.Flag, 'tuic_enable_zero_rtt', _('Enable 0-RTT handshake'), _('Enable 0-RTT QUIC connection handshake on the client side. This is not impacting much on the performance, as the protocol is fully multiplexed.
' + 'Disabling this is highly recommended, as it is vulnerable to replay attacks.')); o.default = o.disabled; o.depends('type', 'tuic'); o.modalonly = true; o = s.option(form.Value, 'tuic_heartbeat', _('Heartbeat interval'), _('Interval for sending heartbeat packets for keeping the connection alive (in seconds).')); o.datatype = 'uinteger'; o.default = '10'; o.depends('type', 'tuic'); o.modalonly = true; /* Tuic config end */ /* VLESS / VMess config start */ o = s.option(form.ListValue, 'vless_flow', _('Flow')); o.value('', _('None')); o.value('xtls-rprx-vision'); o.depends('type', 'vless'); o.modalonly = true; o = s.option(form.Value, 'vmess_alterid', _('Alter ID'), _('Legacy protocol support (VMess MD5 Authentication) is provided for compatibility purposes only, use of alterId > 1 is not recommended.')); o.datatype = 'uinteger'; o.depends('type', 'vmess'); o.modalonly = true; /* VMess config end */ /* Transport config start */ o = s.option(form.ListValue, 'transport', _('Transport'), _('No TCP transport, plain HTTP is merged into the HTTP transport.')); o.value('', _('None')); o.value('grpc', _('gRPC')); o.value('http', _('HTTP')); o.value('quic', _('QUIC')); o.value('ws', _('WebSocket')); o.onchange = function(ev, section_id, value) { var desc = this.map.findElement('id', 'cbid.homeproxy.%s.transport'.format(section_id)).nextElementSibling; if (value === 'http') desc.innerHTML = _('TLS is not enforced. If TLS is not configured, plain HTTP 1.1 is used.'); else if (value === 'quic') desc.innerHTML = _('No additional encryption support: It\'s basically duplicate encryption.'); else desc.innerHTML = _('No TCP transport, plain HTTP is merged into the HTTP transport.'); var tls_element = this.map.findElement('id', 'cbid.homeproxy.%s.tls'.format(section_id)).firstElementChild; if ((value === 'http' && tls_element.checked) || (value === 'grpc' && !features.with_grpc)) this.map.findElement('id', 'cbid.homeproxy.%s.http_idle_timeout'.format(section_id)).nextElementSibling.innerHTML = _('Specifies the time (in seconds) until idle clients should be closed with a GOAWAY frame. PING frames are not considered as activity.'); else if (value === 'grpc' && features.with_grpc) this.map.findElement('id', 'cbid.homeproxy.%s.http_idle_timeout'.format(section_id)).nextElementSibling.innerHTML = _('If the transport doesn\'t see any activity after a duration of this time (in seconds), it pings the client to check if the connection is still active.'); } o.depends('type', 'trojan'); o.depends('type', 'vless'); o.depends('type', 'vmess'); o.modalonly = true; /* gRPC config start */ o = s.option(form.Value, 'grpc_servicename', _('gRPC service name')); o.depends('transport', 'grpc'); o.modalonly = true; /* gRPC config end */ /* HTTP config start */ o = s.option(form.DynamicList, 'http_host', _('Host')); o.datatype = 'hostname'; o.depends('transport', 'http'); o.modalonly = true; o = s.option(form.Value, 'http_path', _('Path')); o.depends('transport', 'http'); o.modalonly = true; o = s.option(form.Value, 'http_method', _('Method')); o.depends('transport', 'http'); o.modalonly = true; o = s.option(form.Value, 'http_idle_timeout', _('Idle timeout'), _('Specifies the time (in seconds) until idle clients should be closed with a GOAWAY frame. PING frames are not considered as activity.')); o.datatype = 'uinteger'; o.depends('transport', 'grpc'); o.depends({'transport': 'http', 'tls': '1'}); o.modalonly = true; if (features.with_grpc) { o = s.option(form.Value, 'http_ping_timeout', _('Ping timeout'), _('The timeout (in seconds) that after performing a keepalive check, the client will wait for activity. If no activity is detected, the connection will be closed.')); o.datatype = 'uinteger'; o.depends('transport', 'grpc'); o.modalonly = true; } /* HTTP config end */ /* WebSocket config start */ o = s.option(form.Value, 'ws_host', _('Host')); o.depends('transport', 'ws'); o.modalonly = true; o = s.option(form.Value, 'ws_path', _('Path')); o.depends('transport', 'ws'); o.modalonly = true; o = s.option(form.Value, 'websocket_early_data', _('Early data'), _('Allowed payload size is in the request.')); o.datatype = 'uinteger'; o.value('2048'); o.depends('transport', 'ws'); o.modalonly = true; o = s.option(form.Value, 'websocket_early_data_header', _('Early data header name'), _('Early data is sent in path instead of header by default.') + '
' + _('To be compatible with Xray-core, set this to Sec-WebSocket-Protocol.')); o.value('Sec-WebSocket-Protocol'); o.depends('transport', 'ws'); o.modalonly = true; /* WebSocket config end */ /* Transport config end */ /* TLS config start */ o = s.option(form.Flag, 'tls', _('TLS')); o.default = o.disabled; o.depends('type', 'http'); o.depends('type', 'hysteria'); o.depends('type', 'hysteria2'); o.depends('type', 'naive'); o.depends('type', 'trojan'); o.depends('type', 'vless'); o.depends('type', 'vmess'); o.rmempty = false; o.validate = function(section_id, value) { if (section_id) { var type = this.map.lookupOption('type', section_id)[0].formvalue(section_id); var tls = this.map.findElement('id', 'cbid.homeproxy.%s.tls'.format(section_id)).firstElementChild; if (['hysteria', 'hysteria2', 'tuic'].includes(type)) { tls.checked = true; tls.disabled = true; } else { tls.disabled = null; } } return true; } o.modalonly = true; o = s.option(form.Value, 'tls_sni', _('TLS SNI'), _('Used to verify the hostname on the returned certificates unless insecure is given.')); o.depends({'tls': '1', 'tls_reality': '0'}); o.depends({'tls': '1', 'tls_reality': null}); o.modalonly = true; o = s.option(form.DynamicList, 'tls_alpn', _('TLS ALPN'), _('List of supported application level protocols, in order of preference.')); o.depends('tls', '1'); o.modalonly = true; o = s.option(form.ListValue, 'tls_min_version', _('Minimum TLS version'), _('The minimum TLS version that is acceptable.')); o.value('', _('default')); for (var i of hp.tls_versions) o.value(i); o.depends('tls', '1'); o.modalonly = true; o = s.option(form.ListValue, 'tls_max_version', _('Maximum TLS version'), _('The maximum TLS version that is acceptable.')); o.value('', _('default')); for (var i of hp.tls_versions) o.value(i); o.depends('tls', '1'); o.modalonly = true; o = s.option(form.MultiValue, 'tls_cipher_suites', _('Cipher suites'), _('The elliptic curves that will be used in an ECDHE handshake, in preference order. If empty, the default will be used.')); for (var i of hp.tls_cipher_suites) o.value(i); o.depends('tls', '1'); o.optional = true; o.modalonly = true; if (features.with_acme) { o = s.option(form.Flag, 'tls_acme', _('Enable ACME'), _('Use ACME TLS certificate issuer.')); o.default = o.disabled; o.depends('tls', '1'); o.modalonly = true; o = s.option(form.DynamicList, 'tls_acme_domain', _('Domains')); o.datatype = 'hostname'; o.depends('tls_acme', '1'); o.rmempty = false; o.modalonly = true; o = s.option(form.Value, 'tls_acme_dsn', _('Default server name'), _('Server name to use when choosing a certificate if the ClientHello\'s ServerName field is empty.')); o.depends('tls_acme', '1'); o.rmempty = false; o.modalonly = true; o = s.option(form.Value, 'tls_acme_email', _('Email'), _('The email address to use when creating or selecting an existing ACME server account.')); o.depends('tls_acme', '1'); o.validate = function(section_id, value) { if (section_id) { if (!value) return _('Expecting: %s').format('non-empty value'); else if (!value.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) return _('Expecting: %s').format('valid email address'); } return true; } o.modalonly = true; o = s.option(form.Value, 'tls_acme_provider', _('CA provider'), _('The ACME CA provider to use.')); o.value('letsencrypt', _('Let\'s Encrypt')); o.value('zerossl', _('ZeroSSL')); o.depends('tls_acme', '1'); o.rmempty = false; o.modalonly = true; o = s.option(form.Flag, 'tls_dns01_challenge', _('DNS01 challenge')) o.default = o.disabled; o.depends('tls_acme', '1'); o.modalonly = true; o = s.option(form.ListValue, 'tls_dns01_provider', _('DNS provider')); o.value('alidns', _('Alibaba Cloud DNS')); o.value('cloudflare', _('Cloudflare')); o.depends('tls_dns01_challenge', '1'); o.default = 'cloudflare'; o.rmempty = false; o.modalonly = true; o = s.option(form.Value, 'tls_dns01_ali_akid', _('Access key ID')); o.depends('tls_dns01_provider', 'alidns'); o.rmempty = false; o.modalonly = true; o = s.option(form.Value, 'tls_dns01_ali_aksec', _('Access key secret')); o.depends('tls_dns01_provider', 'alidns'); o.rmempty = false; o.modalonly = true; o = s.option(form.Value, 'tls_dns01_ali_rid', _('Region ID')); o.depends('tls_dns01_provider', 'alidns'); o.rmempty = false; o.modalonly = true; o = s.option(form.Value, 'tls_dns01_cf_api_token', _('API token')); o.depends('tls_dns01_provider', 'cloudflare'); o.rmempty = false; o.modalonly = true; o = s.option(form.Flag, 'tls_acme_dhc', _('Disable HTTP challenge')); o.default = o.disabled; o.depends('tls_dns01_challenge', '0'); o.modalonly = true; o = s.option(form.Flag, 'tls_acme_dtac', _('Disable TLS ALPN challenge')); o.default = o.disabled; o.depends('tls_dns01_challenge', '0'); o.modalonly = true; o = s.option(form.Value, 'tls_acme_ahp', _('Alternative HTTP port'), _('The alternate port to use for the ACME HTTP challenge; if non-empty, this port will be used instead of 80 to spin up a listener for the HTTP challenge.')); o.datatype = 'port'; o.depends('tls_dns01_challenge', '0'); o.modalonly = true; o = s.option(form.Value, 'tls_acme_atp', _('Alternative TLS port'), _('The alternate port to use for the ACME TLS-ALPN challenge; the system must forward 443 to this port for challenge to succeed.')); o.datatype = 'port'; o.depends('tls_dns01_challenge', '0'); o.modalonly = true; o = s.option(form.Flag, 'tls_acme_external_account', _('External Account Binding'), _('EAB (External Account Binding) contains information necessary to bind or map an ACME account to some other account known by the CA.' + '
External account bindings are "used to associate an ACME account with an existing account in a non-ACME system, such as a CA customer database.')); o.default = o.disabled; o.depends('tls_acme', '1'); o.modalonly = true; o = s.option(form.Value, 'tls_acme_ea_keyid', _('External account key ID')); o.depends('tls_acme_external_account', '1'); o.rmempty = false; o.modalonly = true; o = s.option(form.Value, 'tls_acme_ea_mackey', _('External account MAC key')); o.depends('tls_acme_external_account', '1'); o.rmempty = false; o.modalonly = true; } if (features.with_reality_server) { o = s.option(form.Flag, 'tls_reality', _('REALITY')); o.default = o.disabled; o.depends({'tls': '1', 'tls_acme': '0', 'type': 'vless'}); o.depends({'tls': '1', 'tls_acme': null, 'type': 'vless'}); o.modalonly = true; o = s.option(form.Value, 'tls_reality_private_key', _('REALITY private key')); o.depends('tls_reality', '1'); o.rmempty = false; o.modalonly = true; o = s.option(form.DynamicList, 'tls_reality_short_id', _('REALITY short ID')); o.depends('tls_reality', '1'); o.rmempty = false; o.modalonly = true; o = s.option(form.Value, 'tls_reality_max_time_difference', _('Max time difference'), _('The maximum time difference between the server and the client.')); o.depends('tls_reality', '1'); o.modalonly = true; o = s.option(form.Value, 'tls_reality_server_addr', _('Handshake server address')); o.datatype = 'hostname'; o.depends('tls_reality', '1'); o.rmempty = false; o.modalonly = true; o = s.option(form.Value, 'tls_reality_server_port', _('Handshake server port')); o.datatype = 'port'; o.depends('tls_reality', '1'); o.rmempty = false; o.modalonly = true; } o = s.option(form.Value, 'tls_cert_path', _('Certificate path'), _('The server public key, in PEM format.')); o.value('/etc/homeproxy/certs/server_publickey.pem'); o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': null}); o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': '0'}); o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': '0'}); o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': null}); o.rmempty = false; o.modalonly = true; o = s.option(form.Button, '_upload_cert', _('Upload certificate'), _('Save your configuration before uploading files!')); o.inputstyle = 'action'; o.inputtitle = _('Upload...'); o.depends({'tls': '1', 'tls_cert_path': '/etc/homeproxy/certs/server_publickey.pem'}); o.onclick = L.bind(hp.uploadCertificate, this, _('certificate'), 'server_publickey'); o.modalonly = true; o = s.option(form.Value, 'tls_key_path', _('Key path'), _('The server private key, in PEM format.')); o.value('/etc/homeproxy/certs/server_privatekey.pem'); o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': '0'}); o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': null}); o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': '0'}); o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': null}); o.rmempty = false; o.modalonly = true; o = s.option(form.Button, '_upload_key', _('Upload key'), _('Save your configuration before uploading files!')); o.inputstyle = 'action'; o.inputtitle = _('Upload...'); o.depends({'tls': '1', 'tls_key_path': '/etc/homeproxy/certs/server_privatekey.pem'}); o.onclick = L.bind(hp.uploadCertificate, this, _('private key'), 'server_privatekey'); o.modalonly = true; /* TLS config end */ /* Extra settings start */ o = s.option(form.Flag, 'tcp_fast_open', _('TCP fast open'), _('Enable tcp fast open for listener.')); o.default = o.disabled; o.depends({'network': 'udp', '!reverse': true}); o.modalonly = true; if (features.has_mptcp) { o = s.option(form.Flag, 'tcp_multi_path', _('MultiPath TCP')); o.default = o.disabled; o.depends({'network': 'udp', '!reverse': true}); o.modalonly = true; } o = s.option(form.Flag, 'udp_fragment', _('UDP Fragment'), _('Enable UDP fragmentation.')); o.default = o.disabled; o.depends({'network': 'tcp', '!reverse': true}); o.modalonly = true; o = s.option(form.Flag, 'sniff_override', _('Override destination'), _('Override the connection destination address with the sniffed domain.')); o.rmempty = false; o = s.option(form.ListValue, 'domain_strategy', _('Domain strategy'), _('If set, the requested domain name will be resolved to IP before routing.')); for (var i in hp.dns_strategy) o.value(i, hp.dns_strategy[i]) o.modalonly = true; o = s.option(form.ListValue, 'network', _('Network')); o.value('tcp', _('TCP')); o.value('udp', _('UDP')); o.value('', _('Both')); o.depends('type', 'naive'); o.depends('type', 'shadowsocks'); o.modalonly = true; /* Extra settings end */ return m.render(); } });