Add Fortinet hostcheck support

This commit is contained in:
世界
2026-08-30 17:41:44 +08:00
parent 866f93fa2d
commit b491cac8f7
6 changed files with 114 additions and 44 deletions
@@ -58,6 +58,10 @@
} }
] ]
}, },
"fortinet_host_check": {
"hostcheck": "",
"check_virtual_desktop": ""
},
"no_udp": false, "no_udp": false,
"dtls_local_port": 0, "dtls_local_port": 0,
"compression_disabled": false, "compression_disabled": false,
@@ -342,6 +346,30 @@ TNCC machine certificate path in PEM format.
Conflict with `tncc.certificates.certificate`. Conflict with `tncc.certificates.certificate`.
### fortinet_host_check
Fortinet hostcheck result override.
Hostcheck is disabled by default. It is enabled only when `fortinet_host_check.hostcheck` is non-empty. No operating system, security product, or network interface information is collected automatically.
When enabled and a successful Fortinet login response requests hostcheck, both configured values are submitted to the server before the VPN session is used. The values are sent unchanged as `application/x-www-form-urlencoded` fields.
Some Fortinet servers only request hostcheck from recognized FortiClient user agents. Configure `user_agent` when required by the server policy.
### fortinet_host_check.hostcheck
Fortinet hostcheck result string.
The conventional format is `<security-status>,<os-version>`, for example `0100,10.0.19042`. `security-status` contains four `0` or `1` characters representing, in order, third-party firewall, third-party antivirus, FortiClient firewall, and FortiClient antivirus.
An empty value disables Fortinet hostcheck, even if `fortinet_host_check.check_virtual_desktop` is configured.
### fortinet_host_check.check_virtual_desktop
Fortinet virtual desktop check result string.
FortiClient conventionally sends colon-separated MAC addresses joined by `|`, for example `74:78:27:4d:81:93|84:1b:77:3a:95:84`. An empty value is submitted as an empty field when hostcheck is enabled.
### no_udp ### no_udp
Disable the DTLS or ESP secondary data channel and use the TLS data channel only. Disable the DTLS or ESP secondary data channel and use the TLS data channel only.
@@ -58,6 +58,10 @@
} }
] ]
}, },
"fortinet_host_check": {
"hostcheck": "",
"check_virtual_desktop": ""
},
"no_udp": false, "no_udp": false,
"dtls_local_port": 0, "dtls_local_port": 0,
"compression_disabled": false, "compression_disabled": false,
@@ -342,6 +346,30 @@ PEM 格式的 TNCC 机器证书路径。
`tncc.certificates.certificate` 冲突。 `tncc.certificates.certificate` 冲突。
### fortinet_host_check
Fortinet hostcheck 结果覆盖选项。
默认禁用 hostcheck。仅当 `fortinet_host_check.hostcheck` 非空时启用。不会自动收集操作系统、安全产品或网络接口信息。
启用后,如果成功的 Fortinet 登录响应要求 hostcheck,将在使用 VPN 会话前向服务器提交两个配置值。这些值不经修改,作为 `application/x-www-form-urlencoded` 字段发送。
部分 Fortinet 服务器只会要求可识别的 FortiClient User-Agent 执行 hostcheck。服务器策略有要求时请配置 `user_agent`
### fortinet_host_check.hostcheck
Fortinet hostcheck 结果字符串。
通常格式为 `<security-status>,<os-version>`,例如 `0100,10.0.19042``security-status` 包含四个 `0``1` 字符,依次表示第三方防火墙、第三方杀毒软件、FortiClient 防火墙和 FortiClient 杀毒软件。
空值会禁用 Fortinet hostcheck,即使配置了 `fortinet_host_check.check_virtual_desktop`
### fortinet_host_check.check_virtual_desktop
Fortinet virtual desktop 检查结果字符串。
FortiClient 通常发送以冒号分隔的 MAC 地址,多个地址使用 `|` 连接,例如 `74:78:27:4d:81:93|84:1b:77:3a:95:84`。启用 hostcheck 时,空值会作为空字段提交。
### no_udp ### no_udp
禁用 DTLS 或 ESP 辅助数据通道,仅使用 TLS 数据通道。 禁用 DTLS 或 ESP 辅助数据通道,仅使用 TLS 数据通道。
+1 -1
View File
@@ -47,7 +47,7 @@ require (
github.com/sagernet/sing v0.9.0-beta.3 github.com/sagernet/sing v0.9.0-beta.3
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3
github.com/sagernet/sing-mux v0.3.5 github.com/sagernet/sing-mux v0.3.5
github.com/sagernet/sing-openconnect v0.0.0-20260721013312-6c25fa7e089a github.com/sagernet/sing-openconnect v0.0.0-20260721123934-0d945d43288f
github.com/sagernet/sing-openvpn v0.0.0-20260721005523-64b754d1c277 github.com/sagernet/sing-openvpn v0.0.0-20260721005523-64b754d1c277
github.com/sagernet/sing-quic v0.6.4-0.20260803041931-6c84c468bea2 github.com/sagernet/sing-quic v0.6.4-0.20260803041931-6c84c468bea2
github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks v0.2.8
+2 -2
View File
@@ -317,8 +317,8 @@ github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 h1:3y6
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3/go.mod h1:XEqEDYRCAYLaoPjZ1ifVWJg5iWAJHL2gOAXe/PM28Cg= github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3/go.mod h1:XEqEDYRCAYLaoPjZ1ifVWJg5iWAJHL2gOAXe/PM28Cg=
github.com/sagernet/sing-mux v0.3.5 h1:RHnhVEc+SFqkrK4xMygYjDwwLhzp2Bj3lztSukONfhI= github.com/sagernet/sing-mux v0.3.5 h1:RHnhVEc+SFqkrK4xMygYjDwwLhzp2Bj3lztSukONfhI=
github.com/sagernet/sing-mux v0.3.5/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= github.com/sagernet/sing-mux v0.3.5/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
github.com/sagernet/sing-openconnect v0.0.0-20260721013312-6c25fa7e089a h1:DafepSfytV5uShjQctZ0Cnw+Q9cOBIMT88p8jU2DSck= github.com/sagernet/sing-openconnect v0.0.0-20260721123934-0d945d43288f h1:FKT5eCVyNiRNpa5jov6Fl9aQpAgKuCmkuhqVXvBK8b4=
github.com/sagernet/sing-openconnect v0.0.0-20260721013312-6c25fa7e089a/go.mod h1:4AKZLVcvY3r54UaK2Gbnm7aN8pOwdLz+y4EP0QFZ5Eg= github.com/sagernet/sing-openconnect v0.0.0-20260721123934-0d945d43288f/go.mod h1:4AKZLVcvY3r54UaK2Gbnm7aN8pOwdLz+y4EP0QFZ5Eg=
github.com/sagernet/sing-openvpn v0.0.0-20260721005523-64b754d1c277 h1:4H38L3OxOx1fGEuH4n9lh/5O7XtZTgQ/1V/gdQ+b+Es= github.com/sagernet/sing-openvpn v0.0.0-20260721005523-64b754d1c277 h1:4H38L3OxOx1fGEuH4n9lh/5O7XtZTgQ/1V/gdQ+b+Es=
github.com/sagernet/sing-openvpn v0.0.0-20260721005523-64b754d1c277/go.mod h1:PWX7WygD8jpwfqfaGNySXpJYTn0SOwjBI1BKHHC2+Bw= github.com/sagernet/sing-openvpn v0.0.0-20260721005523-64b754d1c277/go.mod h1:PWX7WygD8jpwfqfaGNySXpJYTn0SOwjBI1BKHHC2+Bw=
github.com/sagernet/sing-quic v0.6.4-0.20260803041931-6c84c468bea2 h1:XhJro6+Ou+WOPjfs22m14lY7Sh6sWPm/f0QiyFUgM60= github.com/sagernet/sing-quic v0.6.4-0.20260803041931-6c84c468bea2 h1:XhJro6+Ou+WOPjfs22m14lY7Sh6sWPm/f0QiyFUgM60=
+47 -41
View File
@@ -4,47 +4,48 @@ import "github.com/sagernet/sing/common/json/badoption"
type OpenConnectEndpointOptions struct { type OpenConnectEndpointOptions struct {
DialerOptions DialerOptions
System bool `json:"system,omitempty"` System bool `json:"system,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"` UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
UDPMapping UDPNATBehavior `json:"udp_mapping,omitempty"` UDPMapping UDPNATBehavior `json:"udp_mapping,omitempty"`
UDPFiltering UDPNATBehavior `json:"udp_filtering,omitempty"` UDPFiltering UDPNATBehavior `json:"udp_filtering,omitempty"`
UDPNATMax uint32 `json:"udp_nat_max,omitempty"` UDPNATMax uint32 `json:"udp_nat_max,omitempty"`
Server string `json:"server"` Server string `json:"server"`
Flavor string `json:"flavor,omitempty"` Flavor string `json:"flavor,omitempty"`
Username string `json:"username,omitempty"` Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"` Password string `json:"password,omitempty"`
AuthGroup string `json:"auth_group,omitempty"` AuthGroup string `json:"auth_group,omitempty"`
Cookie string `json:"cookie,omitempty"` Cookie string `json:"cookie,omitempty"`
Token *OpenConnectTokenOptions `json:"token,omitempty"` Token *OpenConnectTokenOptions `json:"token,omitempty"`
ReportedOS string `json:"reported_os,omitempty"` ReportedOS string `json:"reported_os,omitempty"`
UserAgent string `json:"user_agent,omitempty"` UserAgent string `json:"user_agent,omitempty"`
Version string `json:"version,omitempty"` Version string `json:"version,omitempty"`
LocalHostname string `json:"local_hostname,omitempty"` LocalHostname string `json:"local_hostname,omitempty"`
Mobile *OpenConnectMobileOptions `json:"mobile,omitempty"` Mobile *OpenConnectMobileOptions `json:"mobile,omitempty"`
CSD *OpenConnectCSDOptions `json:"csd,omitempty"` CSD *OpenConnectCSDOptions `json:"csd,omitempty"`
HIP *OpenConnectHIPOptions `json:"hip,omitempty"` HIP *OpenConnectHIPOptions `json:"hip,omitempty"`
TNCC *OpenConnectTNCCOptions `json:"tncc,omitempty"` TNCC *OpenConnectTNCCOptions `json:"tncc,omitempty"`
NoUDP bool `json:"no_udp,omitempty"` FortinetHostCheck *OpenConnectFortinetHostCheckOptions `json:"fortinet_host_check,omitempty"`
DTLSLocalPort uint16 `json:"dtls_local_port,omitempty"` NoUDP bool `json:"no_udp,omitempty"`
CompressionDisabled bool `json:"compression_disabled,omitempty"` DTLSLocalPort uint16 `json:"dtls_local_port,omitempty"`
CompressionMode string `json:"compression_mode,omitempty"` CompressionDisabled bool `json:"compression_disabled,omitempty"`
IPv6Disabled bool `json:"ipv6_disabled,omitempty"` CompressionMode string `json:"compression_mode,omitempty"`
HTTPKeepAliveDisabled bool `json:"http_keepalive_disabled,omitempty"` IPv6Disabled bool `json:"ipv6_disabled,omitempty"`
XMLPostDisabled bool `json:"xml_post_disabled,omitempty"` HTTPKeepAliveDisabled bool `json:"http_keepalive_disabled,omitempty"`
ExternalAuthDisabled bool `json:"external_auth_disabled,omitempty"` XMLPostDisabled bool `json:"xml_post_disabled,omitempty"`
PasswordAuthenticationDisabled bool `json:"password_authentication_disabled,omitempty"` ExternalAuthDisabled bool `json:"external_auth_disabled,omitempty"`
TCPKeepAliveEnabled bool `json:"tcp_keep_alive_enabled,omitempty"` PasswordAuthenticationDisabled bool `json:"password_authentication_disabled,omitempty"`
PFS bool `json:"pfs,omitempty"` TCPKeepAliveEnabled bool `json:"tcp_keep_alive_enabled,omitempty"`
MTU uint32 `json:"mtu,omitempty"` PFS bool `json:"pfs,omitempty"`
BaseMTU uint32 `json:"base_mtu,omitempty"` MTU uint32 `json:"mtu,omitempty"`
DPDInterval badoption.Duration `json:"dpd_interval,omitempty"` BaseMTU uint32 `json:"base_mtu,omitempty"`
ReconnectTimeout badoption.Duration `json:"reconnect_timeout,omitempty"` DPDInterval badoption.Duration `json:"dpd_interval,omitempty"`
TrojanInterval badoption.Duration `json:"trojan_interval,omitempty"` ReconnectTimeout badoption.Duration `json:"reconnect_timeout,omitempty"`
QueueLength uint32 `json:"queue_length,omitempty"` TrojanInterval badoption.Duration `json:"trojan_interval,omitempty"`
AllowInsecureCrypto bool `json:"allow_insecure_crypto,omitempty"` QueueLength uint32 `json:"queue_length,omitempty"`
TLS OpenConnectTLSOptions `json:"tls,omitempty"` AllowInsecureCrypto bool `json:"allow_insecure_crypto,omitempty"`
FormEntries []OpenConnectFormEntryOptions `json:"form_entries,omitempty"` TLS OpenConnectTLSOptions `json:"tls,omitempty"`
FormEntries []OpenConnectFormEntryOptions `json:"form_entries,omitempty"`
} }
type OpenConnectTokenOptions struct { type OpenConnectTokenOptions struct {
@@ -79,6 +80,11 @@ type OpenConnectTNCCOptions struct {
Certificates []OpenConnectTNCCCertificateOptions `json:"certificates,omitempty"` Certificates []OpenConnectTNCCCertificateOptions `json:"certificates,omitempty"`
} }
type OpenConnectFortinetHostCheckOptions struct {
HostCheck string `json:"hostcheck,omitempty"`
CheckVirtualDesktop string `json:"check_virtual_desktop,omitempty"`
}
type OpenConnectTNCCCertificateOptions struct { type OpenConnectTNCCCertificateOptions struct {
Certificate badoption.Listable[string] `json:"certificate,omitempty"` Certificate badoption.Listable[string] `json:"certificate,omitempty"`
CertificatePath string `json:"certificate_path,omitempty"` CertificatePath string `json:"certificate_path,omitempty"`
+8
View File
@@ -254,6 +254,13 @@ func (e *Endpoint) buildClientOptions(options option.OpenConnectEndpointOptions,
Certificates: tnccCertificates, Certificates: tnccCertificates,
} }
} }
var fortinetHostCheckOptions *openconnect.FortinetHostCheckOptions
if options.FortinetHostCheck != nil {
fortinetHostCheckOptions = &openconnect.FortinetHostCheckOptions{
HostCheck: options.FortinetHostCheck.HostCheck,
CheckVirtualDesktop: options.FortinetHostCheck.CheckVirtualDesktop,
}
}
formEntries := common.Map(options.FormEntries, func(entry option.OpenConnectFormEntryOptions) openconnect.FormEntry { formEntries := common.Map(options.FormEntries, func(entry option.OpenConnectFormEntryOptions) openconnect.FormEntry {
return openconnect.FormEntry{ return openconnect.FormEntry{
FormID: entry.FormID, FormID: entry.FormID,
@@ -280,6 +287,7 @@ func (e *Endpoint) buildClientOptions(options option.OpenConnectEndpointOptions,
CSD: csdOptions, CSD: csdOptions,
HIP: hipOptions, HIP: hipOptions,
TNCC: tnccOptions, TNCC: tnccOptions,
FortinetHostCheck: fortinetHostCheckOptions,
NoUDP: options.NoUDP, NoUDP: options.NoUDP,
DTLSLocalPort: options.DTLSLocalPort, DTLSLocalPort: options.DTLSLocalPort,
CompressionDisabled: options.CompressionDisabled, CompressionDisabled: options.CompressionDisabled,