From ceb7154384447d83281ca1239549c2bc78ef7aed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 22 Jul 2026 22:03:58 +0800 Subject: [PATCH] Fix Fortinet SSO again --- adapter/experimental.go | 2 + adapter/openconnect.go | 11 +++-- cmd/internal/build_libbox/main.go | 4 +- daemon/started_service.go | 11 +++-- daemon/started_service.pb.go | 45 +++++++++++++++---- daemon/started_service.proto | 3 ++ docs/configuration/endpoint/openconnect.md | 4 +- docs/configuration/endpoint/openconnect.zh.md | 4 +- experimental/cachefile/cache.go | 6 +++ .../libbox/command_types_openconnect.go | 38 +++++++++++----- go.mod | 2 +- go.sum | 4 +- protocol/openconnect/status.go | 17 +++++-- 13 files changed, 110 insertions(+), 41 deletions(-) diff --git a/adapter/experimental.go b/adapter/experimental.go index b5eb3243a..7db5abfa2 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -32,6 +32,8 @@ type V2RayServer interface { type CacheFile interface { LifecycleService + CacheID() string + StoreFakeIP() bool FakeIPStorage diff --git a/adapter/openconnect.go b/adapter/openconnect.go index e2bf3218f..9a5d7fa9b 100644 --- a/adapter/openconnect.go +++ b/adapter/openconnect.go @@ -52,10 +52,13 @@ type OpenConnectAuthForm struct { } type OpenConnectBrowserRequest struct { - URL string - FinalURL string - CookieNames []string - HeaderNames []string + URL string + FinalURL string + CookieNames []string + EarlyCookieNames []string + HeaderNames []string + CallbackURLPrefixes []string + CacheID string } type OpenConnectBrowserCookie struct { diff --git a/cmd/internal/build_libbox/main.go b/cmd/internal/build_libbox/main.go index 00ab73d91..5b2d9e28f 100644 --- a/cmd/internal/build_libbox/main.go +++ b/cmd/internal/build_libbox/main.go @@ -163,14 +163,14 @@ func buildAndroid() { bindTarget := getAndroidBindTarget() - // Build main variant (SDK 23) + // Build main variant (SDK 24) mainTags := append([]string{}, sharedTags...) // mainTags = append(mainTags, memcTags...) if debugEnabled { mainTags = append(mainTags, debugTags...) } buildAndroidVariant(AndroidBuildConfig{ - AndroidAPI: 23, + AndroidAPI: 24, OutputName: "libbox.aar", Tags: mainTags, }, bindTarget) diff --git a/daemon/started_service.go b/daemon/started_service.go index f217ae1e4..70d7f057b 100644 --- a/daemon/started_service.go +++ b/daemon/started_service.go @@ -1649,10 +1649,13 @@ func openConnectEndpointStatusToProto(tag string, endpointStatus adapter.OpenCon } if endpointStatus.AuthChallenge.Browser != nil { challenge.Challenge = &OpenConnectAuthChallenge_Browser{Browser: &OpenConnectBrowserRequest{ - Url: endpointStatus.AuthChallenge.Browser.URL, - FinalURL: endpointStatus.AuthChallenge.Browser.FinalURL, - CookieNames: endpointStatus.AuthChallenge.Browser.CookieNames, - HeaderNames: endpointStatus.AuthChallenge.Browser.HeaderNames, + Url: endpointStatus.AuthChallenge.Browser.URL, + FinalURL: endpointStatus.AuthChallenge.Browser.FinalURL, + CookieNames: endpointStatus.AuthChallenge.Browser.CookieNames, + EarlyCookieNames: endpointStatus.AuthChallenge.Browser.EarlyCookieNames, + HeaderNames: endpointStatus.AuthChallenge.Browser.HeaderNames, + CallbackURLPrefixes: endpointStatus.AuthChallenge.Browser.CallbackURLPrefixes, + CacheID: endpointStatus.AuthChallenge.Browser.CacheID, }} } result.AuthChallenge = challenge diff --git a/daemon/started_service.pb.go b/daemon/started_service.pb.go index c019f557c..f3fb9192f 100644 --- a/daemon/started_service.pb.go +++ b/daemon/started_service.pb.go @@ -5149,13 +5149,16 @@ func (x *OpenConnectAuthFormChoice) GetLabel() string { } type OpenConnectBrowserRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` - FinalURL string `protobuf:"bytes,2,opt,name=finalURL,proto3" json:"finalURL,omitempty"` - CookieNames []string `protobuf:"bytes,3,rep,name=cookieNames,proto3" json:"cookieNames,omitempty"` - HeaderNames []string `protobuf:"bytes,4,rep,name=headerNames,proto3" json:"headerNames,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + FinalURL string `protobuf:"bytes,2,opt,name=finalURL,proto3" json:"finalURL,omitempty"` + CookieNames []string `protobuf:"bytes,3,rep,name=cookieNames,proto3" json:"cookieNames,omitempty"` + HeaderNames []string `protobuf:"bytes,4,rep,name=headerNames,proto3" json:"headerNames,omitempty"` + CallbackURLPrefixes []string `protobuf:"bytes,5,rep,name=callbackURLPrefixes,proto3" json:"callbackURLPrefixes,omitempty"` + EarlyCookieNames []string `protobuf:"bytes,6,rep,name=earlyCookieNames,proto3" json:"earlyCookieNames,omitempty"` + CacheID string `protobuf:"bytes,7,opt,name=cacheID,proto3" json:"cacheID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *OpenConnectBrowserRequest) Reset() { @@ -5216,6 +5219,27 @@ func (x *OpenConnectBrowserRequest) GetHeaderNames() []string { return nil } +func (x *OpenConnectBrowserRequest) GetCallbackURLPrefixes() []string { + if x != nil { + return x.CallbackURLPrefixes + } + return nil +} + +func (x *OpenConnectBrowserRequest) GetEarlyCookieNames() []string { + if x != nil { + return x.EarlyCookieNames + } + return nil +} + +func (x *OpenConnectBrowserRequest) GetCacheID() string { + if x != nil { + return x.CacheID + } + return "" +} + type OpenConnectBrowserCookie struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -6492,12 +6516,15 @@ const file_daemon_started_service_proto_rawDesc = "" + "\aoptions\x18\x06 \x03(\v2!.daemon.OpenConnectAuthFormChoiceR\aoptions\"G\n" + "\x19OpenConnectAuthFormChoice\x12\x14\n" + "\x05value\x18\x01 \x01(\tR\x05value\x12\x14\n" + - "\x05label\x18\x02 \x01(\tR\x05label\"\x8d\x01\n" + + "\x05label\x18\x02 \x01(\tR\x05label\"\x85\x02\n" + "\x19OpenConnectBrowserRequest\x12\x10\n" + "\x03url\x18\x01 \x01(\tR\x03url\x12\x1a\n" + "\bfinalURL\x18\x02 \x01(\tR\bfinalURL\x12 \n" + "\vcookieNames\x18\x03 \x03(\tR\vcookieNames\x12 \n" + - "\vheaderNames\x18\x04 \x03(\tR\vheaderNames\"D\n" + + "\vheaderNames\x18\x04 \x03(\tR\vheaderNames\x120\n" + + "\x13callbackURLPrefixes\x18\x05 \x03(\tR\x13callbackURLPrefixes\x12*\n" + + "\x10earlyCookieNames\x18\x06 \x03(\tR\x10earlyCookieNames\x12\x18\n" + + "\acacheID\x18\a \x01(\tR\acacheID\"D\n" + "\x18OpenConnectBrowserCookie\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value\"F\n" + diff --git a/daemon/started_service.proto b/daemon/started_service.proto index 650f4122d..a03796f46 100644 --- a/daemon/started_service.proto +++ b/daemon/started_service.proto @@ -582,6 +582,9 @@ message OpenConnectBrowserRequest { string finalURL = 2; repeated string cookieNames = 3; repeated string headerNames = 4; + repeated string callbackURLPrefixes = 5; + repeated string earlyCookieNames = 6; + string cacheID = 7; } message OpenConnectBrowserCookie { diff --git a/docs/configuration/endpoint/openconnect.md b/docs/configuration/endpoint/openconnect.md index 8edaa9f9e..8afb65ba3 100644 --- a/docs/configuration/endpoint/openconnect.md +++ b/docs/configuration/endpoint/openconnect.md @@ -415,9 +415,9 @@ Disable AnyConnect XML POST authentication and start authentication with the leg ### external_auth_disabled -Disable external browser authentication such as SSO and SAML for AnyConnect and GlobalProtect. +Disable external browser authentication such as SSO and SAML for AnyConnect, GlobalProtect, and Fortinet. -When enabled, external authentication is not advertised to the server and an unexpected external authentication request is rejected. +When enabled, external authentication is not advertised for AnyConnect or GlobalProtect, and any unexpected external authentication request, including Fortinet SAML, is rejected. ### password_authentication_disabled diff --git a/docs/configuration/endpoint/openconnect.zh.md b/docs/configuration/endpoint/openconnect.zh.md index 63bebdd26..904a40412 100644 --- a/docs/configuration/endpoint/openconnect.zh.md +++ b/docs/configuration/endpoint/openconnect.zh.md @@ -415,9 +415,9 @@ AnyConnect 压缩模式,可选值为: ### external_auth_disabled -禁用 AnyConnect 和 GlobalProtect 的 SSO、SAML 等外部浏览器认证。 +禁用 AnyConnect、GlobalProtect 和 Fortinet 的 SSO、SAML 等外部浏览器认证。 -启用时不会向服务器声明外部认证支持,并会拒绝意外收到的外部认证请求。 +启用时不会为 AnyConnect 或 GlobalProtect 向服务器声明外部认证支持,并会拒绝任何意外收到的外部认证请求,包括 Fortinet SAML。 ### password_authentication_disabled diff --git a/experimental/cachefile/cache.go b/experimental/cachefile/cache.go index 3ca556757..82fa2a03f 100644 --- a/experimental/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -45,6 +45,7 @@ type CacheFile struct { logger logger.Logger path string cacheID []byte + cacheIDText string storeFakeIP bool storeRDRC bool storeDNS bool @@ -104,6 +105,7 @@ func New(ctx context.Context, logger logger.Logger, options option.CacheFileOpti logger: logger, path: filemanager.BasePath(ctx, path), cacheID: cacheIDBytes, + cacheIDText: options.CacheID, storeFakeIP: options.StoreFakeIP, storeRDRC: options.StoreRDRC, storeDNS: options.StoreDNS, @@ -124,6 +126,10 @@ func (c *CacheFile) Dependencies() []string { return nil } +func (c *CacheFile) CacheID() string { + return c.cacheIDText +} + func (c *CacheFile) SetOptimisticTimeout(timeout time.Duration) { c.optimisticTimeout = timeout } diff --git a/experimental/libbox/command_types_openconnect.go b/experimental/libbox/command_types_openconnect.go index e384e04eb..b31abe9aa 100644 --- a/experimental/libbox/command_types_openconnect.go +++ b/experimental/libbox/command_types_openconnect.go @@ -1,6 +1,8 @@ package libbox import ( + "strings" + "github.com/sagernet/sing-box/daemon" "github.com/sagernet/sing/common" ) @@ -68,20 +70,31 @@ func (f *OpenConnectAuthForm) Fields() OpenConnectAuthFormFieldIterator { } type OpenConnectBrowserRequest struct { - URL string - FinalURL string - cookieNames []string - headerNames []string + URL string + FinalURL string + CacheID string + cookieNames []string + earlyCookieNames []string + headerNames []string + callbackURLPrefixes []string } func (r *OpenConnectBrowserRequest) CookieNames() StringIterator { return newIterator(r.cookieNames) } +func (r *OpenConnectBrowserRequest) EarlyCookieNames() StringIterator { + return newIterator(r.earlyCookieNames) +} + func (r *OpenConnectBrowserRequest) HeaderNames() StringIterator { return newIterator(r.headerNames) } +func (r *OpenConnectBrowserRequest) CallbackURLPrefixes() StringIterator { + return newIterator(r.callbackURLPrefixes) +} + type OpenConnectAuthFormFieldIterator interface { Next() *OpenConnectAuthFormField HasNext() bool @@ -137,9 +150,9 @@ func (r *OpenConnectBrowserResult) AddCookie(name string, value string) { } func (r *OpenConnectBrowserResult) AddHeader(name string, value string) { - for _, header := range r.headers { - if header.Name == name { - header.Values = append(header.Values, value) + for i := range r.headers { + if strings.EqualFold(r.headers[i].Name, name) { + r.headers[i].Values = append(r.headers[i].Values, value) return } } @@ -221,10 +234,13 @@ func openConnectEndpointStatusFromGRPC(status *daemon.OpenConnectEndpointStatus) browser := status.AuthChallenge.GetBrowser() if browser != nil { challenge.Browser = &OpenConnectBrowserRequest{ - URL: browser.Url, - FinalURL: browser.FinalURL, - cookieNames: browser.CookieNames, - headerNames: browser.HeaderNames, + URL: browser.Url, + FinalURL: browser.FinalURL, + CacheID: browser.CacheID, + cookieNames: browser.CookieNames, + earlyCookieNames: browser.EarlyCookieNames, + headerNames: browser.HeaderNames, + callbackURLPrefixes: browser.CallbackURLPrefixes, } } result.AuthChallenge = challenge diff --git a/go.mod b/go.mod index 45eea9fbd..20a0e00d7 100644 --- a/go.mod +++ b/go.mod @@ -46,7 +46,7 @@ require ( github.com/sagernet/sing v0.8.12-0.20260721063414-596db5dd6ef4 github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 github.com/sagernet/sing-mux v0.3.5 - github.com/sagernet/sing-openconnect v0.0.0-20260721123934-0d945d43288f + github.com/sagernet/sing-openconnect v0.0.0-20260722140139-e0c977659ca2 github.com/sagernet/sing-openvpn v0.0.0-20260721005523-64b754d1c277 github.com/sagernet/sing-quic v0.6.4-0.20260720125339-63fa6a7ebc31 github.com/sagernet/sing-shadowsocks v0.2.8 diff --git a/go.sum b/go.sum index 5d6fa850b..68c336f96 100644 --- a/go.sum +++ b/go.sum @@ -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-mux v0.3.5 h1:RHnhVEc+SFqkrK4xMygYjDwwLhzp2Bj3lztSukONfhI= github.com/sagernet/sing-mux v0.3.5/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= -github.com/sagernet/sing-openconnect v0.0.0-20260721123934-0d945d43288f h1:FKT5eCVyNiRNpa5jov6Fl9aQpAgKuCmkuhqVXvBK8b4= -github.com/sagernet/sing-openconnect v0.0.0-20260721123934-0d945d43288f/go.mod h1:4AKZLVcvY3r54UaK2Gbnm7aN8pOwdLz+y4EP0QFZ5Eg= +github.com/sagernet/sing-openconnect v0.0.0-20260722140139-e0c977659ca2 h1:IOzb7F/NRhAKx49+8HEsJmKrZT73EPseFouxQ6MvAOo= +github.com/sagernet/sing-openconnect v0.0.0-20260722140139-e0c977659ca2/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/go.mod h1:PWX7WygD8jpwfqfaGNySXpJYTn0SOwjBI1BKHHC2+Bw= github.com/sagernet/sing-quic v0.6.4-0.20260720125339-63fa6a7ebc31 h1:FuiTs45PaA1/f2Kj6E2QlctFBrJUUFB8QAHnV5im9c8= diff --git a/protocol/openconnect/status.go b/protocol/openconnect/status.go index ea0cd8fbe..b7be1dc56 100644 --- a/protocol/openconnect/status.go +++ b/protocol/openconnect/status.go @@ -7,6 +7,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-openconnect" "github.com/sagernet/sing/common" + "github.com/sagernet/sing/service" ) var _ adapter.OpenConnectEndpoint = (*Endpoint)(nil) @@ -45,11 +46,19 @@ func (e *Endpoint) OpenConnectStatus() adapter.OpenConnectStatus { } } if authChallenge.Browser != nil { + var cacheID string + cacheFile := service.FromContext[adapter.CacheFile](e.loopContext) + if cacheFile != nil { + cacheID = cacheFile.CacheID() + } challenge.Browser = &adapter.OpenConnectBrowserRequest{ - URL: authChallenge.Browser.URL, - FinalURL: authChallenge.Browser.FinalURL, - CookieNames: slices.Clone(authChallenge.Browser.CookieNames), - HeaderNames: slices.Clone(authChallenge.Browser.HeaderNames), + URL: authChallenge.Browser.URL, + FinalURL: authChallenge.Browser.FinalURL, + CookieNames: slices.Clone(authChallenge.Browser.CookieNames), + EarlyCookieNames: slices.Clone(authChallenge.Browser.EarlyCookieNames), + HeaderNames: slices.Clone(authChallenge.Browser.HeaderNames), + CallbackURLPrefixes: slices.Clone(authChallenge.Browser.CallbackURLPrefixes), + CacheID: cacheID, } } status.AuthChallenge = challenge