From 46f00de9aa060ab989353953051268c7c4745664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 17 Jul 2026 16:21:03 +0800 Subject: [PATCH] Add openvpn and openconnect --- adapter/openconnect.go | 62 + adapter/openvpn.go | 57 + cmd/internal/app_store_connect/main.go | 2 +- cmd/internal/build_libbox/main.go | 2 +- common/dialer/default.go | 4 +- common/dialer/udp_listener.go | 9 + common/dialer/wireguard.go | 9 - common/listener/listener_udp.go | 5 +- constant/proxy.go | 9 + daemon/started_service.go | 292 ++- daemon/started_service.pb.go | 1411 +++++++++++-- daemon/started_service.proto | 112 + daemon/started_service_grpc.pb.go | 296 ++- docs/configuration/endpoint/index.md | 11 +- docs/configuration/endpoint/index.zh.md | 11 +- docs/configuration/endpoint/openconnect.md | 387 ++++ docs/configuration/endpoint/openconnect.zh.md | 387 ++++ docs/configuration/endpoint/openvpn-client.md | 501 +++++ .../endpoint/openvpn-client.zh.md | 501 +++++ docs/configuration/endpoint/openvpn-server.md | 319 +++ .../endpoint/openvpn-server.zh.md | 319 +++ docs/configuration/endpoint/tailscale.md | 6 +- docs/configuration/endpoint/tailscale.zh.md | 6 +- experimental/libbox/command_client.go | 178 +- .../libbox/command_types_openconnect.go | 164 ++ experimental/libbox/command_types_openvpn.go | 117 ++ go.mod | 10 +- go.sum | 74 +- include/openconnect.go | 12 + include/openconnect_stub.go | 23 + include/openvpn.go | 12 + include/openvpn_stub.go | 29 + include/registry.go | 2 + mkdocs.yml | 3 + option/openconnect.go | 77 + option/openvpn.go | 123 ++ protocol/openconnect/client.go | 513 +++++ protocol/openconnect/endpoint.go | 158 ++ protocol/openconnect/status.go | 135 ++ protocol/openvpn/client.go | 607 ++++++ protocol/openvpn/endpoint.go | 277 +++ protocol/openvpn/server.go | 571 ++++++ protocol/openvpn/socket_buffer.go | 21 + protocol/openvpn/socket_buffer_linux.go | 28 + protocol/openvpn/status.go | 114 ++ protocol/wireguard/endpoint.go | 6 +- release/DEFAULT_BUILD_TAGS | 2 +- release/DEFAULT_BUILD_TAGS_OTHERS | 2 +- release/DEFAULT_BUILD_TAGS_WINDOWS | 2 +- test/go.mod | 95 +- test/go.sum | 240 ++- test/openconnect_test.go | 575 ++++++ test/openvpn_test.go | 1797 +++++++++++++++++ test/testdata/openconnect/ocserv/Dockerfile | 11 + .../openconnect/ocserv/echo_server.py | 22 + transport/openconnect/device.go | 200 ++ transport/openconnect/device_stack.go | 341 ++++ transport/openconnect/device_stack_stub.go | 13 + transport/openconnect/device_system.go | 366 ++++ transport/openconnect/device_system_stack.go | 105 + transport/openvpn/device.go | 224 ++ transport/openvpn/device_stack.go | 358 ++++ transport/openvpn/device_stack_stub.go | 13 + transport/openvpn/device_system.go | 435 ++++ transport/openvpn/device_system_stack.go | 106 + transport/wireguard/endpoint.go | 10 +- 66 files changed, 12499 insertions(+), 390 deletions(-) create mode 100644 adapter/openconnect.go create mode 100644 adapter/openvpn.go create mode 100644 common/dialer/udp_listener.go delete mode 100644 common/dialer/wireguard.go create mode 100644 docs/configuration/endpoint/openconnect.md create mode 100644 docs/configuration/endpoint/openconnect.zh.md create mode 100644 docs/configuration/endpoint/openvpn-client.md create mode 100644 docs/configuration/endpoint/openvpn-client.zh.md create mode 100644 docs/configuration/endpoint/openvpn-server.md create mode 100644 docs/configuration/endpoint/openvpn-server.zh.md create mode 100644 experimental/libbox/command_types_openconnect.go create mode 100644 experimental/libbox/command_types_openvpn.go create mode 100644 include/openconnect.go create mode 100644 include/openconnect_stub.go create mode 100644 include/openvpn.go create mode 100644 include/openvpn_stub.go create mode 100644 option/openconnect.go create mode 100644 option/openvpn.go create mode 100644 protocol/openconnect/client.go create mode 100644 protocol/openconnect/endpoint.go create mode 100644 protocol/openconnect/status.go create mode 100644 protocol/openvpn/client.go create mode 100644 protocol/openvpn/endpoint.go create mode 100644 protocol/openvpn/server.go create mode 100644 protocol/openvpn/socket_buffer.go create mode 100644 protocol/openvpn/socket_buffer_linux.go create mode 100644 protocol/openvpn/status.go create mode 100644 test/openconnect_test.go create mode 100644 test/openvpn_test.go create mode 100644 test/testdata/openconnect/ocserv/Dockerfile create mode 100644 test/testdata/openconnect/ocserv/echo_server.py create mode 100644 transport/openconnect/device.go create mode 100644 transport/openconnect/device_stack.go create mode 100644 transport/openconnect/device_stack_stub.go create mode 100644 transport/openconnect/device_system.go create mode 100644 transport/openconnect/device_system_stack.go create mode 100644 transport/openvpn/device.go create mode 100644 transport/openvpn/device_stack.go create mode 100644 transport/openvpn/device_stack_stub.go create mode 100644 transport/openvpn/device_system.go create mode 100644 transport/openvpn/device_system_stack.go diff --git a/adapter/openconnect.go b/adapter/openconnect.go new file mode 100644 index 000000000..a0e012c69 --- /dev/null +++ b/adapter/openconnect.go @@ -0,0 +1,62 @@ +package adapter + +import ( + "net/netip" + "time" +) + +const ( + OpenConnectStateConnecting = "connecting" + OpenConnectStateAuthPending = "auth-pending" + OpenConnectStateConnected = "connected" + OpenConnectStateError = "error" +) + +type OpenConnectEndpoint interface { + Endpoint + OpenConnectStatus() OpenConnectStatus + StatusUpdated() <-chan struct{} + CompleteAuthForm(formID string, values map[string]string) error + CancelAuthForm(formID string) error +} + +type OpenConnectStatus struct { + State string + AuthForm *OpenConnectAuthForm + Error string + TunnelInfo *OpenConnectTunnelInfo +} + +type OpenConnectTunnelInfo struct { + Server string + Flavor string + Transport string + IPv4 []netip.Prefix + IPv6 []netip.Prefix + DNS []netip.Addr + MTU uint32 + ConnectedSince time.Time +} + +type OpenConnectAuthForm struct { + ID string + Banner string + Message string + Error string + URL string + Fields []OpenConnectAuthFormField +} + +type OpenConnectAuthFormField struct { + SubmissionKey string + Name string + Label string + Kind string + Value string + Options []OpenConnectAuthFormChoice +} + +type OpenConnectAuthFormChoice struct { + Value string + Label string +} diff --git a/adapter/openvpn.go b/adapter/openvpn.go new file mode 100644 index 000000000..2034dc519 --- /dev/null +++ b/adapter/openvpn.go @@ -0,0 +1,57 @@ +package adapter + +import ( + "net/netip" + "time" +) + +const ( + OpenVPNStateConnecting = "connecting" + OpenVPNStateAuthPending = "auth-pending" + OpenVPNStateConnected = "connected" + OpenVPNStateError = "error" +) + +type OpenVPNEndpoint interface { + Endpoint + OpenVPNStatus() OpenVPNStatus + StatusUpdated() <-chan struct{} + CompleteChallenge(challengeID string, response OpenVPNChallengeResponse) error + CancelChallenge(challengeID string) error +} + +type OpenVPNStatus struct { + State string + Challenge *OpenVPNChallenge + Error string + TunnelInfo *OpenVPNTunnelInfo +} + +type OpenVPNTunnelInfo struct { + Server string + Network string + Cipher string + IPv4 []netip.Prefix + IPv6 []netip.Prefix + DNS []netip.Addr + MTU uint32 + ConnectedSince time.Time +} + +type OpenVPNChallenge struct { + ID string + Kind string + Username string + Message string + URL string + SecretMessage string + Echo bool + PreviousError string + Deadline time.Time +} + +type OpenVPNChallengeResponse struct { + Username string + Password string + Secret string +} diff --git a/cmd/internal/app_store_connect/main.go b/cmd/internal/app_store_connect/main.go index d415abd69..0a1ab61a0 100644 --- a/cmd/internal/app_store_connect/main.go +++ b/cmd/internal/app_store_connect/main.go @@ -289,7 +289,7 @@ func prepareAppStore(ctx context.Context) error { return err } if len(builds.Data) == 0 { - log.Fatal(platform, " ", tag, " no build found") + log.Fatal(string(platform), " ", tag, " no build found") } buildID := common.Ptr(builds.Data[0].ID) if version.ID == "" { diff --git a/cmd/internal/build_libbox/main.go b/cmd/internal/build_libbox/main.go index 8b2c03526..00ab73d91 100644 --- a/cmd/internal/build_libbox/main.go +++ b/cmd/internal/build_libbox/main.go @@ -63,7 +63,7 @@ func init() { sharedFlags = append(sharedFlags, "-ldflags", build_shared.LinkerFlags(currentTag, false)) debugFlags = append(debugFlags, "-ldflags", build_shared.LinkerFlags(currentTag, true)) - sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_naive_outbound", "with_clash_api", "with_usbip", "badlinkname", "tfogo_checklinkname0") + sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_naive_outbound", "with_clash_api", "with_usbip", "with_openvpn", "with_openconnect", "badlinkname", "tfogo_checklinkname0") darwinTags = append(darwinTags, "with_dhcp", "grpcnotrace") // memcTags = append(memcTags, "with_tailscale") sharedTags = append(sharedTags, "with_tailscale", "ts_omit_logtail", "ts_omit_ssh", "ts_omit_drive", "ts_omit_taildrop", "ts_omit_webclient", "ts_omit_doctor", "ts_omit_capture", "ts_omit_kube", "ts_omit_aws", "ts_omit_synology", "ts_omit_bird") diff --git a/common/dialer/default.go b/common/dialer/default.go index a05c42ee3..b1115644b 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -24,7 +24,7 @@ import ( var ( _ ParallelInterfaceDialer = (*DefaultDialer)(nil) - _ WireGuardListener = (*DefaultDialer)(nil) + _ UDPListener = (*DefaultDialer)(nil) ) type DefaultDialer struct { @@ -381,7 +381,7 @@ func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destina return d.trackPacketConn(packetConn, nil) } -func (d *DefaultDialer) WireGuardControl() (control.Func, bool) { +func (d *DefaultDialer) UDPListenerControl() (control.Func, bool) { return d.udpListener.Control, d.autoDetectBindFunc != nil && d.netns == "" } diff --git a/common/dialer/udp_listener.go b/common/dialer/udp_listener.go new file mode 100644 index 000000000..416952c96 --- /dev/null +++ b/common/dialer/udp_listener.go @@ -0,0 +1,9 @@ +package dialer + +import ( + "github.com/sagernet/sing/common/control" +) + +type UDPListener interface { + UDPListenerControl() (control.Func, bool) +} diff --git a/common/dialer/wireguard.go b/common/dialer/wireguard.go deleted file mode 100644 index 2456a0c8d..000000000 --- a/common/dialer/wireguard.go +++ /dev/null @@ -1,9 +0,0 @@ -package dialer - -import ( - "github.com/sagernet/sing/common/control" -) - -type WireGuardListener interface { - WireGuardControl() (control.Func, bool) -} diff --git a/common/listener/listener_udp.go b/common/listener/listener_udp.go index 407eaf5c3..fab87cbc8 100644 --- a/common/listener/listener_udp.go +++ b/common/listener/listener_udp.go @@ -22,8 +22,11 @@ import ( const udpOutputBatchSize = 128 func (l *Listener) ListenUDP() (net.PacketConn, error) { + return l.ListenUDPWithConfig(net.ListenConfig{}) +} + +func (l *Listener) ListenUDPWithConfig(listenConfig net.ListenConfig) (net.PacketConn, error) { bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort) - var listenConfig net.ListenConfig if l.listenOptions.BindInterface != "" { listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1)) } diff --git a/constant/proxy.go b/constant/proxy.go index e91164202..d42e5d92b 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -26,6 +26,9 @@ const ( TypeVLESS = "vless" TypeTUIC = "tuic" TypeHysteria2 = "hysteria2" + TypeOpenConnect = "openconnect" + TypeOpenVPNClient = "openvpn-client" + TypeOpenVPNServer = "openvpn-server" TypeTailscale = "tailscale" TypeCloudflared = "cloudflared" TypeDERP = "derp" @@ -99,6 +102,12 @@ func ProxyDisplayName(proxyType string) string { return "Hysteria2" case TypeAnyTLS: return "AnyTLS" + case TypeOpenConnect: + return "OpenConnect" + case TypeOpenVPNClient: + return "OpenVPN Client" + case TypeOpenVPNServer: + return "OpenVPN Server" case TypeTailscale: return "Tailscale" case TypeCloudflared: diff --git a/daemon/started_service.go b/daemon/started_service.go index 73fc80b03..cad6f5606 100644 --- a/daemon/started_service.go +++ b/daemon/started_service.go @@ -2,6 +2,7 @@ package daemon import ( "context" + "net/netip" "os" "runtime" runtimeDebug "runtime/debug" @@ -32,7 +33,7 @@ import ( "google.golang.org/protobuf/types/known/emptypb" ) -const APIVersion = 2 +const APIVersion = 3 var _ StartedServiceServer = (*StartedService)(nil) @@ -1197,6 +1198,68 @@ func NewSTUNTestResult(result *stun.Result) *STUNTestProgress { } } +func resolveEndpoint[T adapter.Endpoint](instance *Instance, tag string, endpointType string, endpointName string) (T, error) { + var zero T + endpointManager := service.FromContext[adapter.EndpointManager](instance.ctx) + endpoint, loaded := endpointManager.Get(tag) + if !loaded { + return zero, status.Error(codes.NotFound, "endpoint not found: "+tag) + } + if endpoint.Type() != endpointType { + return zero, status.Error(codes.InvalidArgument, "endpoint is not "+endpointName+": "+tag) + } + return endpoint.(T), nil +} + +type endpointStatusProvider interface { + adapter.Endpoint + StatusUpdated() <-chan struct{} +} + +func subscribeEndpointStatus[T endpointStatusProvider](ctx context.Context, endpointManager adapter.EndpointManager, endpointType string, endpointName string, send func([]T) error) error { + var endpoints []T + for _, endpoint := range endpointManager.Endpoints() { + if endpoint.Type() == endpointType { + endpoints = append(endpoints, endpoint.(T)) + } + } + if len(endpoints) == 0 { + return status.Error(codes.NotFound, "no "+endpointName+" endpoint found") + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + updated := make(chan struct{}, 1) + for _, endpoint := range endpoints { + go func(provider T) { + for { + statusUpdated := provider.StatusUpdated() + select { + case updated <- struct{}{}: + default: + } + select { + case <-statusUpdated: + case <-ctx.Done(): + return + } + } + }(endpoint) + } + + for { + select { + case <-updated: + case <-ctx.Done(): + return nil + } + err := send(endpoints) + if err != nil { + return err + } + } +} + func (s *StartedService) StartNetworkQualityTest( request *NetworkQualityTestRequest, server grpc.ServerStreamingServer[NetworkQualityTestProgress], @@ -1523,6 +1586,233 @@ func (s *StartedService) TailscaleLogout(ctx context.Context, request *Tailscale return &emptypb.Empty{}, nil } +func (s *StartedService) SubscribeOpenConnectStatus( + _ *emptypb.Empty, + server grpc.ServerStreamingServer[OpenConnectStatusUpdate], +) error { + err := s.waitForStarted(server.Context()) + if err != nil { + return err + } + s.serviceAccess.RLock() + boxService := s.instance + s.serviceAccess.RUnlock() + + endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx) + return subscribeEndpointStatus(server.Context(), endpointManager, C.TypeOpenConnect, "OpenConnect client", func(endpoints []adapter.OpenConnectEndpoint) error { + return server.Send(&OpenConnectStatusUpdate{ + Endpoints: common.Map(endpoints, func(endpoint adapter.OpenConnectEndpoint) *OpenConnectEndpointStatus { + return openConnectEndpointStatusToProto(endpoint.Tag(), endpoint.OpenConnectStatus()) + }), + }) + }) +} + +func openConnectEndpointStatusToProto(tag string, endpointStatus adapter.OpenConnectStatus) *OpenConnectEndpointStatus { + result := &OpenConnectEndpointStatus{ + EndpointTag: tag, + State: endpointStatus.State, + Error: endpointStatus.Error, + TunnelInfo: openConnectTunnelInfoToProto(endpointStatus.TunnelInfo), + } + if endpointStatus.AuthForm != nil { + fields := common.Map(endpointStatus.AuthForm.Fields, func(field adapter.OpenConnectAuthFormField) *OpenConnectAuthFormField { + return &OpenConnectAuthFormField{ + SubmissionKey: field.SubmissionKey, + Name: field.Name, + Label: field.Label, + Kind: field.Kind, + Value: field.Value, + Options: common.Map(field.Options, func(option adapter.OpenConnectAuthFormChoice) *OpenConnectAuthFormChoice { + return &OpenConnectAuthFormChoice{ + Value: option.Value, + Label: option.Label, + } + }), + } + }) + result.AuthForm = &OpenConnectAuthForm{ + Id: endpointStatus.AuthForm.ID, + Banner: endpointStatus.AuthForm.Banner, + Message: endpointStatus.AuthForm.Message, + Error: endpointStatus.AuthForm.Error, + Url: endpointStatus.AuthForm.URL, + Fields: fields, + } + } + return result +} + +func (s *StartedService) SubmitOpenConnectAuthForm(ctx context.Context, request *OpenConnectAuthFormSubmission) (*emptypb.Empty, error) { + err := s.waitForStarted(ctx) + if err != nil { + return nil, err + } + s.serviceAccess.RLock() + boxService := s.instance + s.serviceAccess.RUnlock() + + endpoint, err := resolveEndpoint[adapter.OpenConnectEndpoint](boxService, request.EndpointTag, C.TypeOpenConnect, "OpenConnect client") + if err != nil { + return nil, err + } + err = endpoint.CompleteAuthForm(request.FormID, request.Values) + if err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} + +func (s *StartedService) CancelOpenConnectAuthForm(ctx context.Context, request *OpenConnectAuthFormCancel) (*emptypb.Empty, error) { + err := s.waitForStarted(ctx) + if err != nil { + return nil, err + } + s.serviceAccess.RLock() + boxService := s.instance + s.serviceAccess.RUnlock() + + endpoint, err := resolveEndpoint[adapter.OpenConnectEndpoint](boxService, request.EndpointTag, C.TypeOpenConnect, "OpenConnect client") + if err != nil { + return nil, err + } + err = endpoint.CancelAuthForm(request.FormID) + if err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} + +func (s *StartedService) SubscribeOpenVPNStatus( + _ *emptypb.Empty, + server grpc.ServerStreamingServer[OpenVPNStatusUpdate], +) error { + err := s.waitForStarted(server.Context()) + if err != nil { + return err + } + s.serviceAccess.RLock() + boxService := s.instance + s.serviceAccess.RUnlock() + + endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx) + return subscribeEndpointStatus(server.Context(), endpointManager, C.TypeOpenVPNClient, "OpenVPN client", func(endpoints []adapter.OpenVPNEndpoint) error { + return server.Send(&OpenVPNStatusUpdate{ + Endpoints: common.Map(endpoints, func(endpoint adapter.OpenVPNEndpoint) *OpenVPNEndpointStatus { + return openVPNEndpointStatusToProto(endpoint.Tag(), endpoint.OpenVPNStatus()) + }), + }) + }) +} + +func openVPNEndpointStatusToProto(tag string, endpointStatus adapter.OpenVPNStatus) *OpenVPNEndpointStatus { + result := &OpenVPNEndpointStatus{ + EndpointTag: tag, + State: endpointStatus.State, + Error: endpointStatus.Error, + TunnelInfo: openVPNTunnelInfoToProto(endpointStatus.TunnelInfo), + } + if endpointStatus.Challenge != nil { + challenge := &OpenVPNChallenge{ + Id: endpointStatus.Challenge.ID, + Kind: endpointStatus.Challenge.Kind, + Username: endpointStatus.Challenge.Username, + Message: endpointStatus.Challenge.Message, + Url: endpointStatus.Challenge.URL, + SecretMessage: endpointStatus.Challenge.SecretMessage, + Echo: endpointStatus.Challenge.Echo, + PreviousError: endpointStatus.Challenge.PreviousError, + } + if !endpointStatus.Challenge.Deadline.IsZero() { + challenge.Deadline = endpointStatus.Challenge.Deadline.Unix() + } + result.Challenge = challenge + } + return result +} + +func openConnectTunnelInfoToProto(tunnelInfo *adapter.OpenConnectTunnelInfo) *OpenConnectTunnelInfo { + if tunnelInfo == nil { + return nil + } + result := &OpenConnectTunnelInfo{ + Server: tunnelInfo.Server, + Flavor: tunnelInfo.Flavor, + Transport: tunnelInfo.Transport, + Mtu: tunnelInfo.MTU, + } + if !tunnelInfo.ConnectedSince.IsZero() { + result.ConnectedSince = tunnelInfo.ConnectedSince.Unix() + } + result.Ipv4 = common.Map(tunnelInfo.IPv4, netip.Prefix.String) + result.Ipv6 = common.Map(tunnelInfo.IPv6, netip.Prefix.String) + result.Dns = common.Map(tunnelInfo.DNS, netip.Addr.String) + return result +} + +func openVPNTunnelInfoToProto(tunnelInfo *adapter.OpenVPNTunnelInfo) *OpenVPNTunnelInfo { + if tunnelInfo == nil { + return nil + } + result := &OpenVPNTunnelInfo{ + Server: tunnelInfo.Server, + Network: tunnelInfo.Network, + Cipher: tunnelInfo.Cipher, + Mtu: tunnelInfo.MTU, + } + if !tunnelInfo.ConnectedSince.IsZero() { + result.ConnectedSince = tunnelInfo.ConnectedSince.Unix() + } + result.Ipv4 = common.Map(tunnelInfo.IPv4, netip.Prefix.String) + result.Ipv6 = common.Map(tunnelInfo.IPv6, netip.Prefix.String) + result.Dns = common.Map(tunnelInfo.DNS, netip.Addr.String) + return result +} + +func (s *StartedService) SubmitOpenVPNChallengeResponse(ctx context.Context, request *OpenVPNChallengeSubmission) (*emptypb.Empty, error) { + err := s.waitForStarted(ctx) + if err != nil { + return nil, err + } + s.serviceAccess.RLock() + boxService := s.instance + s.serviceAccess.RUnlock() + + endpoint, err := resolveEndpoint[adapter.OpenVPNEndpoint](boxService, request.EndpointTag, C.TypeOpenVPNClient, "OpenVPN client") + if err != nil { + return nil, err + } + err = endpoint.CompleteChallenge(request.ChallengeID, adapter.OpenVPNChallengeResponse{ + Username: request.Username, + Password: request.Password, + Secret: request.Secret, + }) + if err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} + +func (s *StartedService) CancelOpenVPNChallenge(ctx context.Context, request *OpenVPNChallengeCancel) (*emptypb.Empty, error) { + err := s.waitForStarted(ctx) + if err != nil { + return nil, err + } + s.serviceAccess.RLock() + boxService := s.instance + s.serviceAccess.RUnlock() + + endpoint, err := resolveEndpoint[adapter.OpenVPNEndpoint](boxService, request.EndpointTag, C.TypeOpenVPNClient, "OpenVPN client") + if err != nil { + return nil, err + } + err = endpoint.CancelChallenge(request.ChallengeID) + if err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} + func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() { } diff --git a/daemon/started_service.pb.go b/daemon/started_service.pb.go index 8b0782a15..4ea5a778c 100644 --- a/daemon/started_service.pb.go +++ b/daemon/started_service.pb.go @@ -4618,6 +4618,1014 @@ func (x *USBSharedDevice) GetState() USBDeviceState { return USBDeviceState_USB_DEVICE_STATE_IDLE } +type OpenConnectStatusUpdate struct { + state protoimpl.MessageState `protogen:"open.v1"` + Endpoints []*OpenConnectEndpointStatus `protobuf:"bytes,1,rep,name=endpoints,proto3" json:"endpoints,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenConnectStatusUpdate) Reset() { + *x = OpenConnectStatusUpdate{} + mi := &file_daemon_started_service_proto_msgTypes[61] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenConnectStatusUpdate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenConnectStatusUpdate) ProtoMessage() {} + +func (x *OpenConnectStatusUpdate) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[61] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenConnectStatusUpdate.ProtoReflect.Descriptor instead. +func (*OpenConnectStatusUpdate) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{61} +} + +func (x *OpenConnectStatusUpdate) GetEndpoints() []*OpenConnectEndpointStatus { + if x != nil { + return x.Endpoints + } + return nil +} + +type OpenConnectEndpointStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + EndpointTag string `protobuf:"bytes,1,opt,name=endpointTag,proto3" json:"endpointTag,omitempty"` + State string `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` + AuthForm *OpenConnectAuthForm `protobuf:"bytes,3,opt,name=authForm,proto3" json:"authForm,omitempty"` + Error string `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"` + TunnelInfo *OpenConnectTunnelInfo `protobuf:"bytes,5,opt,name=tunnelInfo,proto3" json:"tunnelInfo,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenConnectEndpointStatus) Reset() { + *x = OpenConnectEndpointStatus{} + mi := &file_daemon_started_service_proto_msgTypes[62] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenConnectEndpointStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenConnectEndpointStatus) ProtoMessage() {} + +func (x *OpenConnectEndpointStatus) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[62] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenConnectEndpointStatus.ProtoReflect.Descriptor instead. +func (*OpenConnectEndpointStatus) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{62} +} + +func (x *OpenConnectEndpointStatus) GetEndpointTag() string { + if x != nil { + return x.EndpointTag + } + return "" +} + +func (x *OpenConnectEndpointStatus) GetState() string { + if x != nil { + return x.State + } + return "" +} + +func (x *OpenConnectEndpointStatus) GetAuthForm() *OpenConnectAuthForm { + if x != nil { + return x.AuthForm + } + return nil +} + +func (x *OpenConnectEndpointStatus) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *OpenConnectEndpointStatus) GetTunnelInfo() *OpenConnectTunnelInfo { + if x != nil { + return x.TunnelInfo + } + return nil +} + +type OpenConnectTunnelInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Server string `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` + Flavor string `protobuf:"bytes,2,opt,name=flavor,proto3" json:"flavor,omitempty"` + Transport string `protobuf:"bytes,3,opt,name=transport,proto3" json:"transport,omitempty"` + Ipv4 []string `protobuf:"bytes,4,rep,name=ipv4,proto3" json:"ipv4,omitempty"` + Ipv6 []string `protobuf:"bytes,5,rep,name=ipv6,proto3" json:"ipv6,omitempty"` + Dns []string `protobuf:"bytes,6,rep,name=dns,proto3" json:"dns,omitempty"` + Mtu uint32 `protobuf:"varint,7,opt,name=mtu,proto3" json:"mtu,omitempty"` + ConnectedSince int64 `protobuf:"varint,8,opt,name=connectedSince,proto3" json:"connectedSince,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenConnectTunnelInfo) Reset() { + *x = OpenConnectTunnelInfo{} + mi := &file_daemon_started_service_proto_msgTypes[63] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenConnectTunnelInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenConnectTunnelInfo) ProtoMessage() {} + +func (x *OpenConnectTunnelInfo) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[63] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenConnectTunnelInfo.ProtoReflect.Descriptor instead. +func (*OpenConnectTunnelInfo) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{63} +} + +func (x *OpenConnectTunnelInfo) GetServer() string { + if x != nil { + return x.Server + } + return "" +} + +func (x *OpenConnectTunnelInfo) GetFlavor() string { + if x != nil { + return x.Flavor + } + return "" +} + +func (x *OpenConnectTunnelInfo) GetTransport() string { + if x != nil { + return x.Transport + } + return "" +} + +func (x *OpenConnectTunnelInfo) GetIpv4() []string { + if x != nil { + return x.Ipv4 + } + return nil +} + +func (x *OpenConnectTunnelInfo) GetIpv6() []string { + if x != nil { + return x.Ipv6 + } + return nil +} + +func (x *OpenConnectTunnelInfo) GetDns() []string { + if x != nil { + return x.Dns + } + return nil +} + +func (x *OpenConnectTunnelInfo) GetMtu() uint32 { + if x != nil { + return x.Mtu + } + return 0 +} + +func (x *OpenConnectTunnelInfo) GetConnectedSince() int64 { + if x != nil { + return x.ConnectedSince + } + return 0 +} + +type OpenConnectAuthForm struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Banner string `protobuf:"bytes,2,opt,name=banner,proto3" json:"banner,omitempty"` + Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` + Error string `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"` + Url string `protobuf:"bytes,5,opt,name=url,proto3" json:"url,omitempty"` + Fields []*OpenConnectAuthFormField `protobuf:"bytes,6,rep,name=fields,proto3" json:"fields,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenConnectAuthForm) Reset() { + *x = OpenConnectAuthForm{} + mi := &file_daemon_started_service_proto_msgTypes[64] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenConnectAuthForm) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenConnectAuthForm) ProtoMessage() {} + +func (x *OpenConnectAuthForm) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[64] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenConnectAuthForm.ProtoReflect.Descriptor instead. +func (*OpenConnectAuthForm) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{64} +} + +func (x *OpenConnectAuthForm) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *OpenConnectAuthForm) GetBanner() string { + if x != nil { + return x.Banner + } + return "" +} + +func (x *OpenConnectAuthForm) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *OpenConnectAuthForm) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *OpenConnectAuthForm) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *OpenConnectAuthForm) GetFields() []*OpenConnectAuthFormField { + if x != nil { + return x.Fields + } + return nil +} + +type OpenConnectAuthFormField struct { + state protoimpl.MessageState `protogen:"open.v1"` + SubmissionKey string `protobuf:"bytes,1,opt,name=submissionKey,proto3" json:"submissionKey,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Label string `protobuf:"bytes,3,opt,name=label,proto3" json:"label,omitempty"` + Kind string `protobuf:"bytes,4,opt,name=kind,proto3" json:"kind,omitempty"` + Value string `protobuf:"bytes,5,opt,name=value,proto3" json:"value,omitempty"` + Options []*OpenConnectAuthFormChoice `protobuf:"bytes,6,rep,name=options,proto3" json:"options,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenConnectAuthFormField) Reset() { + *x = OpenConnectAuthFormField{} + mi := &file_daemon_started_service_proto_msgTypes[65] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenConnectAuthFormField) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenConnectAuthFormField) ProtoMessage() {} + +func (x *OpenConnectAuthFormField) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[65] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenConnectAuthFormField.ProtoReflect.Descriptor instead. +func (*OpenConnectAuthFormField) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{65} +} + +func (x *OpenConnectAuthFormField) GetSubmissionKey() string { + if x != nil { + return x.SubmissionKey + } + return "" +} + +func (x *OpenConnectAuthFormField) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *OpenConnectAuthFormField) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +func (x *OpenConnectAuthFormField) GetKind() string { + if x != nil { + return x.Kind + } + return "" +} + +func (x *OpenConnectAuthFormField) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +func (x *OpenConnectAuthFormField) GetOptions() []*OpenConnectAuthFormChoice { + if x != nil { + return x.Options + } + return nil +} + +type OpenConnectAuthFormChoice struct { + state protoimpl.MessageState `protogen:"open.v1"` + Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` + Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenConnectAuthFormChoice) Reset() { + *x = OpenConnectAuthFormChoice{} + mi := &file_daemon_started_service_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenConnectAuthFormChoice) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenConnectAuthFormChoice) ProtoMessage() {} + +func (x *OpenConnectAuthFormChoice) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[66] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenConnectAuthFormChoice.ProtoReflect.Descriptor instead. +func (*OpenConnectAuthFormChoice) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{66} +} + +func (x *OpenConnectAuthFormChoice) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +func (x *OpenConnectAuthFormChoice) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +type OpenConnectAuthFormSubmission struct { + state protoimpl.MessageState `protogen:"open.v1"` + EndpointTag string `protobuf:"bytes,1,opt,name=endpointTag,proto3" json:"endpointTag,omitempty"` + FormID string `protobuf:"bytes,2,opt,name=formID,proto3" json:"formID,omitempty"` + Values map[string]string `protobuf:"bytes,3,rep,name=values,proto3" json:"values,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenConnectAuthFormSubmission) Reset() { + *x = OpenConnectAuthFormSubmission{} + mi := &file_daemon_started_service_proto_msgTypes[67] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenConnectAuthFormSubmission) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenConnectAuthFormSubmission) ProtoMessage() {} + +func (x *OpenConnectAuthFormSubmission) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[67] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenConnectAuthFormSubmission.ProtoReflect.Descriptor instead. +func (*OpenConnectAuthFormSubmission) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{67} +} + +func (x *OpenConnectAuthFormSubmission) GetEndpointTag() string { + if x != nil { + return x.EndpointTag + } + return "" +} + +func (x *OpenConnectAuthFormSubmission) GetFormID() string { + if x != nil { + return x.FormID + } + return "" +} + +func (x *OpenConnectAuthFormSubmission) GetValues() map[string]string { + if x != nil { + return x.Values + } + return nil +} + +type OpenConnectAuthFormCancel struct { + state protoimpl.MessageState `protogen:"open.v1"` + EndpointTag string `protobuf:"bytes,1,opt,name=endpointTag,proto3" json:"endpointTag,omitempty"` + FormID string `protobuf:"bytes,2,opt,name=formID,proto3" json:"formID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenConnectAuthFormCancel) Reset() { + *x = OpenConnectAuthFormCancel{} + mi := &file_daemon_started_service_proto_msgTypes[68] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenConnectAuthFormCancel) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenConnectAuthFormCancel) ProtoMessage() {} + +func (x *OpenConnectAuthFormCancel) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[68] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenConnectAuthFormCancel.ProtoReflect.Descriptor instead. +func (*OpenConnectAuthFormCancel) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{68} +} + +func (x *OpenConnectAuthFormCancel) GetEndpointTag() string { + if x != nil { + return x.EndpointTag + } + return "" +} + +func (x *OpenConnectAuthFormCancel) GetFormID() string { + if x != nil { + return x.FormID + } + return "" +} + +type OpenVPNStatusUpdate struct { + state protoimpl.MessageState `protogen:"open.v1"` + Endpoints []*OpenVPNEndpointStatus `protobuf:"bytes,1,rep,name=endpoints,proto3" json:"endpoints,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenVPNStatusUpdate) Reset() { + *x = OpenVPNStatusUpdate{} + mi := &file_daemon_started_service_proto_msgTypes[69] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenVPNStatusUpdate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenVPNStatusUpdate) ProtoMessage() {} + +func (x *OpenVPNStatusUpdate) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[69] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenVPNStatusUpdate.ProtoReflect.Descriptor instead. +func (*OpenVPNStatusUpdate) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{69} +} + +func (x *OpenVPNStatusUpdate) GetEndpoints() []*OpenVPNEndpointStatus { + if x != nil { + return x.Endpoints + } + return nil +} + +type OpenVPNEndpointStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + EndpointTag string `protobuf:"bytes,1,opt,name=endpointTag,proto3" json:"endpointTag,omitempty"` + State string `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` + Challenge *OpenVPNChallenge `protobuf:"bytes,3,opt,name=challenge,proto3" json:"challenge,omitempty"` + Error string `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"` + TunnelInfo *OpenVPNTunnelInfo `protobuf:"bytes,5,opt,name=tunnelInfo,proto3" json:"tunnelInfo,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenVPNEndpointStatus) Reset() { + *x = OpenVPNEndpointStatus{} + mi := &file_daemon_started_service_proto_msgTypes[70] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenVPNEndpointStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenVPNEndpointStatus) ProtoMessage() {} + +func (x *OpenVPNEndpointStatus) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[70] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenVPNEndpointStatus.ProtoReflect.Descriptor instead. +func (*OpenVPNEndpointStatus) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{70} +} + +func (x *OpenVPNEndpointStatus) GetEndpointTag() string { + if x != nil { + return x.EndpointTag + } + return "" +} + +func (x *OpenVPNEndpointStatus) GetState() string { + if x != nil { + return x.State + } + return "" +} + +func (x *OpenVPNEndpointStatus) GetChallenge() *OpenVPNChallenge { + if x != nil { + return x.Challenge + } + return nil +} + +func (x *OpenVPNEndpointStatus) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *OpenVPNEndpointStatus) GetTunnelInfo() *OpenVPNTunnelInfo { + if x != nil { + return x.TunnelInfo + } + return nil +} + +type OpenVPNTunnelInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Server string `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` + Network string `protobuf:"bytes,3,opt,name=network,proto3" json:"network,omitempty"` + Ipv4 []string `protobuf:"bytes,4,rep,name=ipv4,proto3" json:"ipv4,omitempty"` + Ipv6 []string `protobuf:"bytes,5,rep,name=ipv6,proto3" json:"ipv6,omitempty"` + Dns []string `protobuf:"bytes,6,rep,name=dns,proto3" json:"dns,omitempty"` + Mtu uint32 `protobuf:"varint,7,opt,name=mtu,proto3" json:"mtu,omitempty"` + ConnectedSince int64 `protobuf:"varint,8,opt,name=connectedSince,proto3" json:"connectedSince,omitempty"` + Cipher string `protobuf:"bytes,9,opt,name=cipher,proto3" json:"cipher,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenVPNTunnelInfo) Reset() { + *x = OpenVPNTunnelInfo{} + mi := &file_daemon_started_service_proto_msgTypes[71] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenVPNTunnelInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenVPNTunnelInfo) ProtoMessage() {} + +func (x *OpenVPNTunnelInfo) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[71] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenVPNTunnelInfo.ProtoReflect.Descriptor instead. +func (*OpenVPNTunnelInfo) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{71} +} + +func (x *OpenVPNTunnelInfo) GetServer() string { + if x != nil { + return x.Server + } + return "" +} + +func (x *OpenVPNTunnelInfo) GetNetwork() string { + if x != nil { + return x.Network + } + return "" +} + +func (x *OpenVPNTunnelInfo) GetIpv4() []string { + if x != nil { + return x.Ipv4 + } + return nil +} + +func (x *OpenVPNTunnelInfo) GetIpv6() []string { + if x != nil { + return x.Ipv6 + } + return nil +} + +func (x *OpenVPNTunnelInfo) GetDns() []string { + if x != nil { + return x.Dns + } + return nil +} + +func (x *OpenVPNTunnelInfo) GetMtu() uint32 { + if x != nil { + return x.Mtu + } + return 0 +} + +func (x *OpenVPNTunnelInfo) GetConnectedSince() int64 { + if x != nil { + return x.ConnectedSince + } + return 0 +} + +func (x *OpenVPNTunnelInfo) GetCipher() string { + if x != nil { + return x.Cipher + } + return "" +} + +type OpenVPNChallenge struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Kind string `protobuf:"bytes,2,opt,name=kind,proto3" json:"kind,omitempty"` + Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"` + Message string `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"` + Url string `protobuf:"bytes,5,opt,name=url,proto3" json:"url,omitempty"` + SecretMessage string `protobuf:"bytes,6,opt,name=secretMessage,proto3" json:"secretMessage,omitempty"` + Echo bool `protobuf:"varint,7,opt,name=echo,proto3" json:"echo,omitempty"` + PreviousError string `protobuf:"bytes,8,opt,name=previousError,proto3" json:"previousError,omitempty"` + Deadline int64 `protobuf:"varint,9,opt,name=deadline,proto3" json:"deadline,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenVPNChallenge) Reset() { + *x = OpenVPNChallenge{} + mi := &file_daemon_started_service_proto_msgTypes[72] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenVPNChallenge) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenVPNChallenge) ProtoMessage() {} + +func (x *OpenVPNChallenge) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[72] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenVPNChallenge.ProtoReflect.Descriptor instead. +func (*OpenVPNChallenge) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{72} +} + +func (x *OpenVPNChallenge) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *OpenVPNChallenge) GetKind() string { + if x != nil { + return x.Kind + } + return "" +} + +func (x *OpenVPNChallenge) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *OpenVPNChallenge) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *OpenVPNChallenge) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *OpenVPNChallenge) GetSecretMessage() string { + if x != nil { + return x.SecretMessage + } + return "" +} + +func (x *OpenVPNChallenge) GetEcho() bool { + if x != nil { + return x.Echo + } + return false +} + +func (x *OpenVPNChallenge) GetPreviousError() string { + if x != nil { + return x.PreviousError + } + return "" +} + +func (x *OpenVPNChallenge) GetDeadline() int64 { + if x != nil { + return x.Deadline + } + return 0 +} + +type OpenVPNChallengeSubmission struct { + state protoimpl.MessageState `protogen:"open.v1"` + EndpointTag string `protobuf:"bytes,1,opt,name=endpointTag,proto3" json:"endpointTag,omitempty"` + ChallengeID string `protobuf:"bytes,2,opt,name=challengeID,proto3" json:"challengeID,omitempty"` + Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"` + Password string `protobuf:"bytes,4,opt,name=password,proto3" json:"password,omitempty"` + Secret string `protobuf:"bytes,5,opt,name=secret,proto3" json:"secret,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenVPNChallengeSubmission) Reset() { + *x = OpenVPNChallengeSubmission{} + mi := &file_daemon_started_service_proto_msgTypes[73] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenVPNChallengeSubmission) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenVPNChallengeSubmission) ProtoMessage() {} + +func (x *OpenVPNChallengeSubmission) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[73] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenVPNChallengeSubmission.ProtoReflect.Descriptor instead. +func (*OpenVPNChallengeSubmission) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{73} +} + +func (x *OpenVPNChallengeSubmission) GetEndpointTag() string { + if x != nil { + return x.EndpointTag + } + return "" +} + +func (x *OpenVPNChallengeSubmission) GetChallengeID() string { + if x != nil { + return x.ChallengeID + } + return "" +} + +func (x *OpenVPNChallengeSubmission) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *OpenVPNChallengeSubmission) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *OpenVPNChallengeSubmission) GetSecret() string { + if x != nil { + return x.Secret + } + return "" +} + +type OpenVPNChallengeCancel struct { + state protoimpl.MessageState `protogen:"open.v1"` + EndpointTag string `protobuf:"bytes,1,opt,name=endpointTag,proto3" json:"endpointTag,omitempty"` + ChallengeID string `protobuf:"bytes,2,opt,name=challengeID,proto3" json:"challengeID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenVPNChallengeCancel) Reset() { + *x = OpenVPNChallengeCancel{} + mi := &file_daemon_started_service_proto_msgTypes[74] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenVPNChallengeCancel) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenVPNChallengeCancel) ProtoMessage() {} + +func (x *OpenVPNChallengeCancel) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[74] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenVPNChallengeCancel.ProtoReflect.Descriptor instead. +func (*OpenVPNChallengeCancel) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{74} +} + +func (x *OpenVPNChallengeCancel) GetEndpointTag() string { + if x != nil { + return x.EndpointTag + } + return "" +} + +func (x *OpenVPNChallengeCancel) GetChallengeID() string { + if x != nil { + return x.ChallengeID + } + return "" +} + type Log_Message struct { state protoimpl.MessageState `protogen:"open.v1"` Level LogLevel `protobuf:"varint,1,opt,name=level,proto3,enum=daemon.LogLevel" json:"level,omitempty"` @@ -4628,7 +5636,7 @@ type Log_Message struct { func (x *Log_Message) Reset() { *x = Log_Message{} - mi := &file_daemon_started_service_proto_msgTypes[61] + mi := &file_daemon_started_service_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4640,7 +5648,7 @@ func (x *Log_Message) String() string { func (*Log_Message) ProtoMessage() {} func (x *Log_Message) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[61] + mi := &file_daemon_started_service_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5030,7 +6038,91 @@ const file_daemon_started_service_proto_rawDesc = "" + "\x05busId\x18\x02 \x01(\tR\x05busId\x12\x1a\n" + "\bstableId\x18\x03 \x01(\tR\bstableId\x12,\n" + "\abackend\x18\x04 \x01(\x0e2\x12.daemon.USBBackendR\abackend\x12,\n" + - "\x05state\x18\x05 \x01(\x0e2\x16.daemon.USBDeviceStateR\x05state*U\n" + + "\x05state\x18\x05 \x01(\x0e2\x16.daemon.USBDeviceStateR\x05state\"Z\n" + + "\x17OpenConnectStatusUpdate\x12?\n" + + "\tendpoints\x18\x01 \x03(\v2!.daemon.OpenConnectEndpointStatusR\tendpoints\"\xe1\x01\n" + + "\x19OpenConnectEndpointStatus\x12 \n" + + "\vendpointTag\x18\x01 \x01(\tR\vendpointTag\x12\x14\n" + + "\x05state\x18\x02 \x01(\tR\x05state\x127\n" + + "\bauthForm\x18\x03 \x01(\v2\x1b.daemon.OpenConnectAuthFormR\bauthForm\x12\x14\n" + + "\x05error\x18\x04 \x01(\tR\x05error\x12=\n" + + "\n" + + "tunnelInfo\x18\x05 \x01(\v2\x1d.daemon.OpenConnectTunnelInfoR\n" + + "tunnelInfo\"\xd9\x01\n" + + "\x15OpenConnectTunnelInfo\x12\x16\n" + + "\x06server\x18\x01 \x01(\tR\x06server\x12\x16\n" + + "\x06flavor\x18\x02 \x01(\tR\x06flavor\x12\x1c\n" + + "\ttransport\x18\x03 \x01(\tR\ttransport\x12\x12\n" + + "\x04ipv4\x18\x04 \x03(\tR\x04ipv4\x12\x12\n" + + "\x04ipv6\x18\x05 \x03(\tR\x04ipv6\x12\x10\n" + + "\x03dns\x18\x06 \x03(\tR\x03dns\x12\x10\n" + + "\x03mtu\x18\a \x01(\rR\x03mtu\x12&\n" + + "\x0econnectedSince\x18\b \x01(\x03R\x0econnectedSince\"\xb9\x01\n" + + "\x13OpenConnectAuthForm\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x16\n" + + "\x06banner\x18\x02 \x01(\tR\x06banner\x12\x18\n" + + "\amessage\x18\x03 \x01(\tR\amessage\x12\x14\n" + + "\x05error\x18\x04 \x01(\tR\x05error\x12\x10\n" + + "\x03url\x18\x05 \x01(\tR\x03url\x128\n" + + "\x06fields\x18\x06 \x03(\v2 .daemon.OpenConnectAuthFormFieldR\x06fields\"\xd1\x01\n" + + "\x18OpenConnectAuthFormField\x12$\n" + + "\rsubmissionKey\x18\x01 \x01(\tR\rsubmissionKey\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x14\n" + + "\x05label\x18\x03 \x01(\tR\x05label\x12\x12\n" + + "\x04kind\x18\x04 \x01(\tR\x04kind\x12\x14\n" + + "\x05value\x18\x05 \x01(\tR\x05value\x12;\n" + + "\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\"\xdf\x01\n" + + "\x1dOpenConnectAuthFormSubmission\x12 \n" + + "\vendpointTag\x18\x01 \x01(\tR\vendpointTag\x12\x16\n" + + "\x06formID\x18\x02 \x01(\tR\x06formID\x12I\n" + + "\x06values\x18\x03 \x03(\v21.daemon.OpenConnectAuthFormSubmission.ValuesEntryR\x06values\x1a9\n" + + "\vValuesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"U\n" + + "\x19OpenConnectAuthFormCancel\x12 \n" + + "\vendpointTag\x18\x01 \x01(\tR\vendpointTag\x12\x16\n" + + "\x06formID\x18\x02 \x01(\tR\x06formID\"R\n" + + "\x13OpenVPNStatusUpdate\x12;\n" + + "\tendpoints\x18\x01 \x03(\v2\x1d.daemon.OpenVPNEndpointStatusR\tendpoints\"\xd8\x01\n" + + "\x15OpenVPNEndpointStatus\x12 \n" + + "\vendpointTag\x18\x01 \x01(\tR\vendpointTag\x12\x14\n" + + "\x05state\x18\x02 \x01(\tR\x05state\x126\n" + + "\tchallenge\x18\x03 \x01(\v2\x18.daemon.OpenVPNChallengeR\tchallenge\x12\x14\n" + + "\x05error\x18\x04 \x01(\tR\x05error\x129\n" + + "\n" + + "tunnelInfo\x18\x05 \x01(\v2\x19.daemon.OpenVPNTunnelInfoR\n" + + "tunnelInfo\"\xd7\x01\n" + + "\x11OpenVPNTunnelInfo\x12\x16\n" + + "\x06server\x18\x01 \x01(\tR\x06server\x12\x18\n" + + "\anetwork\x18\x03 \x01(\tR\anetwork\x12\x12\n" + + "\x04ipv4\x18\x04 \x03(\tR\x04ipv4\x12\x12\n" + + "\x04ipv6\x18\x05 \x03(\tR\x04ipv6\x12\x10\n" + + "\x03dns\x18\x06 \x03(\tR\x03dns\x12\x10\n" + + "\x03mtu\x18\a \x01(\rR\x03mtu\x12&\n" + + "\x0econnectedSince\x18\b \x01(\x03R\x0econnectedSince\x12\x16\n" + + "\x06cipher\x18\t \x01(\tR\x06cipherJ\x04\b\x02\x10\x03\"\xfa\x01\n" + + "\x10OpenVPNChallenge\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04kind\x18\x02 \x01(\tR\x04kind\x12\x1a\n" + + "\busername\x18\x03 \x01(\tR\busername\x12\x18\n" + + "\amessage\x18\x04 \x01(\tR\amessage\x12\x10\n" + + "\x03url\x18\x05 \x01(\tR\x03url\x12$\n" + + "\rsecretMessage\x18\x06 \x01(\tR\rsecretMessage\x12\x12\n" + + "\x04echo\x18\a \x01(\bR\x04echo\x12$\n" + + "\rpreviousError\x18\b \x01(\tR\rpreviousError\x12\x1a\n" + + "\bdeadline\x18\t \x01(\x03R\bdeadline\"\xb0\x01\n" + + "\x1aOpenVPNChallengeSubmission\x12 \n" + + "\vendpointTag\x18\x01 \x01(\tR\vendpointTag\x12 \n" + + "\vchallengeID\x18\x02 \x01(\tR\vchallengeID\x12\x1a\n" + + "\busername\x18\x03 \x01(\tR\busername\x12\x1a\n" + + "\bpassword\x18\x04 \x01(\tR\bpassword\x12\x16\n" + + "\x06secret\x18\x05 \x01(\tR\x06secret\"\\\n" + + "\x16OpenVPNChallengeCancel\x12 \n" + + "\vendpointTag\x18\x01 \x01(\tR\vendpointTag\x12 \n" + + "\vchallengeID\x18\x02 \x01(\tR\vchallengeID*U\n" + "\bLogLevel\x12\t\n" + "\x05PANIC\x10\x00\x12\t\n" + "\x05FATAL\x10\x01\x12\t\n" + @@ -5053,7 +6145,7 @@ const file_daemon_started_service_proto_rawDesc = "" + "\x17USB_BACKEND_LINUX_SYSFS\x10\x01\x12\x17\n" + "\x13USB_BACKEND_DYNAMIC\x10\x02\x12\x1c\n" + "\x18USB_BACKEND_DARWIN_IOKIT\x10\x03\x12\x1f\n" + - "\x1bUSB_BACKEND_WINDOWS_VBOXUSB\x10\x042\xd4\x10\n" + + "\x1bUSB_BACKEND_WINDOWS_VBOXUSB\x10\x042\xee\x14\n" + "\x0eStartedService\x127\n" + "\n" + "GetVersion\x12\x16.google.protobuf.Empty\x1a\x0f.daemon.Version\"\x00\x12K\n" + @@ -5083,7 +6175,13 @@ const file_daemon_started_service_proto_rawDesc = "" + "\x0fTailscaleLogout\x12\x1e.daemon.TailscaleLogoutRequest\x1a\x16.google.protobuf.Empty\"\x00\x12f\n" + "\x18StartTailscaleSSHSession\x12!.daemon.TailscaleSSHClientMessage\x1a!.daemon.TailscaleSSHServerMessage\"\x00(\x010\x01\x12O\n" + "\x11ProvideUSBDevices\x12\x1a.daemon.USBProviderMessage\x1a\x18.daemon.USBServerMessage\"\x00(\x010\x01\x12Y\n" + - "\x1aSubscribeUSBIPServerStatus\x12\x16.google.protobuf.Empty\x1a\x1f.daemon.USBIPServerStatusUpdate\"\x000\x01B%Z#github.com/sagernet/sing-box/daemonb\x06proto3" + "\x1aSubscribeUSBIPServerStatus\x12\x16.google.protobuf.Empty\x1a\x1f.daemon.USBIPServerStatusUpdate\"\x000\x01\x12Y\n" + + "\x1aSubscribeOpenConnectStatus\x12\x16.google.protobuf.Empty\x1a\x1f.daemon.OpenConnectStatusUpdate\"\x000\x01\x12\\\n" + + "\x19SubmitOpenConnectAuthForm\x12%.daemon.OpenConnectAuthFormSubmission\x1a\x16.google.protobuf.Empty\"\x00\x12X\n" + + "\x19CancelOpenConnectAuthForm\x12!.daemon.OpenConnectAuthFormCancel\x1a\x16.google.protobuf.Empty\"\x00\x12Q\n" + + "\x16SubscribeOpenVPNStatus\x12\x16.google.protobuf.Empty\x1a\x1b.daemon.OpenVPNStatusUpdate\"\x000\x01\x12^\n" + + "\x1eSubmitOpenVPNChallengeResponse\x12\".daemon.OpenVPNChallengeSubmission\x1a\x16.google.protobuf.Empty\"\x00\x12R\n" + + "\x16CancelOpenVPNChallenge\x12\x1e.daemon.OpenVPNChallengeCancel\x1a\x16.google.protobuf.Empty\"\x00B%Z#github.com/sagernet/sing-box/daemonb\x06proto3" var ( file_daemon_started_service_proto_rawDescOnce sync.Once @@ -5099,82 +6197,96 @@ func file_daemon_started_service_proto_rawDescGZIP() []byte { var ( file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 5) - file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 62) + file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 77) file_daemon_started_service_proto_goTypes = []any{ - (LogLevel)(0), // 0: daemon.LogLevel - (ConnectionEventType)(0), // 1: daemon.ConnectionEventType - (USBDeviceState)(0), // 2: daemon.USBDeviceState - (USBBackend)(0), // 3: daemon.USBBackend - (ServiceStatus_Type)(0), // 4: daemon.ServiceStatus.Type - (*Version)(nil), // 5: daemon.Version - (*ServiceStatus)(nil), // 6: daemon.ServiceStatus - (*SubscribeStatusRequest)(nil), // 7: daemon.SubscribeStatusRequest - (*Log)(nil), // 8: daemon.Log - (*DefaultLogLevel)(nil), // 9: daemon.DefaultLogLevel - (*Status)(nil), // 10: daemon.Status - (*Groups)(nil), // 11: daemon.Groups - (*Group)(nil), // 12: daemon.Group - (*GroupItem)(nil), // 13: daemon.GroupItem - (*URLTestRequest)(nil), // 14: daemon.URLTestRequest - (*SelectOutboundRequest)(nil), // 15: daemon.SelectOutboundRequest - (*SetGroupExpandRequest)(nil), // 16: daemon.SetGroupExpandRequest - (*ClashMode)(nil), // 17: daemon.ClashMode - (*ClashModeStatus)(nil), // 18: daemon.ClashModeStatus - (*SubscribeConnectionsRequest)(nil), // 19: daemon.SubscribeConnectionsRequest - (*ConnectionEvent)(nil), // 20: daemon.ConnectionEvent - (*ConnectionEvents)(nil), // 21: daemon.ConnectionEvents - (*Connection)(nil), // 22: daemon.Connection - (*ProcessInfo)(nil), // 23: daemon.ProcessInfo - (*CloseConnectionRequest)(nil), // 24: daemon.CloseConnectionRequest - (*DeprecatedWarnings)(nil), // 25: daemon.DeprecatedWarnings - (*DeprecatedWarning)(nil), // 26: daemon.DeprecatedWarning - (*StartedAt)(nil), // 27: daemon.StartedAt - (*OutboundList)(nil), // 28: daemon.OutboundList - (*NetworkQualityTestRequest)(nil), // 29: daemon.NetworkQualityTestRequest - (*NetworkQualityTestProgress)(nil), // 30: daemon.NetworkQualityTestProgress - (*STUNTestRequest)(nil), // 31: daemon.STUNTestRequest - (*STUNTestProgress)(nil), // 32: daemon.STUNTestProgress - (*TailscaleStatusUpdate)(nil), // 33: daemon.TailscaleStatusUpdate - (*TailscaleEndpointStatus)(nil), // 34: daemon.TailscaleEndpointStatus - (*TailscaleUserGroup)(nil), // 35: daemon.TailscaleUserGroup - (*TailscalePeer)(nil), // 36: daemon.TailscalePeer - (*TailscalePingRequest)(nil), // 37: daemon.TailscalePingRequest - (*TailscalePingResponse)(nil), // 38: daemon.TailscalePingResponse - (*SetTailscaleExitNodeRequest)(nil), // 39: daemon.SetTailscaleExitNodeRequest - (*TailscaleLogoutRequest)(nil), // 40: daemon.TailscaleLogoutRequest - (*TailscaleSSHClientMessage)(nil), // 41: daemon.TailscaleSSHClientMessage - (*TailscaleSSHStart)(nil), // 42: daemon.TailscaleSSHStart - (*TailscaleSSHInput)(nil), // 43: daemon.TailscaleSSHInput - (*TailscaleSSHResize)(nil), // 44: daemon.TailscaleSSHResize - (*TailscaleSSHServerMessage)(nil), // 45: daemon.TailscaleSSHServerMessage - (*TailscaleSSHAuthBanner)(nil), // 46: daemon.TailscaleSSHAuthBanner - (*TailscaleSSHReady)(nil), // 47: daemon.TailscaleSSHReady - (*TailscaleSSHOutput)(nil), // 48: daemon.TailscaleSSHOutput - (*TailscaleSSHExit)(nil), // 49: daemon.TailscaleSSHExit - (*TailscaleSSHError)(nil), // 50: daemon.TailscaleSSHError - (*USBProviderMessage)(nil), // 51: daemon.USBProviderMessage - (*USBServerMessage)(nil), // 52: daemon.USBServerMessage - (*USBDeviceDescriptor)(nil), // 53: daemon.USBDeviceDescriptor - (*USBDeviceAttach)(nil), // 54: daemon.USBDeviceAttach - (*USBInterface)(nil), // 55: daemon.USBInterface - (*USBDeviceDetach)(nil), // 56: daemon.USBDeviceDetach - (*USBDeviceReady)(nil), // 57: daemon.USBDeviceReady - (*USBURBRequest)(nil), // 58: daemon.USBURBRequest - (*USBURBResponse)(nil), // 59: daemon.USBURBResponse - (*USBIsoPacket)(nil), // 60: daemon.USBIsoPacket - (*USBEndpointAbort)(nil), // 61: daemon.USBEndpointAbort - (*USBError)(nil), // 62: daemon.USBError - (*USBIPServerStatusUpdate)(nil), // 63: daemon.USBIPServerStatusUpdate - (*USBIPServerStatus)(nil), // 64: daemon.USBIPServerStatus - (*USBSharedDevice)(nil), // 65: daemon.USBSharedDevice - (*Log_Message)(nil), // 66: daemon.Log.Message - (*emptypb.Empty)(nil), // 67: google.protobuf.Empty + (LogLevel)(0), // 0: daemon.LogLevel + (ConnectionEventType)(0), // 1: daemon.ConnectionEventType + (USBDeviceState)(0), // 2: daemon.USBDeviceState + (USBBackend)(0), // 3: daemon.USBBackend + (ServiceStatus_Type)(0), // 4: daemon.ServiceStatus.Type + (*Version)(nil), // 5: daemon.Version + (*ServiceStatus)(nil), // 6: daemon.ServiceStatus + (*SubscribeStatusRequest)(nil), // 7: daemon.SubscribeStatusRequest + (*Log)(nil), // 8: daemon.Log + (*DefaultLogLevel)(nil), // 9: daemon.DefaultLogLevel + (*Status)(nil), // 10: daemon.Status + (*Groups)(nil), // 11: daemon.Groups + (*Group)(nil), // 12: daemon.Group + (*GroupItem)(nil), // 13: daemon.GroupItem + (*URLTestRequest)(nil), // 14: daemon.URLTestRequest + (*SelectOutboundRequest)(nil), // 15: daemon.SelectOutboundRequest + (*SetGroupExpandRequest)(nil), // 16: daemon.SetGroupExpandRequest + (*ClashMode)(nil), // 17: daemon.ClashMode + (*ClashModeStatus)(nil), // 18: daemon.ClashModeStatus + (*SubscribeConnectionsRequest)(nil), // 19: daemon.SubscribeConnectionsRequest + (*ConnectionEvent)(nil), // 20: daemon.ConnectionEvent + (*ConnectionEvents)(nil), // 21: daemon.ConnectionEvents + (*Connection)(nil), // 22: daemon.Connection + (*ProcessInfo)(nil), // 23: daemon.ProcessInfo + (*CloseConnectionRequest)(nil), // 24: daemon.CloseConnectionRequest + (*DeprecatedWarnings)(nil), // 25: daemon.DeprecatedWarnings + (*DeprecatedWarning)(nil), // 26: daemon.DeprecatedWarning + (*StartedAt)(nil), // 27: daemon.StartedAt + (*OutboundList)(nil), // 28: daemon.OutboundList + (*NetworkQualityTestRequest)(nil), // 29: daemon.NetworkQualityTestRequest + (*NetworkQualityTestProgress)(nil), // 30: daemon.NetworkQualityTestProgress + (*STUNTestRequest)(nil), // 31: daemon.STUNTestRequest + (*STUNTestProgress)(nil), // 32: daemon.STUNTestProgress + (*TailscaleStatusUpdate)(nil), // 33: daemon.TailscaleStatusUpdate + (*TailscaleEndpointStatus)(nil), // 34: daemon.TailscaleEndpointStatus + (*TailscaleUserGroup)(nil), // 35: daemon.TailscaleUserGroup + (*TailscalePeer)(nil), // 36: daemon.TailscalePeer + (*TailscalePingRequest)(nil), // 37: daemon.TailscalePingRequest + (*TailscalePingResponse)(nil), // 38: daemon.TailscalePingResponse + (*SetTailscaleExitNodeRequest)(nil), // 39: daemon.SetTailscaleExitNodeRequest + (*TailscaleLogoutRequest)(nil), // 40: daemon.TailscaleLogoutRequest + (*TailscaleSSHClientMessage)(nil), // 41: daemon.TailscaleSSHClientMessage + (*TailscaleSSHStart)(nil), // 42: daemon.TailscaleSSHStart + (*TailscaleSSHInput)(nil), // 43: daemon.TailscaleSSHInput + (*TailscaleSSHResize)(nil), // 44: daemon.TailscaleSSHResize + (*TailscaleSSHServerMessage)(nil), // 45: daemon.TailscaleSSHServerMessage + (*TailscaleSSHAuthBanner)(nil), // 46: daemon.TailscaleSSHAuthBanner + (*TailscaleSSHReady)(nil), // 47: daemon.TailscaleSSHReady + (*TailscaleSSHOutput)(nil), // 48: daemon.TailscaleSSHOutput + (*TailscaleSSHExit)(nil), // 49: daemon.TailscaleSSHExit + (*TailscaleSSHError)(nil), // 50: daemon.TailscaleSSHError + (*USBProviderMessage)(nil), // 51: daemon.USBProviderMessage + (*USBServerMessage)(nil), // 52: daemon.USBServerMessage + (*USBDeviceDescriptor)(nil), // 53: daemon.USBDeviceDescriptor + (*USBDeviceAttach)(nil), // 54: daemon.USBDeviceAttach + (*USBInterface)(nil), // 55: daemon.USBInterface + (*USBDeviceDetach)(nil), // 56: daemon.USBDeviceDetach + (*USBDeviceReady)(nil), // 57: daemon.USBDeviceReady + (*USBURBRequest)(nil), // 58: daemon.USBURBRequest + (*USBURBResponse)(nil), // 59: daemon.USBURBResponse + (*USBIsoPacket)(nil), // 60: daemon.USBIsoPacket + (*USBEndpointAbort)(nil), // 61: daemon.USBEndpointAbort + (*USBError)(nil), // 62: daemon.USBError + (*USBIPServerStatusUpdate)(nil), // 63: daemon.USBIPServerStatusUpdate + (*USBIPServerStatus)(nil), // 64: daemon.USBIPServerStatus + (*USBSharedDevice)(nil), // 65: daemon.USBSharedDevice + (*OpenConnectStatusUpdate)(nil), // 66: daemon.OpenConnectStatusUpdate + (*OpenConnectEndpointStatus)(nil), // 67: daemon.OpenConnectEndpointStatus + (*OpenConnectTunnelInfo)(nil), // 68: daemon.OpenConnectTunnelInfo + (*OpenConnectAuthForm)(nil), // 69: daemon.OpenConnectAuthForm + (*OpenConnectAuthFormField)(nil), // 70: daemon.OpenConnectAuthFormField + (*OpenConnectAuthFormChoice)(nil), // 71: daemon.OpenConnectAuthFormChoice + (*OpenConnectAuthFormSubmission)(nil), // 72: daemon.OpenConnectAuthFormSubmission + (*OpenConnectAuthFormCancel)(nil), // 73: daemon.OpenConnectAuthFormCancel + (*OpenVPNStatusUpdate)(nil), // 74: daemon.OpenVPNStatusUpdate + (*OpenVPNEndpointStatus)(nil), // 75: daemon.OpenVPNEndpointStatus + (*OpenVPNTunnelInfo)(nil), // 76: daemon.OpenVPNTunnelInfo + (*OpenVPNChallenge)(nil), // 77: daemon.OpenVPNChallenge + (*OpenVPNChallengeSubmission)(nil), // 78: daemon.OpenVPNChallengeSubmission + (*OpenVPNChallengeCancel)(nil), // 79: daemon.OpenVPNChallengeCancel + (*Log_Message)(nil), // 80: daemon.Log.Message + nil, // 81: daemon.OpenConnectAuthFormSubmission.ValuesEntry + (*emptypb.Empty)(nil), // 82: google.protobuf.Empty } ) - var file_daemon_started_service_proto_depIdxs = []int32{ 4, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type - 66, // 1: daemon.Log.messages:type_name -> daemon.Log.Message + 80, // 1: daemon.Log.messages:type_name -> daemon.Log.Message 0, // 2: daemon.DefaultLogLevel.level:type_name -> daemon.LogLevel 12, // 3: daemon.Groups.group:type_name -> daemon.Group 13, // 4: daemon.Group.items:type_name -> daemon.GroupItem @@ -5213,68 +6325,89 @@ var file_daemon_started_service_proto_depIdxs = []int32{ 53, // 37: daemon.USBSharedDevice.descriptor:type_name -> daemon.USBDeviceDescriptor 3, // 38: daemon.USBSharedDevice.backend:type_name -> daemon.USBBackend 2, // 39: daemon.USBSharedDevice.state:type_name -> daemon.USBDeviceState - 0, // 40: daemon.Log.Message.level:type_name -> daemon.LogLevel - 67, // 41: daemon.StartedService.GetVersion:input_type -> google.protobuf.Empty - 67, // 42: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty - 67, // 43: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty - 67, // 44: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty - 67, // 45: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty - 7, // 46: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest - 67, // 47: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty - 67, // 48: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty - 67, // 49: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty - 17, // 50: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode - 14, // 51: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest - 15, // 52: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest - 16, // 53: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest - 19, // 54: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest - 24, // 55: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest - 67, // 56: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty - 67, // 57: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty - 67, // 58: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty - 67, // 59: daemon.StartedService.SubscribeOutbounds:input_type -> google.protobuf.Empty - 29, // 60: daemon.StartedService.StartNetworkQualityTest:input_type -> daemon.NetworkQualityTestRequest - 31, // 61: daemon.StartedService.StartSTUNTest:input_type -> daemon.STUNTestRequest - 67, // 62: daemon.StartedService.SubscribeTailscaleStatus:input_type -> google.protobuf.Empty - 37, // 63: daemon.StartedService.StartTailscalePing:input_type -> daemon.TailscalePingRequest - 39, // 64: daemon.StartedService.SetTailscaleExitNode:input_type -> daemon.SetTailscaleExitNodeRequest - 40, // 65: daemon.StartedService.TailscaleLogout:input_type -> daemon.TailscaleLogoutRequest - 41, // 66: daemon.StartedService.StartTailscaleSSHSession:input_type -> daemon.TailscaleSSHClientMessage - 51, // 67: daemon.StartedService.ProvideUSBDevices:input_type -> daemon.USBProviderMessage - 67, // 68: daemon.StartedService.SubscribeUSBIPServerStatus:input_type -> google.protobuf.Empty - 5, // 69: daemon.StartedService.GetVersion:output_type -> daemon.Version - 6, // 70: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus - 8, // 71: daemon.StartedService.SubscribeLog:output_type -> daemon.Log - 9, // 72: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel - 67, // 73: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty - 10, // 74: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status - 11, // 75: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups - 18, // 76: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus - 17, // 77: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode - 67, // 78: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty - 67, // 79: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty - 67, // 80: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty - 67, // 81: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty - 21, // 82: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents - 67, // 83: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty - 67, // 84: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty - 25, // 85: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings - 27, // 86: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt - 28, // 87: daemon.StartedService.SubscribeOutbounds:output_type -> daemon.OutboundList - 30, // 88: daemon.StartedService.StartNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress - 32, // 89: daemon.StartedService.StartSTUNTest:output_type -> daemon.STUNTestProgress - 33, // 90: daemon.StartedService.SubscribeTailscaleStatus:output_type -> daemon.TailscaleStatusUpdate - 38, // 91: daemon.StartedService.StartTailscalePing:output_type -> daemon.TailscalePingResponse - 67, // 92: daemon.StartedService.SetTailscaleExitNode:output_type -> google.protobuf.Empty - 67, // 93: daemon.StartedService.TailscaleLogout:output_type -> google.protobuf.Empty - 45, // 94: daemon.StartedService.StartTailscaleSSHSession:output_type -> daemon.TailscaleSSHServerMessage - 52, // 95: daemon.StartedService.ProvideUSBDevices:output_type -> daemon.USBServerMessage - 63, // 96: daemon.StartedService.SubscribeUSBIPServerStatus:output_type -> daemon.USBIPServerStatusUpdate - 69, // [69:97] is the sub-list for method output_type - 41, // [41:69] is the sub-list for method input_type - 41, // [41:41] is the sub-list for extension type_name - 41, // [41:41] is the sub-list for extension extendee - 0, // [0:41] is the sub-list for field type_name + 67, // 40: daemon.OpenConnectStatusUpdate.endpoints:type_name -> daemon.OpenConnectEndpointStatus + 69, // 41: daemon.OpenConnectEndpointStatus.authForm:type_name -> daemon.OpenConnectAuthForm + 68, // 42: daemon.OpenConnectEndpointStatus.tunnelInfo:type_name -> daemon.OpenConnectTunnelInfo + 70, // 43: daemon.OpenConnectAuthForm.fields:type_name -> daemon.OpenConnectAuthFormField + 71, // 44: daemon.OpenConnectAuthFormField.options:type_name -> daemon.OpenConnectAuthFormChoice + 81, // 45: daemon.OpenConnectAuthFormSubmission.values:type_name -> daemon.OpenConnectAuthFormSubmission.ValuesEntry + 75, // 46: daemon.OpenVPNStatusUpdate.endpoints:type_name -> daemon.OpenVPNEndpointStatus + 77, // 47: daemon.OpenVPNEndpointStatus.challenge:type_name -> daemon.OpenVPNChallenge + 76, // 48: daemon.OpenVPNEndpointStatus.tunnelInfo:type_name -> daemon.OpenVPNTunnelInfo + 0, // 49: daemon.Log.Message.level:type_name -> daemon.LogLevel + 82, // 50: daemon.StartedService.GetVersion:input_type -> google.protobuf.Empty + 82, // 51: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty + 82, // 52: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty + 82, // 53: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty + 82, // 54: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty + 7, // 55: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest + 82, // 56: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty + 82, // 57: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty + 82, // 58: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty + 17, // 59: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode + 14, // 60: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest + 15, // 61: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest + 16, // 62: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest + 19, // 63: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest + 24, // 64: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest + 82, // 65: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty + 82, // 66: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty + 82, // 67: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty + 82, // 68: daemon.StartedService.SubscribeOutbounds:input_type -> google.protobuf.Empty + 29, // 69: daemon.StartedService.StartNetworkQualityTest:input_type -> daemon.NetworkQualityTestRequest + 31, // 70: daemon.StartedService.StartSTUNTest:input_type -> daemon.STUNTestRequest + 82, // 71: daemon.StartedService.SubscribeTailscaleStatus:input_type -> google.protobuf.Empty + 37, // 72: daemon.StartedService.StartTailscalePing:input_type -> daemon.TailscalePingRequest + 39, // 73: daemon.StartedService.SetTailscaleExitNode:input_type -> daemon.SetTailscaleExitNodeRequest + 40, // 74: daemon.StartedService.TailscaleLogout:input_type -> daemon.TailscaleLogoutRequest + 41, // 75: daemon.StartedService.StartTailscaleSSHSession:input_type -> daemon.TailscaleSSHClientMessage + 51, // 76: daemon.StartedService.ProvideUSBDevices:input_type -> daemon.USBProviderMessage + 82, // 77: daemon.StartedService.SubscribeUSBIPServerStatus:input_type -> google.protobuf.Empty + 82, // 78: daemon.StartedService.SubscribeOpenConnectStatus:input_type -> google.protobuf.Empty + 72, // 79: daemon.StartedService.SubmitOpenConnectAuthForm:input_type -> daemon.OpenConnectAuthFormSubmission + 73, // 80: daemon.StartedService.CancelOpenConnectAuthForm:input_type -> daemon.OpenConnectAuthFormCancel + 82, // 81: daemon.StartedService.SubscribeOpenVPNStatus:input_type -> google.protobuf.Empty + 78, // 82: daemon.StartedService.SubmitOpenVPNChallengeResponse:input_type -> daemon.OpenVPNChallengeSubmission + 79, // 83: daemon.StartedService.CancelOpenVPNChallenge:input_type -> daemon.OpenVPNChallengeCancel + 5, // 84: daemon.StartedService.GetVersion:output_type -> daemon.Version + 6, // 85: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus + 8, // 86: daemon.StartedService.SubscribeLog:output_type -> daemon.Log + 9, // 87: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel + 82, // 88: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty + 10, // 89: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status + 11, // 90: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups + 18, // 91: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus + 17, // 92: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode + 82, // 93: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty + 82, // 94: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty + 82, // 95: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty + 82, // 96: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty + 21, // 97: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents + 82, // 98: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty + 82, // 99: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty + 25, // 100: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings + 27, // 101: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt + 28, // 102: daemon.StartedService.SubscribeOutbounds:output_type -> daemon.OutboundList + 30, // 103: daemon.StartedService.StartNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress + 32, // 104: daemon.StartedService.StartSTUNTest:output_type -> daemon.STUNTestProgress + 33, // 105: daemon.StartedService.SubscribeTailscaleStatus:output_type -> daemon.TailscaleStatusUpdate + 38, // 106: daemon.StartedService.StartTailscalePing:output_type -> daemon.TailscalePingResponse + 82, // 107: daemon.StartedService.SetTailscaleExitNode:output_type -> google.protobuf.Empty + 82, // 108: daemon.StartedService.TailscaleLogout:output_type -> google.protobuf.Empty + 45, // 109: daemon.StartedService.StartTailscaleSSHSession:output_type -> daemon.TailscaleSSHServerMessage + 52, // 110: daemon.StartedService.ProvideUSBDevices:output_type -> daemon.USBServerMessage + 63, // 111: daemon.StartedService.SubscribeUSBIPServerStatus:output_type -> daemon.USBIPServerStatusUpdate + 66, // 112: daemon.StartedService.SubscribeOpenConnectStatus:output_type -> daemon.OpenConnectStatusUpdate + 82, // 113: daemon.StartedService.SubmitOpenConnectAuthForm:output_type -> google.protobuf.Empty + 82, // 114: daemon.StartedService.CancelOpenConnectAuthForm:output_type -> google.protobuf.Empty + 74, // 115: daemon.StartedService.SubscribeOpenVPNStatus:output_type -> daemon.OpenVPNStatusUpdate + 82, // 116: daemon.StartedService.SubmitOpenVPNChallengeResponse:output_type -> google.protobuf.Empty + 82, // 117: daemon.StartedService.CancelOpenVPNChallenge:output_type -> google.protobuf.Empty + 84, // [84:118] is the sub-list for method output_type + 50, // [50:84] is the sub-list for method input_type + 50, // [50:50] is the sub-list for extension type_name + 50, // [50:50] is the sub-list for extension extendee + 0, // [0:50] is the sub-list for field type_name } func init() { file_daemon_started_service_proto_init() } @@ -5311,7 +6444,7 @@ func file_daemon_started_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_started_service_proto_rawDesc), len(file_daemon_started_service_proto_rawDesc)), NumEnums: 5, - NumMessages: 62, + NumMessages: 77, NumExtensions: 0, NumServices: 1, }, diff --git a/daemon/started_service.proto b/daemon/started_service.proto index 97af9f8c4..94da556a9 100644 --- a/daemon/started_service.proto +++ b/daemon/started_service.proto @@ -38,6 +38,12 @@ service StartedService { rpc StartTailscaleSSHSession(stream TailscaleSSHClientMessage) returns (stream TailscaleSSHServerMessage) {} rpc ProvideUSBDevices(stream USBProviderMessage) returns (stream USBServerMessage) {} rpc SubscribeUSBIPServerStatus(google.protobuf.Empty) returns (stream USBIPServerStatusUpdate) {} + rpc SubscribeOpenConnectStatus(google.protobuf.Empty) returns (stream OpenConnectStatusUpdate) {} + rpc SubmitOpenConnectAuthForm(OpenConnectAuthFormSubmission) returns (google.protobuf.Empty) {} + rpc CancelOpenConnectAuthForm(OpenConnectAuthFormCancel) returns (google.protobuf.Empty) {} + rpc SubscribeOpenVPNStatus(google.protobuf.Empty) returns (stream OpenVPNStatusUpdate) {} + rpc SubmitOpenVPNChallengeResponse(OpenVPNChallengeSubmission) returns (google.protobuf.Empty) {} + rpc CancelOpenVPNChallenge(OpenVPNChallengeCancel) returns (google.protobuf.Empty) {} } message Version { @@ -516,3 +522,109 @@ enum USBBackend { USB_BACKEND_DARWIN_IOKIT = 3; USB_BACKEND_WINDOWS_VBOXUSB = 4; } + +message OpenConnectStatusUpdate { + repeated OpenConnectEndpointStatus endpoints = 1; +} + +message OpenConnectEndpointStatus { + string endpointTag = 1; + string state = 2; + OpenConnectAuthForm authForm = 3; + string error = 4; + OpenConnectTunnelInfo tunnelInfo = 5; +} + +message OpenConnectTunnelInfo { + string server = 1; + string flavor = 2; + string transport = 3; + repeated string ipv4 = 4; + repeated string ipv6 = 5; + repeated string dns = 6; + uint32 mtu = 7; + int64 connectedSince = 8; +} + +message OpenConnectAuthForm { + string id = 1; + string banner = 2; + string message = 3; + string error = 4; + string url = 5; + repeated OpenConnectAuthFormField fields = 6; +} + +message OpenConnectAuthFormField { + string submissionKey = 1; + string name = 2; + string label = 3; + string kind = 4; + string value = 5; + repeated OpenConnectAuthFormChoice options = 6; +} + +message OpenConnectAuthFormChoice { + string value = 1; + string label = 2; +} + +message OpenConnectAuthFormSubmission { + string endpointTag = 1; + string formID = 2; + map values = 3; +} + +message OpenConnectAuthFormCancel { + string endpointTag = 1; + string formID = 2; +} + +message OpenVPNStatusUpdate { + repeated OpenVPNEndpointStatus endpoints = 1; +} + +message OpenVPNEndpointStatus { + string endpointTag = 1; + string state = 2; + OpenVPNChallenge challenge = 3; + string error = 4; + OpenVPNTunnelInfo tunnelInfo = 5; +} + +message OpenVPNTunnelInfo { + string server = 1; + reserved 2; + string network = 3; + repeated string ipv4 = 4; + repeated string ipv6 = 5; + repeated string dns = 6; + uint32 mtu = 7; + int64 connectedSince = 8; + string cipher = 9; +} + +message OpenVPNChallenge { + string id = 1; + string kind = 2; + string username = 3; + string message = 4; + string url = 5; + string secretMessage = 6; + bool echo = 7; + string previousError = 8; + int64 deadline = 9; +} + +message OpenVPNChallengeSubmission { + string endpointTag = 1; + string challengeID = 2; + string username = 3; + string password = 4; + string secret = 5; +} + +message OpenVPNChallengeCancel { + string endpointTag = 1; + string challengeID = 2; +} diff --git a/daemon/started_service_grpc.pb.go b/daemon/started_service_grpc.pb.go index a707fdb38..bbd3ac04f 100644 --- a/daemon/started_service_grpc.pb.go +++ b/daemon/started_service_grpc.pb.go @@ -15,34 +15,40 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - StartedService_GetVersion_FullMethodName = "/daemon.StartedService/GetVersion" - StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus" - StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog" - StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel" - StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs" - StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus" - StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups" - StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus" - StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode" - StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode" - StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest" - StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound" - StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand" - StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections" - StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection" - StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections" - StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings" - StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt" - StartedService_SubscribeOutbounds_FullMethodName = "/daemon.StartedService/SubscribeOutbounds" - StartedService_StartNetworkQualityTest_FullMethodName = "/daemon.StartedService/StartNetworkQualityTest" - StartedService_StartSTUNTest_FullMethodName = "/daemon.StartedService/StartSTUNTest" - StartedService_SubscribeTailscaleStatus_FullMethodName = "/daemon.StartedService/SubscribeTailscaleStatus" - StartedService_StartTailscalePing_FullMethodName = "/daemon.StartedService/StartTailscalePing" - StartedService_SetTailscaleExitNode_FullMethodName = "/daemon.StartedService/SetTailscaleExitNode" - StartedService_TailscaleLogout_FullMethodName = "/daemon.StartedService/TailscaleLogout" - StartedService_StartTailscaleSSHSession_FullMethodName = "/daemon.StartedService/StartTailscaleSSHSession" - StartedService_ProvideUSBDevices_FullMethodName = "/daemon.StartedService/ProvideUSBDevices" - StartedService_SubscribeUSBIPServerStatus_FullMethodName = "/daemon.StartedService/SubscribeUSBIPServerStatus" + StartedService_GetVersion_FullMethodName = "/daemon.StartedService/GetVersion" + StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus" + StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog" + StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel" + StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs" + StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus" + StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups" + StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus" + StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode" + StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode" + StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest" + StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound" + StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand" + StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections" + StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection" + StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections" + StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings" + StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt" + StartedService_SubscribeOutbounds_FullMethodName = "/daemon.StartedService/SubscribeOutbounds" + StartedService_StartNetworkQualityTest_FullMethodName = "/daemon.StartedService/StartNetworkQualityTest" + StartedService_StartSTUNTest_FullMethodName = "/daemon.StartedService/StartSTUNTest" + StartedService_SubscribeTailscaleStatus_FullMethodName = "/daemon.StartedService/SubscribeTailscaleStatus" + StartedService_StartTailscalePing_FullMethodName = "/daemon.StartedService/StartTailscalePing" + StartedService_SetTailscaleExitNode_FullMethodName = "/daemon.StartedService/SetTailscaleExitNode" + StartedService_TailscaleLogout_FullMethodName = "/daemon.StartedService/TailscaleLogout" + StartedService_StartTailscaleSSHSession_FullMethodName = "/daemon.StartedService/StartTailscaleSSHSession" + StartedService_ProvideUSBDevices_FullMethodName = "/daemon.StartedService/ProvideUSBDevices" + StartedService_SubscribeUSBIPServerStatus_FullMethodName = "/daemon.StartedService/SubscribeUSBIPServerStatus" + StartedService_SubscribeOpenConnectStatus_FullMethodName = "/daemon.StartedService/SubscribeOpenConnectStatus" + StartedService_SubmitOpenConnectAuthForm_FullMethodName = "/daemon.StartedService/SubmitOpenConnectAuthForm" + StartedService_CancelOpenConnectAuthForm_FullMethodName = "/daemon.StartedService/CancelOpenConnectAuthForm" + StartedService_SubscribeOpenVPNStatus_FullMethodName = "/daemon.StartedService/SubscribeOpenVPNStatus" + StartedService_SubmitOpenVPNChallengeResponse_FullMethodName = "/daemon.StartedService/SubmitOpenVPNChallengeResponse" + StartedService_CancelOpenVPNChallenge_FullMethodName = "/daemon.StartedService/CancelOpenVPNChallenge" ) // StartedServiceClient is the client API for StartedService service. @@ -77,6 +83,12 @@ type StartedServiceClient interface { StartTailscaleSSHSession(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[TailscaleSSHClientMessage, TailscaleSSHServerMessage], error) ProvideUSBDevices(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[USBProviderMessage, USBServerMessage], error) SubscribeUSBIPServerStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[USBIPServerStatusUpdate], error) + SubscribeOpenConnectStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OpenConnectStatusUpdate], error) + SubmitOpenConnectAuthForm(ctx context.Context, in *OpenConnectAuthFormSubmission, opts ...grpc.CallOption) (*emptypb.Empty, error) + CancelOpenConnectAuthForm(ctx context.Context, in *OpenConnectAuthFormCancel, opts ...grpc.CallOption) (*emptypb.Empty, error) + SubscribeOpenVPNStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OpenVPNStatusUpdate], error) + SubmitOpenVPNChallengeResponse(ctx context.Context, in *OpenVPNChallengeSubmission, opts ...grpc.CallOption) (*emptypb.Empty, error) + CancelOpenVPNChallenge(ctx context.Context, in *OpenVPNChallengeCancel, opts ...grpc.CallOption) (*emptypb.Empty, error) } type startedServiceClient struct { @@ -481,6 +493,84 @@ func (c *startedServiceClient) SubscribeUSBIPServerStatus(ctx context.Context, i // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type StartedService_SubscribeUSBIPServerStatusClient = grpc.ServerStreamingClient[USBIPServerStatusUpdate] +func (c *startedServiceClient) SubscribeOpenConnectStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OpenConnectStatusUpdate], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[14], StartedService_SubscribeOpenConnectStatus_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[emptypb.Empty, OpenConnectStatusUpdate]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeOpenConnectStatusClient = grpc.ServerStreamingClient[OpenConnectStatusUpdate] + +func (c *startedServiceClient) SubmitOpenConnectAuthForm(ctx context.Context, in *OpenConnectAuthFormSubmission, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, StartedService_SubmitOpenConnectAuthForm_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *startedServiceClient) CancelOpenConnectAuthForm(ctx context.Context, in *OpenConnectAuthFormCancel, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, StartedService_CancelOpenConnectAuthForm_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *startedServiceClient) SubscribeOpenVPNStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OpenVPNStatusUpdate], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[15], StartedService_SubscribeOpenVPNStatus_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[emptypb.Empty, OpenVPNStatusUpdate]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeOpenVPNStatusClient = grpc.ServerStreamingClient[OpenVPNStatusUpdate] + +func (c *startedServiceClient) SubmitOpenVPNChallengeResponse(ctx context.Context, in *OpenVPNChallengeSubmission, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, StartedService_SubmitOpenVPNChallengeResponse_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *startedServiceClient) CancelOpenVPNChallenge(ctx context.Context, in *OpenVPNChallengeCancel, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, StartedService_CancelOpenVPNChallenge_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // StartedServiceServer is the server API for StartedService service. // All implementations must embed UnimplementedStartedServiceServer // for forward compatibility. @@ -513,6 +603,12 @@ type StartedServiceServer interface { StartTailscaleSSHSession(grpc.BidiStreamingServer[TailscaleSSHClientMessage, TailscaleSSHServerMessage]) error ProvideUSBDevices(grpc.BidiStreamingServer[USBProviderMessage, USBServerMessage]) error SubscribeUSBIPServerStatus(*emptypb.Empty, grpc.ServerStreamingServer[USBIPServerStatusUpdate]) error + SubscribeOpenConnectStatus(*emptypb.Empty, grpc.ServerStreamingServer[OpenConnectStatusUpdate]) error + SubmitOpenConnectAuthForm(context.Context, *OpenConnectAuthFormSubmission) (*emptypb.Empty, error) + CancelOpenConnectAuthForm(context.Context, *OpenConnectAuthFormCancel) (*emptypb.Empty, error) + SubscribeOpenVPNStatus(*emptypb.Empty, grpc.ServerStreamingServer[OpenVPNStatusUpdate]) error + SubmitOpenVPNChallengeResponse(context.Context, *OpenVPNChallengeSubmission) (*emptypb.Empty, error) + CancelOpenVPNChallenge(context.Context, *OpenVPNChallengeCancel) (*emptypb.Empty, error) mustEmbedUnimplementedStartedServiceServer() } @@ -634,6 +730,30 @@ func (UnimplementedStartedServiceServer) ProvideUSBDevices(grpc.BidiStreamingSer func (UnimplementedStartedServiceServer) SubscribeUSBIPServerStatus(*emptypb.Empty, grpc.ServerStreamingServer[USBIPServerStatusUpdate]) error { return status.Error(codes.Unimplemented, "method SubscribeUSBIPServerStatus not implemented") } + +func (UnimplementedStartedServiceServer) SubscribeOpenConnectStatus(*emptypb.Empty, grpc.ServerStreamingServer[OpenConnectStatusUpdate]) error { + return status.Error(codes.Unimplemented, "method SubscribeOpenConnectStatus not implemented") +} + +func (UnimplementedStartedServiceServer) SubmitOpenConnectAuthForm(context.Context, *OpenConnectAuthFormSubmission) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method SubmitOpenConnectAuthForm not implemented") +} + +func (UnimplementedStartedServiceServer) CancelOpenConnectAuthForm(context.Context, *OpenConnectAuthFormCancel) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method CancelOpenConnectAuthForm not implemented") +} + +func (UnimplementedStartedServiceServer) SubscribeOpenVPNStatus(*emptypb.Empty, grpc.ServerStreamingServer[OpenVPNStatusUpdate]) error { + return status.Error(codes.Unimplemented, "method SubscribeOpenVPNStatus not implemented") +} + +func (UnimplementedStartedServiceServer) SubmitOpenVPNChallengeResponse(context.Context, *OpenVPNChallengeSubmission) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method SubmitOpenVPNChallengeResponse not implemented") +} + +func (UnimplementedStartedServiceServer) CancelOpenVPNChallenge(context.Context, *OpenVPNChallengeCancel) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method CancelOpenVPNChallenge not implemented") +} func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {} func (UnimplementedStartedServiceServer) testEmbeddedByValue() {} @@ -1053,6 +1173,100 @@ func _StartedService_SubscribeUSBIPServerStatus_Handler(srv interface{}, stream // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type StartedService_SubscribeUSBIPServerStatusServer = grpc.ServerStreamingServer[USBIPServerStatusUpdate] +func _StartedService_SubscribeOpenConnectStatus_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(emptypb.Empty) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StartedServiceServer).SubscribeOpenConnectStatus(m, &grpc.GenericServerStream[emptypb.Empty, OpenConnectStatusUpdate]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeOpenConnectStatusServer = grpc.ServerStreamingServer[OpenConnectStatusUpdate] + +func _StartedService_SubmitOpenConnectAuthForm_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(OpenConnectAuthFormSubmission) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).SubmitOpenConnectAuthForm(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_SubmitOpenConnectAuthForm_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).SubmitOpenConnectAuthForm(ctx, req.(*OpenConnectAuthFormSubmission)) + } + return interceptor(ctx, in, info, handler) +} + +func _StartedService_CancelOpenConnectAuthForm_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(OpenConnectAuthFormCancel) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).CancelOpenConnectAuthForm(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_CancelOpenConnectAuthForm_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).CancelOpenConnectAuthForm(ctx, req.(*OpenConnectAuthFormCancel)) + } + return interceptor(ctx, in, info, handler) +} + +func _StartedService_SubscribeOpenVPNStatus_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(emptypb.Empty) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StartedServiceServer).SubscribeOpenVPNStatus(m, &grpc.GenericServerStream[emptypb.Empty, OpenVPNStatusUpdate]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeOpenVPNStatusServer = grpc.ServerStreamingServer[OpenVPNStatusUpdate] + +func _StartedService_SubmitOpenVPNChallengeResponse_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(OpenVPNChallengeSubmission) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).SubmitOpenVPNChallengeResponse(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_SubmitOpenVPNChallengeResponse_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).SubmitOpenVPNChallengeResponse(ctx, req.(*OpenVPNChallengeSubmission)) + } + return interceptor(ctx, in, info, handler) +} + +func _StartedService_CancelOpenVPNChallenge_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(OpenVPNChallengeCancel) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).CancelOpenVPNChallenge(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_CancelOpenVPNChallenge_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).CancelOpenVPNChallenge(ctx, req.(*OpenVPNChallengeCancel)) + } + return interceptor(ctx, in, info, handler) +} + // StartedService_ServiceDesc is the grpc.ServiceDesc for StartedService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -1116,6 +1330,22 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{ MethodName: "TailscaleLogout", Handler: _StartedService_TailscaleLogout_Handler, }, + { + MethodName: "SubmitOpenConnectAuthForm", + Handler: _StartedService_SubmitOpenConnectAuthForm_Handler, + }, + { + MethodName: "CancelOpenConnectAuthForm", + Handler: _StartedService_CancelOpenConnectAuthForm_Handler, + }, + { + MethodName: "SubmitOpenVPNChallengeResponse", + Handler: _StartedService_SubmitOpenVPNChallengeResponse_Handler, + }, + { + MethodName: "CancelOpenVPNChallenge", + Handler: _StartedService_CancelOpenVPNChallenge_Handler, + }, }, Streams: []grpc.StreamDesc{ { @@ -1190,6 +1420,16 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{ Handler: _StartedService_SubscribeUSBIPServerStatus_Handler, ServerStreams: true, }, + { + StreamName: "SubscribeOpenConnectStatus", + Handler: _StartedService_SubscribeOpenConnectStatus_Handler, + ServerStreams: true, + }, + { + StreamName: "SubscribeOpenVPNStatus", + Handler: _StartedService_SubscribeOpenVPNStatus_Handler, + ServerStreams: true, + }, }, Metadata: "daemon/started_service.proto", } diff --git a/docs/configuration/endpoint/index.md b/docs/configuration/endpoint/index.md index b409a7839..6e7614b05 100644 --- a/docs/configuration/endpoint/index.md +++ b/docs/configuration/endpoint/index.md @@ -19,10 +19,13 @@ An endpoint is a protocol with inbound and outbound behavior. ### Fields -| Type | Format | -|-------------|---------------------------| -| `wireguard` | [WireGuard](./wireguard/) | -| `tailscale` | [Tailscale](./tailscale/) | +| Type | Format | +|------------------|-----------------------------------------| +| `wireguard` | [WireGuard](./wireguard/) | +| `tailscale` | [Tailscale](./tailscale/) | +| `openconnect` | [OpenConnect Client](./openconnect/) | +| `openvpn-client` | [OpenVPN Client](./openvpn-client/) | +| `openvpn-server` | [OpenVPN Server](./openvpn-server/) | #### tag diff --git a/docs/configuration/endpoint/index.zh.md b/docs/configuration/endpoint/index.zh.md index f7e71b757..a75bc6316 100644 --- a/docs/configuration/endpoint/index.zh.md +++ b/docs/configuration/endpoint/index.zh.md @@ -19,10 +19,13 @@ ### 字段 -| 类型 | 格式 | -|-------------|---------------------------| -| `wireguard` | [WireGuard](./wireguard/) | -| `tailscale` | [Tailscale](./tailscale/) | +| 类型 | 格式 | +|------------------|-----------------------------------------| +| `wireguard` | [WireGuard](./wireguard/) | +| `tailscale` | [Tailscale](./tailscale/) | +| `openconnect` | [OpenConnect 客户端](./openconnect/) | +| `openvpn-client` | [OpenVPN 客户端](./openvpn-client/) | +| `openvpn-server` | [OpenVPN 服务器](./openvpn-server/) | #### tag diff --git a/docs/configuration/endpoint/openconnect.md b/docs/configuration/endpoint/openconnect.md new file mode 100644 index 000000000..c8f7f3e12 --- /dev/null +++ b/docs/configuration/endpoint/openconnect.md @@ -0,0 +1,387 @@ +# OpenConnect Client + +!!! question "Since sing-box 1.14.0" + +==Client only== + +## Structure + +```json +{ + "type": "openconnect", + "tag": "oc-client", + + "system": false, + "name": "", + "server": "vpn.example.com", + "flavor": "anyconnect", + "username": "", + "password": "", + "auth_group": "", + "token": { + "mode": "", + "secret": "", + "pin": "", + "password": "", + "device_id": "", + "counter": 0 + }, + "reported_os": "", + "user_agent": "", + "csd": { + "wrapper_path": "" + }, + "hip": { + "wrapper_path": "" + }, + "tncc": { + "wrapper_path": "", + "device_id": "", + "user_agent": "", + "machine_identification_enabled": false, + "certificates": [ + { + "certificate": [], + "certificate_path": "" + } + ] + }, + "no_udp": false, + "allow_insecure_crypto": false, + "tls": { + "certificate_authority": [], + "certificate_authority_path": "", + "client_certificate": [], + "client_certificate_path": "", + "client_key": [], + "client_key_path": "", + "client_key_password": "", + "mca_certificate": [], + "mca_certificate_path": "", + "mca_key": [], + "mca_key_path": "", + "mca_key_password": "" + }, + "form_entries": [ + { + "form_id": "", + "submission_key": "", + "name": "", + "value": "", + "promote": false + } + ], + + ... // Dial Fields +} +``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item. + +## Fields + +### system + +Use a system interface. + +Requires privilege and cannot conflict with existing system interfaces. + +If disabled, sing-box uses the internal network stack. + +### name + +Custom interface name for the system interface. + +An automatically generated `oc` interface name is used by default. + +### server + +==Required== + +OpenConnect VPN server HTTPS URL. + +The `https://` scheme is added if omitted. URL user information, queries, and fragments are not supported. + +### flavor + +OpenConnect protocol flavor, one of `anyconnect`, `gp`, `fortinet`, `f5`, `pulse`, or `nc`. + +`anyconnect` is used by default. + +### username + +Username used to fill matching authentication form fields. + +### password + +Password used to fill matching authentication form fields. + +### auth_group + +Authentication group used to preselect a matching group, realm, domain, or gateway choice when supported by the selected flavor. + +### token + +Software token configuration for automatically answering matching token fields. + +### token.mode + +==Required== + +Software token mode, one of: + +- `totp`: Time-based One-Time Password. +- `hotp`: HMAC-based One-Time Password. +- `stoken`: RSA SecurID software token. + +### token.secret + +==Required== + +Software token secret. + +For `totp` and `hotp`, this can be a Base32 secret, a `base32:`-prefixed secret, or an `otpauth://` URI of the matching type. + +For `stoken`, this is the encoded RSA SecurID CTF token content. + +### token.pin + +RSA SecurID PIN for `stoken` mode. + +### token.password + +Password for decrypting a password-protected RSA SecurID token in `stoken` mode. + +### token.device_id + +Device ID for decrypting a device-bound RSA SecurID token in `stoken` mode. + +### token.counter + +Initial counter for `hotp` mode. + +If zero, the counter from an `otpauth://` URI is used when present; otherwise the counter starts at zero. + +### reported_os + +Operating system identity reported to the VPN server when supported by the selected flavor. + +For `anyconnect`, `gp`, and `pulse`, the supported values are `linux`, `linux-64`, `win`, `mac-intel`, `android`, and `apple-ios`. + +`anyconnect` uses `linux-64` by default. `gp` and `pulse` select a value based on the system platform by default. + +### user_agent + +User agent reported to the VPN server when supported by the selected flavor. + +The default is flavor-specific. + +### csd + +AnyConnect CSD/host scan compliance options. + +Built-in CSD handling is used by default when requested by the server. + +### csd.wrapper_path + +Path to an external AnyConnect CSD wrapper executable. + +Built-in CSD handling is used if empty. + +### hip + +GlobalProtect HIP check and report options. + +Built-in HIP reporting is used by default when requested by the server. + +### hip.wrapper_path + +Path to an external GlobalProtect HIP report wrapper executable. + +Built-in HIP reporting is used if empty. + +### tncc + +Network Connect TNCC compliance options. + +Built-in TNCC handling is used by default when requested by the server. + +### tncc.wrapper_path + +Path to an external Network Connect TNCC wrapper executable. + +Built-in TNCC handling is used if empty. + +Conflict with `tncc.device_id`, `tncc.user_agent`, `tncc.machine_identification_enabled`, and `tncc.certificates`. + +### tncc.device_id + +Device ID reported by the built-in TNCC handler. + +Conflict with `tncc.wrapper_path`. + +### tncc.user_agent + +User agent used by the built-in TNCC handler. + +`Neoteris HC Http` is used by default. + +Conflict with `tncc.wrapper_path`. + +### tncc.machine_identification_enabled + +Enable built-in TNCC machine identification, including the platform, hostname, and observed MAC addresses. + +Conflict with `tncc.wrapper_path`. + +### tncc.certificates + +Machine certificates used by the built-in TNCC handler to answer certificate requests. + +Requires `tncc.machine_identification_enabled`. + +Conflict with `tncc.wrapper_path`. + +### tncc.certificates.certificate + +TNCC machine certificate content in PEM format. + +Conflict with `tncc.certificates.certificate_path`. + +### tncc.certificates.certificate_path + +TNCC machine certificate path in PEM format. + +Conflict with `tncc.certificates.certificate`. + +### no_udp + +Disable the DTLS or ESP secondary data channel and use the TLS data channel only. + +### allow_insecure_crypto + +Allow deprecated TLS and DTLS versions and cipher suites required by legacy VPN servers. + +Disabled by default. This option does not disable server certificate verification. + +### tls + +OpenConnect TLS configuration. + +### tls.certificate_authority + +Additional trusted CA certificate content in PEM format. + +The certificates are added to the system certificate pool. + +Conflict with `tls.certificate_authority_path`. + +### tls.certificate_authority_path + +Path to additional trusted CA certificates in PEM format. + +The certificates are added to the system certificate pool. + +Conflict with `tls.certificate_authority`. + +### tls.client_certificate + +Client certificate chain content in PEM format. + +Conflict with `tls.client_certificate_path`. + +### tls.client_certificate_path + +Client certificate chain path in PEM format. + +Conflict with `tls.client_certificate`. + +### tls.client_key + +Client private key content in PEM format. + +Conflict with `tls.client_key_path`. + +### tls.client_key_path + +Client private key path in PEM format. + +Conflict with `tls.client_key`. + +The client certificate and key must both be set or both be empty. + +### tls.client_key_password + +Password for the encrypted client private key. + +### tls.mca_certificate + +AnyConnect multiple-certificate authentication (MCA) certificate chain content in PEM format. + +Conflict with `tls.mca_certificate_path`. + +### tls.mca_certificate_path + +AnyConnect multiple-certificate authentication (MCA) certificate chain path in PEM format. + +Conflict with `tls.mca_certificate`. + +### tls.mca_key + +AnyConnect multiple-certificate authentication (MCA) private key content in PEM format. + +Conflict with `tls.mca_key_path`. + +### tls.mca_key_path + +AnyConnect multiple-certificate authentication (MCA) private key path in PEM format. + +Conflict with `tls.mca_key`. + +The MCA certificate and key must both be set or both be empty. + +### tls.mca_key_password + +Password for the encrypted MCA private key. + +### form_entries + +Authentication form field overrides. + +An entry matches by `submission_key` when set, or by the combination of `form_id` and `name`. Later matching entries take precedence. + +### form_entries.form_id + +Authentication form identifier used with `form_entries.name` when `form_entries.submission_key` is empty. + +### form_entries.submission_key + +Authentication field submission key. + +Either `form_entries.submission_key` or both `form_entries.form_id` and `form_entries.name` are required. + +### form_entries.name + +Authentication field name used with `form_entries.form_id` when `form_entries.submission_key` is empty. + +### form_entries.value + +Value supplied automatically for the matching authentication field. + +Conflict with `form_entries.promote`. + +### form_entries.promote + +Ask for the matching authentication field interactively instead of supplying an automatic value. + +Conflict with `form_entries.value`. + +## Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. + +## Interactive authentication + +Use `Tools` > `Endpoints` in the sing-box dashboard or any sing-box graphical client to authenticate and manage the endpoint. diff --git a/docs/configuration/endpoint/openconnect.zh.md b/docs/configuration/endpoint/openconnect.zh.md new file mode 100644 index 000000000..a515b84ac --- /dev/null +++ b/docs/configuration/endpoint/openconnect.zh.md @@ -0,0 +1,387 @@ +# OpenConnect 客户端 + +!!! question "自 sing-box 1.14.0 起" + +==仅客户端== + +## 结构 + +```json +{ + "type": "openconnect", + "tag": "oc-client", + + "system": false, + "name": "", + "server": "vpn.example.com", + "flavor": "anyconnect", + "username": "", + "password": "", + "auth_group": "", + "token": { + "mode": "", + "secret": "", + "pin": "", + "password": "", + "device_id": "", + "counter": 0 + }, + "reported_os": "", + "user_agent": "", + "csd": { + "wrapper_path": "" + }, + "hip": { + "wrapper_path": "" + }, + "tncc": { + "wrapper_path": "", + "device_id": "", + "user_agent": "", + "machine_identification_enabled": false, + "certificates": [ + { + "certificate": [], + "certificate_path": "" + } + ] + }, + "no_udp": false, + "allow_insecure_crypto": false, + "tls": { + "certificate_authority": [], + "certificate_authority_path": "", + "client_certificate": [], + "client_certificate_path": "", + "client_key": [], + "client_key_path": "", + "client_key_password": "", + "mca_certificate": [], + "mca_certificate_path": "", + "mca_key": [], + "mca_key_path": "", + "mca_key_password": "" + }, + "form_entries": [ + { + "form_id": "", + "submission_key": "", + "name": "", + "value": "", + "promote": false + } + ], + + ... // 拨号字段 +} +``` + +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签。 + +## 字段 + +### system + +使用系统接口。 + +需要权限,且不能与现有系统接口冲突。 + +禁用时,sing-box 使用内部网络栈。 + +### name + +系统接口的自定义接口名称。 + +默认使用自动生成的 `oc` 接口名称。 + +### server + +==必填== + +OpenConnect VPN 服务器 HTTPS URL。 + +省略协议时会添加 `https://`。不支持 URL 用户信息、查询和片段。 + +### flavor + +OpenConnect 协议 flavor,可选值为 `anyconnect`、`gp`、`fortinet`、`f5`、`pulse` 或 `nc`。 + +默认使用 `anyconnect`。 + +### username + +用于填充匹配认证表单字段的用户名。 + +### password + +用于填充匹配认证表单字段的密码。 + +### auth_group + +认证组,用于在所选 flavor 支持时预选匹配的组、realm、domain 或 gateway 选项。 + +### token + +用于自动回答匹配 token 字段的软件 token 配置。 + +### token.mode + +==必填== + +软件 token 模式,可选值为: + +- `totp`:基于时间的一次性密码。 +- `hotp`:基于 HMAC 的一次性密码。 +- `stoken`:RSA SecurID 软件 token。 + +### token.secret + +==必填== + +软件 token 密钥。 + +对于 `totp` 和 `hotp`,可以是 Base32 密钥、带 `base32:` 前缀的密钥或类型匹配的 `otpauth://` URI。 + +对于 `stoken`,这是编码后的 RSA SecurID CTF token 内容。 + +### token.pin + +`stoken` 模式的 RSA SecurID PIN。 + +### token.password + +`stoken` 模式下用于解密受密码保护的 RSA SecurID token 的密码。 + +### token.device_id + +`stoken` 模式下用于解密设备绑定 RSA SecurID token 的设备 ID。 + +### token.counter + +`hotp` 模式的初始计数器。 + +为零时,如果 `otpauth://` URI 中存在计数器,则使用该计数器;否则从零开始。 + +### reported_os + +所选 flavor 支持时向 VPN 服务器报告的操作系统标识。 + +对于 `anyconnect`、`gp` 和 `pulse`,支持的值为 `linux`、`linux-64`、`win`、`mac-intel`、`android` 和 `apple-ios`。 + +`anyconnect` 默认使用 `linux-64`。`gp` 和 `pulse` 默认根据系统平台选择值。 + +### user_agent + +所选 flavor 支持时向 VPN 服务器报告的 User-Agent。 + +默认值由 flavor 决定。 + +### csd + +AnyConnect CSD/host scan 合规性选项。 + +服务器请求 CSD 时,默认使用内置 CSD 处理。 + +### csd.wrapper_path + +外部 AnyConnect CSD wrapper 可执行文件的路径。 + +为空时使用内置 CSD 处理。 + +### hip + +GlobalProtect HIP 检查和报告选项。 + +服务器请求 HIP 时,默认使用内置 HIP 报告。 + +### hip.wrapper_path + +外部 GlobalProtect HIP report wrapper 可执行文件的路径。 + +为空时使用内置 HIP 报告。 + +### tncc + +Network Connect TNCC 合规性选项。 + +服务器请求 TNCC 时,默认使用内置 TNCC 处理。 + +### tncc.wrapper_path + +外部 Network Connect TNCC wrapper 可执行文件的路径。 + +为空时使用内置 TNCC 处理。 + +与 `tncc.device_id`、`tncc.user_agent`、`tncc.machine_identification_enabled` 和 `tncc.certificates` 冲突。 + +### tncc.device_id + +内置 TNCC 处理程序报告的设备 ID。 + +与 `tncc.wrapper_path` 冲突。 + +### tncc.user_agent + +内置 TNCC 处理程序使用的 User-Agent。 + +默认使用 `Neoteris HC Http`。 + +与 `tncc.wrapper_path` 冲突。 + +### tncc.machine_identification_enabled + +启用内置 TNCC 机器标识,包括平台、主机名和观测到的 MAC 地址。 + +与 `tncc.wrapper_path` 冲突。 + +### tncc.certificates + +内置 TNCC 处理程序用于回答证书请求的机器证书。 + +需要启用 `tncc.machine_identification_enabled`。 + +与 `tncc.wrapper_path` 冲突。 + +### tncc.certificates.certificate + +PEM 格式的 TNCC 机器证书内容。 + +与 `tncc.certificates.certificate_path` 冲突。 + +### tncc.certificates.certificate_path + +PEM 格式的 TNCC 机器证书路径。 + +与 `tncc.certificates.certificate` 冲突。 + +### no_udp + +禁用 DTLS 或 ESP 辅助数据通道,仅使用 TLS 数据通道。 + +### allow_insecure_crypto + +允许旧版 VPN 服务器所需的已弃用 TLS 和 DTLS 版本及密码套件。 + +默认禁用。此选项不会禁用服务器证书验证。 + +### tls + +OpenConnect TLS 配置。 + +### tls.certificate_authority + +PEM 格式的附加受信任 CA 证书内容。 + +这些证书会添加到系统证书池。 + +与 `tls.certificate_authority_path` 冲突。 + +### tls.certificate_authority_path + +PEM 格式的附加受信任 CA 证书路径。 + +这些证书会添加到系统证书池。 + +与 `tls.certificate_authority` 冲突。 + +### tls.client_certificate + +PEM 格式的客户端证书链内容。 + +与 `tls.client_certificate_path` 冲突。 + +### tls.client_certificate_path + +PEM 格式的客户端证书链路径。 + +与 `tls.client_certificate` 冲突。 + +### tls.client_key + +PEM 格式的客户端私钥内容。 + +与 `tls.client_key_path` 冲突。 + +### tls.client_key_path + +PEM 格式的客户端私钥路径。 + +与 `tls.client_key` 冲突。 + +客户端证书和私钥必须同时设置或同时为空。 + +### tls.client_key_password + +加密客户端私钥的密码。 + +### tls.mca_certificate + +PEM 格式的 AnyConnect 多证书认证(MCA)证书链内容。 + +与 `tls.mca_certificate_path` 冲突。 + +### tls.mca_certificate_path + +PEM 格式的 AnyConnect 多证书认证(MCA)证书链路径。 + +与 `tls.mca_certificate` 冲突。 + +### tls.mca_key + +PEM 格式的 AnyConnect 多证书认证(MCA)私钥内容。 + +与 `tls.mca_key_path` 冲突。 + +### tls.mca_key_path + +PEM 格式的 AnyConnect 多证书认证(MCA)私钥路径。 + +与 `tls.mca_key` 冲突。 + +MCA 证书和私钥必须同时设置或同时为空。 + +### tls.mca_key_password + +加密 MCA 私钥的密码。 + +### form_entries + +认证表单字段覆盖。 + +设置 `submission_key` 时按该字段匹配,否则按 `form_id` 和 `name` 的组合匹配。后面的匹配项优先。 + +### form_entries.form_id + +`form_entries.submission_key` 为空时,与 `form_entries.name` 一起使用的认证表单标识符。 + +### form_entries.submission_key + +认证字段提交键。 + +`form_entries.submission_key` 或 `form_entries.form_id` 与 `form_entries.name` 的组合之一必填。 + +### form_entries.name + +`form_entries.submission_key` 为空时,与 `form_entries.form_id` 一起使用的认证字段名称。 + +### form_entries.value + +自动提供给匹配认证字段的值。 + +与 `form_entries.promote` 冲突。 + +### form_entries.promote + +交互询问匹配的认证字段,而不是自动提供值。 + +与 `form_entries.value` 冲突。 + +## 拨号字段 + +参阅[拨号字段](/zh/configuration/shared/dial/)了解详情。 + +## 交互式认证 + +在 sing-box dashboard 或任意 sing-box 图形客户端的 `工具` > `端点` 中认证和管理 endpoint。 diff --git a/docs/configuration/endpoint/openvpn-client.md b/docs/configuration/endpoint/openvpn-client.md new file mode 100644 index 000000000..a8a61fe91 --- /dev/null +++ b/docs/configuration/endpoint/openvpn-client.md @@ -0,0 +1,501 @@ +# OpenVPN Client + +!!! question "Since sing-box 1.14.0" + +## Structure + +```json +{ + "type": "openvpn-client", + "tag": "ovpn-client", + + "server": "127.0.0.1", + "server_port": 1194, + "servers": [ + { + "server": "127.0.0.1", + "server_port": 1194, + "network": "udp" + } + ], + "remote_random": false, + "network": "udp", + "username": "", + "password": "", + "auth_retry": "none", + "static_challenge": "", + "static_challenge_echo": false, + "tls": { + "server_name": "", + "server_name_type": "name", + "certificate": [], + "certificate_path": "", + "client_certificate": [], + "client_certificate_path": "", + "client_key": [], + "client_key_path": "", + "peer_fingerprint": [], + "crl_path": "", + "remote_certificate_ku": [], + "remote_certificate_eku": "", + "version_min": "1.2", + "version_max": "", + "cipher": "", + "groups": "", + "control_wrap": { + "type": "", + "key": [], + "key_path": "", + "direction": "" + } + }, + "data_ciphers": [], + "data_ciphers_fallback": "", + "auth": "", + "mss_fix": 0, + "fragment": 0, + "compression": "", + "compression_lzo": "", + "allow_compression": "no", + "route_no_pull": false, + "pull_filters": [ + { + "action": "ignore", + "text": "route " + } + ], + "routes": [], + "route_gateway": "", + "route_metric": 0, + "redirect_gateway": false, + "redirect_gateway_flags": [], + "keepalive_interval": "", + "keepalive_timeout": "", + "renegotiate_interval": "", + "explicit_exit_notify": 0, + "system": false, + "name": "", + "mtu": 1500, + "udp_timeout": "", + + ... // Dial Fields +} +``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item. + +## Fields + +### server + +OpenVPN server address. + +Either `server` or `servers` is required. + +Conflict with `servers`. + +### server_port + +OpenVPN server port. + +Required when `server` is set. + +### servers + +List of OpenVPN servers. + +The client tries the servers in order and moves to the next server when a connection fails. + +Either `server` or `servers` is required. + +Conflict with `server`. + +### servers.server + +==Required== + +OpenVPN server address. + +### servers.server_port + +==Required== + +OpenVPN server port. + +### servers.network + +OpenVPN transport network for this server, one of `udp` or `tcp`. + +The top-level `network` is used by default. + +### remote_random + +Randomize the `servers` order before connecting. + +Disabled by default. + +### network + +Default OpenVPN transport network, one of `udp` or `tcp`. + +`udp` is used by default. + +This value applies to `server` and to `servers` entries without their own `network`. + +### username + +Username for OpenVPN username/password authentication. + +### password + +Password for OpenVPN username/password authentication. + +### auth_retry + +Behavior after username/password authentication fails, one of `none`, `nointeract`, or `interact`. + +`none` is used by default and treats a permanent authentication failure as terminal. + +`nointeract` and `interact` allow authentication retries. + +### static_challenge + +Static challenge text shown when requesting an authentication response. + +### static_challenge_echo + +Show the static challenge response as plain text. + +### tls + +==Required== + +OpenVPN control channel TLS configuration. + +### tls.server_name + +Expected server certificate name. + +Certificate name verification is disabled if empty. The certificate chain or fingerprint and server certificate usage are still verified. + +### tls.server_name_type + +Certificate field matched by `tls.server_name`, one of `subject`, `name`, or `name-prefix`. + +`name` is used by default when `tls.server_name` is set. + +`subject` matches the full certificate subject, `name` matches the common name exactly, and `name-prefix` matches a common name prefix. + +### tls.certificate + +Trusted CA certificate content. + +One of `tls.certificate`, `tls.certificate_path`, or `tls.peer_fingerprint` is required. + +Conflict with `tls.certificate_path`. + +### tls.certificate_path + +Trusted CA certificate path. + +One of `tls.certificate`, `tls.certificate_path`, or `tls.peer_fingerprint` is required. + +Conflict with `tls.certificate`. + +### tls.client_certificate + +Client certificate content. + +Conflict with `tls.client_certificate_path`. + +### tls.client_certificate_path + +Client certificate path. + +Conflict with `tls.client_certificate`. + +### tls.client_key + +Client private key content. + +Conflict with `tls.client_key_path`. + +### tls.client_key_path + +Client private key path. + +Conflict with `tls.client_key`. + +The client certificate and key must both be set or both be empty. + +### tls.peer_fingerprint + +Allowed SHA-256 fingerprints of the server leaf certificate. + +Each fingerprint must be 64 lowercase hexadecimal characters without separators. + +When a trusted CA is also configured, both the certificate chain and fingerprint are verified. Without a trusted CA, the fingerprint, certificate validity period, configured name, and certificate usage are verified, but the certificate chain is not. + +### tls.crl_path + +Path to a PEM or DER certificate revocation list used to reject revoked server certificates. + +The CRL signature and validity period are verified against the trusted certificate chain. + +Disabled by default. + +### tls.remote_certificate_ku + +Required server certificate key usage masks, written as hexadecimal values in OpenVPN `remote-cert-ku` format. + +Multiple values are combined, and all requested usages must be present. + +Disabled by default. + +### tls.remote_certificate_eku + +Required server certificate extended key usage, one of `server` or `client`. + +Disabled by default. The standard OpenVPN server certificate usage check still applies. + +### tls.version_min + +Minimum TLS version, one of `1.0`, `1.1`, `1.2`, or `1.3`. + +`1.2` is used by default. + +### tls.version_max + +Maximum TLS version, one of `1.0`, `1.1`, `1.2`, or `1.3`. + +The maximum supported version is used by default. + +The value cannot be lower than `tls.version_min`. + +### tls.cipher + +Colon-separated OpenSSL cipher suite names allowed for TLS 1.2 and earlier. + +The default TLS cipher suites are used when empty. TLS 1.3 cipher suites are not controlled by this field. + +### tls.groups + +Colon-separated TLS key exchange groups in preference order. + +Supported groups are `X25519`, `SECP256R1`, `SECP384R1`, and `SECP521R1`, including their common OpenSSL and NIST aliases. + +The default TLS groups are used when empty. + +### tls.control_wrap + +OpenVPN control channel wrapping. + +Equivalent to OpenVPN `tls-auth`, `tls-crypt`, and `tls-crypt-v2`. + +Disabled if empty. + +### tls.control_wrap.type + +Control channel wrapping type, one of `tls_auth`, `tls_crypt`, or `tls_crypt_v2`. + +### tls.control_wrap.key + +Control channel wrapping key content. + +Conflict with `tls.control_wrap.key_path`. + +### tls.control_wrap.key_path + +Control channel wrapping key path. + +Conflict with `tls.control_wrap.key`. + +### tls.control_wrap.direction + +`tls-auth` key direction, one of `server` or `client`. + +Only available when `tls.control_wrap.type` is `tls_auth`. The key is used bidirectionally if empty. + +### data_ciphers + +Allowed OpenVPN data channel ciphers. + +`AES-256-GCM`, `AES-128-GCM`, and `CHACHA20-POLY1305` are used by default. + +### data_ciphers_fallback + +Data channel cipher for peers that do not support cipher negotiation. + +Disabled by default. + +### auth + +OpenVPN data channel authentication digest. + +`SHA1` is used by default. It only applies to non-AEAD data ciphers and `tls_auth`. + +### mss_fix + +Maximum OpenVPN UDP packet size used to clamp the MSS of TCP connections sent through the tunnel. + +This prevents TCP packets from exceeding the path MTU after OpenVPN encapsulation. + +Disabled when `0`. + +### fragment + +Maximum OpenVPN UDP packet size used for OpenVPN data channel fragmentation. + +Disabled when `0`. A non-zero value must be at least `68`. + +Conflict with TCP transport. + +### compression + +OpenVPN `compress` framing mode, one of `none`, `no`, `lz4`, `lz4-v2`, `stub`, `stub-v2`, `disabled`, or `off`. + +Disabled by default. + +Compression can weaken traffic confidentiality. Prefer `stub` or `stub-v2` only when framing compatibility is required. + +### compression_lzo + +OpenVPN `comp-lzo` mode, one of `none`, `no`, `yes`, `adaptive`, `asym`, `disabled`, or `off`. + +Disabled by default. + +Compression can weaken traffic confidentiality. Enable it only when required by the server. + +### allow_compression + +Policy for compression pushed by the server, one of `no`, `asym`, or `yes`. + +`no` is used by default and permits only compression stub framing. `asym` accepts compressed packets from the server but does not compress outgoing packets. `yes` permits compression in both directions. + +Conflict with non-stub compression enabled by `compression` or `compression_lzo` when set to `no`. + +### route_no_pull + +Ignore routes, route gateways, and redirect-gateway options pushed by the server. + +Other pushed options are still accepted, and locally configured `routes` are still used. + +Disabled by default. + +### pull_filters + +Ordered filters for options pushed by the server. + +The first filter whose `text` is a case-insensitive prefix of the complete pushed option is applied. Options that match no filter are accepted. + +### pull_filters.action + +==Required== + +Filter action, one of `accept`, `ignore`, or `reject`. + +`accept` applies the option, `ignore` discards it, and `reject` terminates the connection. + +### pull_filters.text + +==Required== + +Case-insensitive prefix to match against the pushed option name and value. + +For example, `route ` matches pushed IPv4 route options without matching `route-gateway`. + +### routes + +IPv4 and IPv6 route prefixes routed through the OpenVPN endpoint. + +These routes are used in addition to routes accepted from the server. + +### route_gateway + +IPv4 gateway for routes through the OpenVPN endpoint. + +When empty, the VPN gateway received from the server is used. + +### route_metric + +Default metric for routes through the OpenVPN endpoint. + +The platform default is used when `0`. + +### redirect_gateway + +Route all IPv4 traffic through the OpenVPN endpoint. + +Disabled by default. + +### redirect_gateway_flags + +OpenVPN `redirect-gateway` flags. + +`!ipv4` disables the IPv4 default route, and `ipv6` also routes all IPv6 traffic through the endpoint. Other OpenVPN flags are accepted for compatibility but do not change endpoint routing. + +Empty by default. + +### keepalive_interval + +Interval for sending OpenVPN keepalive ping packets. + +Locally configured values take precedence over server-pushed keepalive values. + +Disabled by default. + +### keepalive_timeout + +Time without receiving OpenVPN traffic before the connection is restarted. + +Locally configured values take precedence over server-pushed keepalive values. + +Disabled by default. + +### renegotiate_interval + +OpenVPN TLS renegotiation interval. + +If empty or set to `0s`, the OpenVPN default `1h` is used. + +### explicit_exit_notify + +Number of OpenVPN exit notifications sent when closing a UDP connection. + +Disabled when `0`. At most `10` notifications are sent. + +### system + +Use a system interface. + +Requires privilege and cannot conflict with existing system interfaces. + +If disabled, sing-box uses the internal network stack. + +### name + +Custom interface name for the system interface. + +An automatically generated `ovpn` interface name is used by default. + +### mtu + +OpenVPN interface MTU. + +When empty, `1500` is used until a server-pushed MTU is received. + +### udp_timeout + +UDP NAT expiration time. + +`5m` is used by default. + +## Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. + +## Interactive authentication + +Use `Tools` > `Endpoints` in the sing-box dashboard or any sing-box graphical client to authenticate and manage the endpoint. diff --git a/docs/configuration/endpoint/openvpn-client.zh.md b/docs/configuration/endpoint/openvpn-client.zh.md new file mode 100644 index 000000000..bc48ecab6 --- /dev/null +++ b/docs/configuration/endpoint/openvpn-client.zh.md @@ -0,0 +1,501 @@ +# OpenVPN 客户端 + +!!! question "自 sing-box 1.14.0 起" + +## 结构 + +```json +{ + "type": "openvpn-client", + "tag": "ovpn-client", + + "server": "127.0.0.1", + "server_port": 1194, + "servers": [ + { + "server": "127.0.0.1", + "server_port": 1194, + "network": "udp" + } + ], + "remote_random": false, + "network": "udp", + "username": "", + "password": "", + "auth_retry": "none", + "static_challenge": "", + "static_challenge_echo": false, + "tls": { + "server_name": "", + "server_name_type": "name", + "certificate": [], + "certificate_path": "", + "client_certificate": [], + "client_certificate_path": "", + "client_key": [], + "client_key_path": "", + "peer_fingerprint": [], + "crl_path": "", + "remote_certificate_ku": [], + "remote_certificate_eku": "", + "version_min": "1.2", + "version_max": "", + "cipher": "", + "groups": "", + "control_wrap": { + "type": "", + "key": [], + "key_path": "", + "direction": "" + } + }, + "data_ciphers": [], + "data_ciphers_fallback": "", + "auth": "", + "mss_fix": 0, + "fragment": 0, + "compression": "", + "compression_lzo": "", + "allow_compression": "no", + "route_no_pull": false, + "pull_filters": [ + { + "action": "ignore", + "text": "route " + } + ], + "routes": [], + "route_gateway": "", + "route_metric": 0, + "redirect_gateway": false, + "redirect_gateway_flags": [], + "keepalive_interval": "", + "keepalive_timeout": "", + "renegotiate_interval": "", + "explicit_exit_notify": 0, + "system": false, + "name": "", + "mtu": 1500, + "udp_timeout": "", + + ... // 拨号字段 +} +``` + +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签。 + +## 字段 + +### server + +OpenVPN 服务器地址。 + +`server` 和 `servers` 之一必填。 + +与 `servers` 冲突。 + +### server_port + +OpenVPN 服务器端口。 + +设置 `server` 时必填。 + +### servers + +OpenVPN 服务器列表。 + +客户端按顺序尝试服务器,并在连接失败时尝试下一台服务器。 + +`server` 和 `servers` 之一必填。 + +与 `server` 冲突。 + +### servers.server + +==必填== + +OpenVPN 服务器地址。 + +### servers.server_port + +==必填== + +OpenVPN 服务器端口。 + +### servers.network + +该服务器的 OpenVPN 传输网络,可选值为 `udp` 或 `tcp`。 + +默认使用顶层 `network`。 + +### remote_random + +连接前随机排列 `servers` 顺序。 + +默认禁用。 + +### network + +默认 OpenVPN 传输网络,可选值为 `udp` 或 `tcp`。 + +默认使用 `udp`。 + +该值应用于 `server` 和未单独设置 `network` 的 `servers` 条目。 + +### username + +OpenVPN 用户名/密码认证的用户名。 + +### password + +OpenVPN 用户名/密码认证的密码。 + +### auth_retry + +用户名/密码认证失败后的行为,可选值为 `none`、`nointeract` 或 `interact`。 + +默认使用 `none`,并将永久认证失败视为终止错误。 + +`nointeract` 和 `interact` 允许重试认证。 + +### static_challenge + +请求认证响应时显示的静态质询文本。 + +### static_challenge_echo + +以明文显示静态质询响应。 + +### tls + +==必填== + +OpenVPN 控制通道 TLS 配置。 + +### tls.server_name + +预期的服务器证书名称。 + +为空时禁用证书名称验证,但仍会验证证书链或 fingerprint 与服务器证书用途。 + +### tls.server_name_type + +与 `tls.server_name` 匹配的证书字段,可选值为 `subject`、`name` 或 `name-prefix`。 + +设置 `tls.server_name` 时默认使用 `name`。 + +`subject` 匹配完整证书 subject,`name` 精确匹配 common name,`name-prefix` 匹配 common name 前缀。 + +### tls.certificate + +受信任 CA 证书内容。 + +`tls.certificate`、`tls.certificate_path` 和 `tls.peer_fingerprint` 之一必填。 + +与 `tls.certificate_path` 冲突。 + +### tls.certificate_path + +受信任 CA 证书路径。 + +`tls.certificate`、`tls.certificate_path` 和 `tls.peer_fingerprint` 之一必填。 + +与 `tls.certificate` 冲突。 + +### tls.client_certificate + +客户端证书内容。 + +与 `tls.client_certificate_path` 冲突。 + +### tls.client_certificate_path + +客户端证书路径。 + +与 `tls.client_certificate` 冲突。 + +### tls.client_key + +客户端私钥内容。 + +与 `tls.client_key_path` 冲突。 + +### tls.client_key_path + +客户端私钥路径。 + +与 `tls.client_key` 冲突。 + +客户端证书和私钥必须同时设置或同时为空。 + +### tls.peer_fingerprint + +允许的服务器 leaf certificate 的 SHA-256 fingerprint。 + +每个 fingerprint 必须是不带分隔符的 64 字符小写十六进制字符串。 + +同时配置受信任 CA 时,会同时验证证书链和 fingerprint。未配置受信任 CA 时,会验证 fingerprint、证书有效期、配置的名称和证书用途,但不验证证书链。 + +### tls.crl_path + +用于拒绝已吊销服务器证书的 PEM 或 DER CRL 文件路径。 + +根据受信任证书链验证 CRL 签名和有效期。 + +默认禁用。 + +### tls.remote_certificate_ku + +服务器证书所需的 Key Usage mask,使用 OpenVPN `remote-cert-ku` 格式的十六进制值。 + +多个值会被组合,证书必须包含所有要求的用途。 + +默认禁用。 + +### tls.remote_certificate_eku + +服务器证书所需的 Extended Key Usage,可选值为 `server` 或 `client`。 + +默认禁用。标准 OpenVPN 服务器证书用途检查仍然生效。 + +### tls.version_min + +最低 TLS 版本,可选值为 `1.0`、`1.1`、`1.2` 或 `1.3`。 + +默认使用 `1.2`。 + +### tls.version_max + +最高 TLS 版本,可选值为 `1.0`、`1.1`、`1.2` 或 `1.3`。 + +默认使用支持的最高版本。 + +该值不能低于 `tls.version_min`。 + +### tls.cipher + +TLS 1.2 及更低版本允许的 OpenSSL cipher suite 名称,以冒号分隔。 + +为空时使用默认 TLS cipher suite。该字段不控制 TLS 1.3 cipher suite。 + +### tls.groups + +按偏好顺序排列的 TLS key exchange group,以冒号分隔。 + +支持 `X25519`、`SECP256R1`、`SECP384R1` 和 `SECP521R1`,包括其常用 OpenSSL 和 NIST 别名。 + +为空时使用默认 TLS group。 + +### tls.control_wrap + +OpenVPN 控制通道封装。 + +等价于 OpenVPN `tls-auth`、`tls-crypt` 和 `tls-crypt-v2`。 + +为空时禁用。 + +### tls.control_wrap.type + +控制通道封装类型,可选值为 `tls_auth`、`tls_crypt` 或 `tls_crypt_v2`。 + +### tls.control_wrap.key + +控制通道封装密钥内容。 + +与 `tls.control_wrap.key_path` 冲突。 + +### tls.control_wrap.key_path + +控制通道封装密钥路径。 + +与 `tls.control_wrap.key` 冲突。 + +### tls.control_wrap.direction + +`tls-auth` 密钥方向,可选值为 `server` 或 `client`。 + +仅当 `tls.control_wrap.type` 为 `tls_auth` 时可用。为空时双向使用密钥。 + +### data_ciphers + +允许的 OpenVPN 数据通道 cipher。 + +默认使用 `AES-256-GCM`、`AES-128-GCM` 和 `CHACHA20-POLY1305`。 + +### data_ciphers_fallback + +用于不支持 cipher 协商的对端的数据通道 cipher。 + +默认禁用。 + +### auth + +OpenVPN 数据通道认证摘要。 + +默认使用 `SHA1`,仅应用于非 AEAD 数据 cipher 和 `tls_auth`。 + +### mss_fix + +OpenVPN UDP packet 的最大大小,用于限制通过隧道发送的 TCP 连接 MSS。 + +这可以避免 TCP packet 在 OpenVPN 封装后超过 path MTU。 + +设为 `0` 时禁用。 + +### fragment + +用于 OpenVPN 数据通道 fragmentation 的最大 OpenVPN UDP packet 大小。 + +设为 `0` 时禁用。非零值必须至少为 `68`。 + +与 TCP 传输冲突。 + +### compression + +OpenVPN `compress` framing 模式,可选值为 `none`、`no`、`lz4`、`lz4-v2`、`stub`、`stub-v2`、`disabled` 或 `off`。 + +默认禁用。 + +Compression 可能削弱流量机密性。仅在需要 framing 兼容性时使用 `stub` 或 `stub-v2`。 + +### compression_lzo + +OpenVPN `comp-lzo` 模式,可选值为 `none`、`no`、`yes`、`adaptive`、`asym`、`disabled` 或 `off`。 + +默认禁用。 + +Compression 可能削弱流量机密性。仅在服务器要求时启用。 + +### allow_compression + +服务器推送的 compression 策略,可选值为 `no`、`asym` 或 `yes`。 + +默认使用 `no`,仅允许 compression stub framing。`asym` 接受来自服务器的 compressed packet,但不压缩出站 packet。`yes` 允许双向 compression。 + +当设为 `no` 时,与通过 `compression` 或 `compression_lzo` 启用的非 stub compression 冲突。 + +### route_no_pull + +忽略服务器推送的 route、route gateway 和 redirect-gateway 选项。 + +仍会接受其他推送选项,并继续使用本地配置的 `routes`。 + +默认禁用。 + +### pull_filters + +服务器推送选项的有序 pull filter 列表。 + +应用第一个 `text` 为完整推送选项大小写不敏感前缀的 filter。未匹配任何 filter 的选项会被接受。 + +### pull_filters.action + +==必填== + +Filter action,可选值为 `accept`、`ignore` 或 `reject`。 + +`accept` 应用选项,`ignore` 丢弃选项,`reject` 终止连接。 + +### pull_filters.text + +==必填== + +用于匹配推送选项名称和值的大小写不敏感前缀。 + +例如,`route ` 会匹配推送的 IPv4 route 选项,但不会匹配 `route-gateway`。 + +### routes + +通过 OpenVPN endpoint 路由的 IPv4 和 IPv6 route prefix。 + +这些 route 会与从服务器接受的 route 一起使用。 + +### route_gateway + +通过 OpenVPN endpoint 路由的 IPv4 gateway。 + +为空时使用从服务器接收的 VPN gateway。 + +### route_metric + +通过 OpenVPN endpoint 路由的默认 metric。 + +设为 `0` 时使用平台默认值。 + +### redirect_gateway + +通过 OpenVPN endpoint 路由所有 IPv4 流量。 + +默认禁用。 + +### redirect_gateway_flags + +OpenVPN `redirect-gateway` flag。 + +`!ipv4` 禁用 IPv4 default route,`ipv6` 还会通过 endpoint 路由所有 IPv6 流量。接受其他 OpenVPN flag 以兼容配置,但它们不会改变 endpoint 路由。 + +默认为空。 + +### keepalive_interval + +发送 OpenVPN keepalive ping packet 的间隔。 + +本地配置值优先于服务器推送的 keepalive 值。 + +默认禁用。 + +### keepalive_timeout + +未接收 OpenVPN 流量后重新启动连接的时间。 + +本地配置值优先于服务器推送的 keepalive 值。 + +默认禁用。 + +### renegotiate_interval + +OpenVPN TLS 重新协商间隔。 + +如果为空或设为 `0s`,使用 OpenVPN 默认值 `1h`。 + +### explicit_exit_notify + +关闭 UDP 连接时发送的 OpenVPN exit notification 数量。 + +设为 `0` 时禁用。最多发送 `10` 个 notification。 + +### system + +使用系统接口。 + +需要权限,且不能与现有系统接口冲突。 + +禁用时,sing-box 使用内部网络栈。 + +### name + +系统接口的自定义接口名称。 + +默认使用自动生成的 `ovpn` 接口名称。 + +### mtu + +OpenVPN 接口 MTU。 + +为空时使用服务器推送的 MTU;收到服务器配置前使用 `1500`。 + +### udp_timeout + +UDP NAT 过期时间。 + +默认使用 `5m`。 + +## 拨号字段 + +参阅[拨号字段](/zh/configuration/shared/dial/)。 + +## 交互式认证 + +在 sing-box dashboard 或任意 sing-box 图形客户端的 `工具` > `端点` 中认证和管理 endpoint。 diff --git a/docs/configuration/endpoint/openvpn-server.md b/docs/configuration/endpoint/openvpn-server.md new file mode 100644 index 000000000..583d61262 --- /dev/null +++ b/docs/configuration/endpoint/openvpn-server.md @@ -0,0 +1,319 @@ +# OpenVPN Server + +!!! question "Since sing-box 1.14.0" + +## Structure + +```json +{ + "type": "openvpn-server", + "tag": "ovpn-server", + + ... // Listen Fields + + "system": false, + "name": "", + "mtu": 1500, + "network": "udp", + "max_clients": 1024, + "address": [], + "topology": "subnet", + "users": [ + { + "username": "", + "password": "" + } + ], + "tls": { + "certificate": [], + "certificate_path": "", + "key": [], + "key_path": "", + "client_certificate": [], + "client_certificate_path": "", + "verify_client_certificate": "require", + "control_wrap": { + "type": "tls_crypt", + "key": [], + "key_path": "", + "direction": "" + } + }, + "data_ciphers": [], + "data_ciphers_fallback": "", + "auth": "", + "push": { + "routes": [], + "dns": [], + "redirect_gateway": false, + "redirect_gateway_flags": [], + "block_outside_dns": false + }, + "keepalive_interval": "", + "keepalive_timeout": "", + "renegotiate_interval": "", + "udp_timeout": "" +} +``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + +## Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +## Fields + +### system + +Use system interface. + +Requires privilege and cannot conflict with existing system interfaces. + +If disabled, sing-box uses the internal network stack. + +### name + +Custom interface name for system interface. + +An automatically generated `ovpn` interface name is used by default. + +### mtu + +OpenVPN interface MTU. + +`1500` will be used by default. + +### network + +OpenVPN transport network, one of `udp` or `tcp`. + +`udp` will be used by default. + +Only one transport network is served per endpoint; to serve both TCP and UDP, +configure two endpoints with separate `address` subnets, +matching upstream OpenVPN which requires two server processes. + +### max_clients + +Maximum number of established and pending TLS client sessions. + +`1024` is used by default. The value must be smaller than `16777216`, the size of the OpenVPN peer-id space. + +### address + +==Required== + +List of OpenVPN server address prefixes. + +At most one IPv4 prefix and one IPv6 prefix are supported. + +The prefix address is assigned to the server interface. The masked prefix is used as the client address pool and route. + +The first IPv4 and IPv6 prefix addresses are used as the endpoint's local addresses. + +### topology + +OpenVPN topology pushed to clients, one of `subnet`, `p2p` or `net30`. + +`subnet` will be used by default. + +### users + +List of OpenVPN username/password users. + +If set, clients must pass username/password authentication in addition to any certificate policy configured by `tls.verify_client_certificate`. + +### users.username + +Username. + +### users.password + +Password. + +### tls + +==Required== + +OpenVPN control channel TLS configuration. + +### tls.certificate + +TLS server certificate content. + +Either `tls.certificate` or `tls.certificate_path` is required. + +Conflict with `tls.certificate_path`. + +### tls.certificate_path + +TLS server certificate path. + +Either `tls.certificate` or `tls.certificate_path` is required. + +Conflict with `tls.certificate`. + +### tls.key + +TLS server private key content. + +Either `tls.key` or `tls.key_path` is required. + +Conflict with `tls.key_path`. + +### tls.key_path + +TLS server private key path. + +Either `tls.key` or `tls.key_path` is required. + +Conflict with `tls.key`. + +### tls.client_certificate + +TLS CA certificate content, used to verify client certificates. + +Either `tls.client_certificate` or `tls.client_certificate_path` is required. + +Conflict with `tls.client_certificate_path`. + +### tls.client_certificate_path + +TLS CA certificate path, used to verify client certificates. + +Either `tls.client_certificate` or `tls.client_certificate_path` is required. + +Conflict with `tls.client_certificate`. + +### tls.verify_client_certificate + +OpenVPN client certificate policy, one of `require`, `optional` or `none`. + +`require` will be used by default. + +If set to `optional`, a client certificate is verified when provided, but clients without a certificate are allowed. + +If set to `none`, client certificates are not requested. + +This field does not replace `users`; when `users` is set, username/password authentication is still required. + +### tls.control_wrap + +OpenVPN control channel wrapping. + +Equivalent to OpenVPN `tls-auth`, `tls-crypt` and `tls-crypt-v2`. + +Disabled by default. + +### tls.control_wrap.type + +==Required== + +Control channel wrapping type, one of `tls_auth`, `tls_crypt` or `tls_crypt_v2`. + +For `tls_crypt_v2`, the key is the server key. + +### tls.control_wrap.key + +Control channel wrapping key content. + +Either `tls.control_wrap.key` or `tls.control_wrap.key_path` is required. + +Conflict with `tls.control_wrap.key_path`. + +### tls.control_wrap.key_path + +Control channel wrapping key path. + +Either `tls.control_wrap.key` or `tls.control_wrap.key_path` is required. + +Conflict with `tls.control_wrap.key`. + +### tls.control_wrap.direction + +OpenVPN `tls-auth` key direction, one of `server` or `client`. + +Only available when `tls.control_wrap.type` is `tls_auth`. + +`server` maps to OpenVPN key direction `0`, and `client` maps to `1`; by convention servers use `0` and clients use `1`. + +If empty, the key is used bidirectionally, matching an omitted `key-direction` on both peers. + +### data_ciphers + +Allowed OpenVPN data channel ciphers. + +`AES-256-GCM`, `AES-128-GCM` and `CHACHA20-POLY1305` are used by default. + +### data_ciphers_fallback + +OpenVPN data channel cipher for legacy clients that do not support cipher negotiation. + +Equivalent to OpenVPN `data-ciphers-fallback`. + +Disabled by default. + +### auth + +OpenVPN data channel authentication digest. + +`SHA1` will be used by default, matching the upstream default; it only applies to non-AEAD data ciphers and `tls_auth`. + +### push + +Options pushed to clients. + +### push.routes + +Routes to push to clients. + +IPv4 and IPv6 prefixes can be mixed. + +### push.dns + +DNS server addresses to push to clients. + +### push.redirect_gateway + +Push `redirect-gateway` to clients, which routes client traffic through the VPN according to `push.redirect_gateway_flags`. + +When `push.redirect_gateway_flags` is empty, `def1` is used by default. + +### push.redirect_gateway_flags + +OpenVPN `redirect-gateway` flags to push to clients. + +Only available when `push.redirect_gateway` is enabled. + +`def1` is used by default. + +### push.block_outside_dns + +Push `block-outside-dns` to clients, which blocks DNS queries outside the VPN on Windows clients. + +### keepalive_interval + +OpenVPN keepalive ping interval to push to clients. + +Disabled by default. + +### keepalive_timeout + +OpenVPN keepalive ping timeout to push to clients. + +Disabled by default. + +### renegotiate_interval + +OpenVPN TLS renegotiation interval. + +If empty or set to `0s`, the OpenVPN default `1h` is used. + +### udp_timeout + +UDP NAT expiration time for traffic through the OpenVPN interface. + +`5m` will be used by default. diff --git a/docs/configuration/endpoint/openvpn-server.zh.md b/docs/configuration/endpoint/openvpn-server.zh.md new file mode 100644 index 000000000..9cd9ff766 --- /dev/null +++ b/docs/configuration/endpoint/openvpn-server.zh.md @@ -0,0 +1,319 @@ +# OpenVPN 服务器 + +!!! question "自 sing-box 1.14.0 起" + +## 结构 + +```json +{ + "type": "openvpn-server", + "tag": "ovpn-server", + + ... // 监听字段 + + "system": false, + "name": "", + "mtu": 1500, + "network": "udp", + "max_clients": 1024, + "address": [], + "topology": "subnet", + "users": [ + { + "username": "", + "password": "" + } + ], + "tls": { + "certificate": [], + "certificate_path": "", + "key": [], + "key_path": "", + "client_certificate": [], + "client_certificate_path": "", + "verify_client_certificate": "require", + "control_wrap": { + "type": "tls_crypt", + "key": [], + "key_path": "", + "direction": "" + } + }, + "data_ciphers": [], + "data_ciphers_fallback": "", + "auth": "", + "push": { + "routes": [], + "dns": [], + "redirect_gateway": false, + "redirect_gateway_flags": [], + "block_outside_dns": false + }, + "keepalive_interval": "", + "keepalive_timeout": "", + "renegotiate_interval": "", + "udp_timeout": "" +} +``` + +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签 + +## 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/)。 + +## 字段 + +### system + +使用系统接口。 + +需要特权且不能与已有系统接口冲突。 + +如果禁用,sing-box 将使用内部网络栈。 + +### name + +系统接口的自定义接口名称。 + +默认使用自动生成的 `ovpn` 接口名称。 + +### mtu + +OpenVPN 接口 MTU。 + +默认使用 `1500`。 + +### network + +OpenVPN 传输网络,`udp` 或 `tcp` 之一。 + +默认使用 `udp`。 + +每个端点仅服务一种传输网络;如需同时服务 TCP 与 UDP, +需要配置两个端点并使用互不重叠的 `address` 子网, +与上游 OpenVPN 需要两个服务进程一致。 + +### max_clients + +已建立与握手中的 TLS 客户端会话的最大数量。 + +默认使用 `1024`。该值必须小于 OpenVPN peer-id 空间的大小 `16777216`。 + +### address + +==必填== + +OpenVPN 服务器地址前缀列表。 + +最多支持一个 IPv4 前缀和一个 IPv6 前缀。 + +前缀地址被分配给服务器接口。掩码后的前缀用作客户端地址池和路由。 + +第一个 IPv4 和 IPv6 前缀地址用作端点的本地地址。 + +### topology + +推送给客户端的 OpenVPN topology,`subnet`、`p2p` 或 `net30` 之一。 + +默认使用 `subnet`。 + +### users + +OpenVPN 用户名/密码用户列表。 + +如果设置,客户端除了通过 `tls.verify_client_certificate` 配置的证书策略外,还必须通过用户名/密码认证。 + +### users.username + +用户名。 + +### users.password + +密码。 + +### tls + +==必填== + +OpenVPN 控制信道 TLS 配置。 + +### tls.certificate + +TLS 服务器证书内容。 + +`tls.certificate` 或 `tls.certificate_path` 必填其一。 + +与 `tls.certificate_path` 冲突。 + +### tls.certificate_path + +TLS 服务器证书路径。 + +`tls.certificate` 或 `tls.certificate_path` 必填其一。 + +与 `tls.certificate` 冲突。 + +### tls.key + +TLS 服务器私钥内容。 + +`tls.key` 或 `tls.key_path` 必填其一。 + +与 `tls.key_path` 冲突。 + +### tls.key_path + +TLS 服务器私钥路径。 + +`tls.key` 或 `tls.key_path` 必填其一。 + +与 `tls.key` 冲突。 + +### tls.client_certificate + +TLS CA 证书内容,用于验证客户端证书。 + +`tls.client_certificate` 或 `tls.client_certificate_path` 必填其一。 + +与 `tls.client_certificate_path` 冲突。 + +### tls.client_certificate_path + +TLS CA 证书路径,用于验证客户端证书。 + +`tls.client_certificate` 或 `tls.client_certificate_path` 必填其一。 + +与 `tls.client_certificate` 冲突。 + +### tls.verify_client_certificate + +OpenVPN 客户端证书策略,`require`、`optional` 或 `none` 之一。 + +默认使用 `require`。 + +设为 `optional` 时,客户端提供证书则验证,不提供证书的客户端也被允许。 + +设为 `none` 时,不请求客户端证书。 + +该字段不替代 `users`;设置 `users` 后仍然要求用户名/密码认证。 + +### tls.control_wrap + +OpenVPN 控制信道包装。 + +等价于 OpenVPN `tls-auth`、`tls-crypt` 和 `tls-crypt-v2`。 + +默认禁用。 + +### tls.control_wrap.type + +==必填== + +控制信道包装类型,`tls_auth`、`tls_crypt` 或 `tls_crypt_v2` 之一。 + +对于 `tls_crypt_v2`,密钥为服务器密钥。 + +### tls.control_wrap.key + +控制信道包装密钥内容。 + +`tls.control_wrap.key` 或 `tls.control_wrap.key_path` 必填其一。 + +与 `tls.control_wrap.key_path` 冲突。 + +### tls.control_wrap.key_path + +控制信道包装密钥路径。 + +`tls.control_wrap.key` 或 `tls.control_wrap.key_path` 必填其一。 + +与 `tls.control_wrap.key` 冲突。 + +### tls.control_wrap.direction + +OpenVPN `tls-auth` 密钥方向,`server` 或 `client` 之一。 + +仅当 `tls.control_wrap.type` 为 `tls_auth` 时可用。 + +`server` 对应 OpenVPN 密钥方向 `0`,`client` 对应 `1`;按照惯例服务器使用 `0`,客户端使用 `1`。 + +如果为空,密钥被双向使用,与两端均省略 `key-direction` 的行为一致。 + +### data_ciphers + +允许的 OpenVPN 数据信道加密方式。 + +默认使用 `AES-256-GCM`、`AES-128-GCM` 和 `CHACHA20-POLY1305`。 + +### data_ciphers_fallback + +用于不支持加密方式协商的遗留客户端的 OpenVPN 数据信道加密方式。 + +等价于 OpenVPN `data-ciphers-fallback`。 + +默认禁用。 + +### auth + +OpenVPN 数据信道认证摘要。 + +默认使用 `SHA1`,与上游默认值一致;仅对非 AEAD 数据信道加密方式和 `tls_auth` 生效。 + +### push + +推送给客户端的选项。 + +### push.routes + +推送给客户端的路由。 + +IPv4 和 IPv6 前缀可以混用。 + +### push.dns + +推送给客户端的 DNS 服务器地址。 + +### push.redirect_gateway + +向客户端推送 `redirect-gateway`,根据 `push.redirect_gateway_flags` 通过 VPN 路由客户端流量。 + +当 `push.redirect_gateway_flags` 为空时,默认使用 `def1`。 + +### push.redirect_gateway_flags + +向客户端推送的 OpenVPN `redirect-gateway` flag。 + +仅当启用 `push.redirect_gateway` 时可用。 + +默认使用 `def1`。 + +### push.block_outside_dns + +向客户端推送 `block-outside-dns`,在 Windows 客户端上阻止 VPN 之外的 DNS 查询。 + +### keepalive_interval + +推送给客户端的 OpenVPN keepalive ping 间隔。 + +默认禁用。 + +### keepalive_timeout + +推送给客户端的 OpenVPN keepalive ping 超时。 + +默认禁用。 + +### renegotiate_interval + +OpenVPN TLS 重协商间隔。 + +如果为空或设为 `0s`,使用 OpenVPN 默认值 `1h`。 + +### udp_timeout + +通过 OpenVPN 接口的流量的 UDP NAT 过期时间。 + +默认使用 `5m`。 diff --git a/docs/configuration/endpoint/tailscale.md b/docs/configuration/endpoint/tailscale.md index 5c41ad936..e083b8dae 100644 --- a/docs/configuration/endpoint/tailscale.md +++ b/docs/configuration/endpoint/tailscale.md @@ -60,7 +60,7 @@ Example: `$HOME/.tailscale` !!! note - Auth key is not required. By default, sing-box will log the login URL (or popup a notification on graphical clients). + Auth key is not required. By default, sing-box will log the login URL. The auth key to create the node. If the node is already created (from state previously stored), then this field is not used. @@ -208,3 +208,7 @@ Refuse local and remote TCP and Unix-socket forwarding, including SSH agent forw Dial Fields in Tailscale endpoints only control how it connects to the control plane and have nothing to do with actual connections. See [Dial Fields](/configuration/shared/dial/) for details. + +### Interactive authentication + +Use `Tools` > `Endpoints` in the sing-box dashboard or any sing-box graphical client to authenticate and manage the endpoint. diff --git a/docs/configuration/endpoint/tailscale.zh.md b/docs/configuration/endpoint/tailscale.zh.md index 5ba6c7dad..7cc36fb2a 100644 --- a/docs/configuration/endpoint/tailscale.zh.md +++ b/docs/configuration/endpoint/tailscale.zh.md @@ -60,7 +60,7 @@ icon: material/new-box !!! note - 认证密钥不是必需的。默认情况下,sing-box 将记录登录 URL(或在图形客户端上弹出通知)。 + 认证密钥不是必需的。默认情况下,sing-box 将记录登录 URL。 用于创建节点的认证密钥。如果节点已经创建(从之前存储的状态),则不使用此字段。 @@ -207,3 +207,7 @@ UDP NAT 过期时间。 Tailscale 端点中的拨号字段仅控制它如何连接到控制平面,与实际连接无关。 参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。 + +### 交互式认证 + +在 sing-box dashboard 或任意 sing-box 图形客户端的 `工具` > `端点` 中认证和管理 endpoint。 diff --git a/experimental/libbox/command_client.go b/experimental/libbox/command_client.go index c46b8d627..77adf75ee 100644 --- a/experimental/libbox/command_client.go +++ b/experimental/libbox/command_client.go @@ -879,32 +879,26 @@ func (c *CommandClient) StartSTUNTest(server string, outboundTag string, handler return session, nil } -func (c *CommandClient) SubscribeTailscaleStatus(handler TailscaleStatusHandler) (*TailscaleStatusSubscription, error) { +func subscribeStatus[T any](c *CommandClient, session *streamSession, name string, start func(context.Context, daemon.StartedServiceClient) (grpc.ServerStreamingClient[T], error), onUpdate func(*T), onError func(string)) error { client, parentCtx, err := c.getClientForCall() if err != nil { - return nil, E.Cause(err, "subscribe tailscale status") + return E.Cause(err, "subscribe ", name) } streamCtx, cancel := context.WithCancel(parentCtx) - session := &TailscaleStatusSubscription{ - streamSession: streamSession{ - ctx: streamCtx, - cancel: cancel, - closeDone: make(chan struct{}), - }, + *session = streamSession{ + ctx: streamCtx, + cancel: cancel, + closeDone: make(chan struct{}), } - failStart := func(cause error, message string) (*TailscaleStatusSubscription, error) { + stream, err := start(streamCtx, client) + if err != nil { cancel() if c.standalone { c.closeConnection() } - return nil, E.Cause(cause, message) - } - - stream, err := client.SubscribeTailscaleStatus(streamCtx, &emptypb.Empty{}) - if err != nil { - return failStart(err, "subscribe tailscale status") + return E.Cause(err, "subscribe ", name) } standalone := c.standalone @@ -924,71 +918,123 @@ func (c *CommandClient) SubscribeTailscaleStatus(handler TailscaleStatusHandler) if status.Code(recvErr) == codes.NotFound || status.Code(recvErr) == codes.Unavailable { return } - handler.OnError(E.Cause(recvErr, "tailscale status recv").Error()) + onError(E.Cause(recvErr, name, " recv").Error()) return } - handler.OnStatusUpdate(tailscaleStatusUpdateFromGRPC(event)) + onUpdate(event) } }() + return nil +} +func (c *CommandClient) SubscribeTailscaleStatus(handler TailscaleStatusHandler) (*TailscaleStatusSubscription, error) { + session := new(TailscaleStatusSubscription) + err := subscribeStatus(c, &session.streamSession, "tailscale status", func(ctx context.Context, client daemon.StartedServiceClient) (grpc.ServerStreamingClient[daemon.TailscaleStatusUpdate], error) { + return client.SubscribeTailscaleStatus(ctx, &emptypb.Empty{}) + }, func(update *daemon.TailscaleStatusUpdate) { + handler.OnStatusUpdate(tailscaleStatusUpdateFromGRPC(update)) + }, handler.OnError) + if err != nil { + return nil, err + } return session, nil } func (c *CommandClient) SubscribeUSBIPServerStatus(handler USBIPServerStatusHandler) (*USBIPServerStatusSubscription, error) { - client, parentCtx, err := c.getClientForCall() + session := new(USBIPServerStatusSubscription) + err := subscribeStatus(c, &session.streamSession, "usbip server status", func(ctx context.Context, client daemon.StartedServiceClient) (grpc.ServerStreamingClient[daemon.USBIPServerStatusUpdate], error) { + return client.SubscribeUSBIPServerStatus(ctx, &emptypb.Empty{}) + }, func(update *daemon.USBIPServerStatusUpdate) { + handler.OnStatusUpdate(usbipServerStatusUpdateFromGRPC(update)) + }, handler.OnError) if err != nil { - return nil, E.Cause(err, "subscribe usbip server status") + return nil, err } - - streamCtx, cancel := context.WithCancel(parentCtx) - session := &USBIPServerStatusSubscription{ - streamSession: streamSession{ - ctx: streamCtx, - cancel: cancel, - closeDone: make(chan struct{}), - }, - } - - failStart := func(cause error, message string) (*USBIPServerStatusSubscription, error) { - cancel() - if c.standalone { - c.closeConnection() - } - return nil, E.Cause(cause, message) - } - - stream, err := client.SubscribeUSBIPServerStatus(streamCtx, &emptypb.Empty{}) - if err != nil { - return failStart(err, "subscribe usbip server status") - } - - standalone := c.standalone - go func() { - defer func() { - close(session.closeDone) - if standalone { - c.closeConnection() - } - }() - for { - event, recvErr := stream.Recv() - if recvErr != nil { - if session.ctx.Err() != nil { - return - } - if status.Code(recvErr) == codes.NotFound || status.Code(recvErr) == codes.Unavailable { - return - } - handler.OnError(E.Cause(recvErr, "usbip server status recv").Error()) - return - } - handler.OnStatusUpdate(usbipServerStatusUpdateFromGRPC(event)) - } - }() - return session, nil } +func (c *CommandClient) SubscribeOpenConnectStatus(handler OpenConnectStatusHandler) (*OpenConnectStatusSubscription, error) { + session := new(OpenConnectStatusSubscription) + err := subscribeStatus(c, &session.streamSession, "openconnect status", func(ctx context.Context, client daemon.StartedServiceClient) (grpc.ServerStreamingClient[daemon.OpenConnectStatusUpdate], error) { + return client.SubscribeOpenConnectStatus(ctx, &emptypb.Empty{}) + }, func(update *daemon.OpenConnectStatusUpdate) { + handler.OnStatusUpdate(openConnectStatusUpdateFromGRPC(update)) + }, handler.OnError) + if err != nil { + return nil, err + } + return session, nil +} + +func (c *CommandClient) SubmitOpenConnectAuthForm(endpointTag string, formID string, values *OpenConnectFormValues) error { + _, err := callWithResult(c, func(ctx context.Context, client daemon.StartedServiceClient) (*emptypb.Empty, error) { + return client.SubmitOpenConnectAuthForm(ctx, &daemon.OpenConnectAuthFormSubmission{ + EndpointTag: endpointTag, + FormID: formID, + Values: values.values, + }) + }) + if err != nil { + return E.Cause(err, "submit openconnect authentication form") + } + return nil +} + +func (c *CommandClient) CancelOpenConnectAuthForm(endpointTag string, formID string) error { + _, err := callWithResult(c, func(ctx context.Context, client daemon.StartedServiceClient) (*emptypb.Empty, error) { + return client.CancelOpenConnectAuthForm(ctx, &daemon.OpenConnectAuthFormCancel{ + EndpointTag: endpointTag, + FormID: formID, + }) + }) + if err != nil { + return E.Cause(err, "cancel openconnect authentication form") + } + return nil +} + +func (c *CommandClient) SubscribeOpenVPNStatus(handler OpenVPNStatusHandler) (*OpenVPNStatusSubscription, error) { + session := new(OpenVPNStatusSubscription) + err := subscribeStatus(c, &session.streamSession, "openvpn status", func(ctx context.Context, client daemon.StartedServiceClient) (grpc.ServerStreamingClient[daemon.OpenVPNStatusUpdate], error) { + return client.SubscribeOpenVPNStatus(ctx, &emptypb.Empty{}) + }, func(update *daemon.OpenVPNStatusUpdate) { + handler.OnStatusUpdate(openVPNStatusUpdateFromGRPC(update)) + }, handler.OnError) + if err != nil { + return nil, err + } + return session, nil +} + +func (c *CommandClient) SubmitOpenVPNChallengeResponse(endpointTag string, challengeID string, response *OpenVPNChallengeResponse) error { + _, err := callWithResult(c, func(ctx context.Context, client daemon.StartedServiceClient) (*emptypb.Empty, error) { + return client.SubmitOpenVPNChallengeResponse(ctx, &daemon.OpenVPNChallengeSubmission{ + EndpointTag: endpointTag, + ChallengeID: challengeID, + Username: response.Username, + Password: response.Password, + Secret: response.Secret, + }) + }) + if err != nil { + return E.Cause(err, "submit openvpn challenge response") + } + return nil +} + +func (c *CommandClient) CancelOpenVPNChallenge(endpointTag string, challengeID string) error { + _, err := callWithResult(c, func(ctx context.Context, client daemon.StartedServiceClient) (*emptypb.Empty, error) { + return client.CancelOpenVPNChallenge(ctx, &daemon.OpenVPNChallengeCancel{ + EndpointTag: endpointTag, + ChallengeID: challengeID, + }) + }) + if err != nil { + return E.Cause(err, "cancel openvpn challenge") + } + return nil +} + func (c *CommandClient) SetTailscaleExitNode(endpointTag string, stableID string) error { _, err := callWithResult(c, func(ctx context.Context, client daemon.StartedServiceClient) (*emptypb.Empty, error) { return client.SetTailscaleExitNode(ctx, &daemon.SetTailscaleExitNodeRequest{ diff --git a/experimental/libbox/command_types_openconnect.go b/experimental/libbox/command_types_openconnect.go new file mode 100644 index 000000000..cca27ce2c --- /dev/null +++ b/experimental/libbox/command_types_openconnect.go @@ -0,0 +1,164 @@ +package libbox + +import ( + "github.com/sagernet/sing-box/daemon" + "github.com/sagernet/sing/common" +) + +type OpenConnectStatusUpdate struct { + endpoints []*OpenConnectEndpointStatus +} + +func (u *OpenConnectStatusUpdate) Endpoints() OpenConnectEndpointStatusIterator { + return newIterator(u.endpoints) +} + +type OpenConnectEndpointStatusIterator interface { + Next() *OpenConnectEndpointStatus + HasNext() bool +} + +type OpenConnectEndpointStatus struct { + EndpointTag string + State string + AuthForm *OpenConnectAuthForm + Error string + TunnelInfo *OpenConnectTunnelInfo +} + +type OpenConnectTunnelInfo struct { + Server string + Flavor string + Transport string + ipv4 []string + ipv6 []string + dns []string + MTU int32 + ConnectedSince int64 +} + +func (i *OpenConnectTunnelInfo) IPv4() StringIterator { + return newIterator(i.ipv4) +} + +func (i *OpenConnectTunnelInfo) IPv6() StringIterator { + return newIterator(i.ipv6) +} + +func (i *OpenConnectTunnelInfo) DNS() StringIterator { + return newIterator(i.dns) +} + +type OpenConnectAuthForm struct { + ID string + Banner string + Message string + Error string + URL string + fields []*OpenConnectAuthFormField +} + +func (f *OpenConnectAuthForm) Fields() OpenConnectAuthFormFieldIterator { + return newIterator(f.fields) +} + +type OpenConnectAuthFormFieldIterator interface { + Next() *OpenConnectAuthFormField + HasNext() bool +} + +type OpenConnectAuthFormField struct { + SubmissionKey string + Name string + Label string + Kind string + Value string + options []*OpenConnectAuthFormChoice +} + +func (f *OpenConnectAuthFormField) Options() OpenConnectAuthFormChoiceIterator { + return newIterator(f.options) +} + +type OpenConnectAuthFormChoiceIterator interface { + Next() *OpenConnectAuthFormChoice + HasNext() bool +} + +type OpenConnectAuthFormChoice struct { + Value string + Label string +} + +type OpenConnectFormValues struct { + values map[string]string +} + +func NewOpenConnectFormValues() *OpenConnectFormValues { + return &OpenConnectFormValues{values: make(map[string]string)} +} + +func (v *OpenConnectFormValues) Add(key string, value string) { + v.values[key] = value +} + +type OpenConnectStatusHandler interface { + OnStatusUpdate(status *OpenConnectStatusUpdate) + OnError(message string) +} + +type OpenConnectStatusSubscription struct { + streamSession +} + +func openConnectStatusUpdateFromGRPC(update *daemon.OpenConnectStatusUpdate) *OpenConnectStatusUpdate { + return &OpenConnectStatusUpdate{ + endpoints: common.Map(update.Endpoints, openConnectEndpointStatusFromGRPC), + } +} + +func openConnectEndpointStatusFromGRPC(status *daemon.OpenConnectEndpointStatus) *OpenConnectEndpointStatus { + result := &OpenConnectEndpointStatus{ + EndpointTag: status.EndpointTag, + State: status.State, + Error: status.Error, + } + if status.AuthForm != nil { + fields := common.Map(status.AuthForm.Fields, func(field *daemon.OpenConnectAuthFormField) *OpenConnectAuthFormField { + return &OpenConnectAuthFormField{ + SubmissionKey: field.SubmissionKey, + Name: field.Name, + Label: field.Label, + Kind: field.Kind, + Value: field.Value, + options: common.Map(field.Options, func(option *daemon.OpenConnectAuthFormChoice) *OpenConnectAuthFormChoice { + return &OpenConnectAuthFormChoice{ + Value: option.Value, + Label: option.Label, + } + }), + } + }) + result.AuthForm = &OpenConnectAuthForm{ + ID: status.AuthForm.Id, + Banner: status.AuthForm.Banner, + Message: status.AuthForm.Message, + Error: status.AuthForm.Error, + URL: status.AuthForm.Url, + fields: fields, + } + } + if status.TunnelInfo != nil { + result.TunnelInfo = &OpenConnectTunnelInfo{ + Server: status.TunnelInfo.Server, + Flavor: status.TunnelInfo.Flavor, + Transport: status.TunnelInfo.Transport, + ipv4: status.TunnelInfo.Ipv4, + ipv6: status.TunnelInfo.Ipv6, + dns: status.TunnelInfo.Dns, + MTU: int32(status.TunnelInfo.Mtu), + ConnectedSince: status.TunnelInfo.ConnectedSince, + } + } + return result +} diff --git a/experimental/libbox/command_types_openvpn.go b/experimental/libbox/command_types_openvpn.go new file mode 100644 index 000000000..202d07c3b --- /dev/null +++ b/experimental/libbox/command_types_openvpn.go @@ -0,0 +1,117 @@ +package libbox + +import ( + "github.com/sagernet/sing-box/daemon" + "github.com/sagernet/sing/common" +) + +type OpenVPNStatusUpdate struct { + endpoints []*OpenVPNEndpointStatus +} + +func (u *OpenVPNStatusUpdate) Endpoints() OpenVPNEndpointStatusIterator { + return newIterator(u.endpoints) +} + +type OpenVPNEndpointStatusIterator interface { + Next() *OpenVPNEndpointStatus + HasNext() bool +} + +type OpenVPNEndpointStatus struct { + EndpointTag string + State string + Challenge *OpenVPNChallenge + Error string + TunnelInfo *OpenVPNTunnelInfo +} + +type OpenVPNTunnelInfo struct { + Server string + Network string + Cipher string + ipv4 []string + ipv6 []string + dns []string + MTU int32 + ConnectedSince int64 +} + +func (i *OpenVPNTunnelInfo) IPv4() StringIterator { + return newIterator(i.ipv4) +} + +func (i *OpenVPNTunnelInfo) IPv6() StringIterator { + return newIterator(i.ipv6) +} + +func (i *OpenVPNTunnelInfo) DNS() StringIterator { + return newIterator(i.dns) +} + +type OpenVPNChallenge struct { + ID string + Kind string + Username string + Message string + URL string + SecretMessage string + Echo bool + PreviousError string + Deadline int64 +} + +type OpenVPNChallengeResponse struct { + Username string + Password string + Secret string +} + +type OpenVPNStatusHandler interface { + OnStatusUpdate(status *OpenVPNStatusUpdate) + OnError(message string) +} + +type OpenVPNStatusSubscription struct { + streamSession +} + +func openVPNStatusUpdateFromGRPC(update *daemon.OpenVPNStatusUpdate) *OpenVPNStatusUpdate { + return &OpenVPNStatusUpdate{ + endpoints: common.Map(update.Endpoints, openVPNEndpointStatusFromGRPC), + } +} + +func openVPNEndpointStatusFromGRPC(status *daemon.OpenVPNEndpointStatus) *OpenVPNEndpointStatus { + result := &OpenVPNEndpointStatus{ + EndpointTag: status.EndpointTag, + State: status.State, + Error: status.Error, + } + if status.Challenge != nil { + result.Challenge = &OpenVPNChallenge{ + ID: status.Challenge.Id, + Kind: status.Challenge.Kind, + Username: status.Challenge.Username, + Message: status.Challenge.Message, + URL: status.Challenge.Url, + SecretMessage: status.Challenge.SecretMessage, + Echo: status.Challenge.Echo, + PreviousError: status.Challenge.PreviousError, + Deadline: status.Challenge.Deadline, + } + } + if status.TunnelInfo != nil { + result.TunnelInfo = &OpenVPNTunnelInfo{ + Server: status.TunnelInfo.Server, + Network: status.TunnelInfo.Network, + Cipher: status.TunnelInfo.Cipher, + ipv4: status.TunnelInfo.Ipv4, + ipv6: status.TunnelInfo.Ipv6, + dns: status.TunnelInfo.Dns, + MTU: int32(status.TunnelInfo.Mtu), + ConnectedSince: status.TunnelInfo.ConnectedSince, + } + } + return result +} diff --git a/go.mod b/go.mod index 9ee476370..0feb915ae 100644 --- a/go.mod +++ b/go.mod @@ -46,6 +46,8 @@ require ( github.com/sagernet/sing v0.8.12-0.20260717023913-84ab32b56cb8 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-20260717081856-cf2c71a71aba + github.com/sagernet/sing-openvpn v0.0.0-20260717092601-0db6ebb53109 github.com/sagernet/sing-quic v0.6.4-0.20260709034545-e23afe1172dc github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 @@ -83,6 +85,7 @@ require ( github.com/ajg/form v1.5.1 // indirect github.com/akutz/memconn v0.1.0 // indirect github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect + github.com/anchore/go-lzo v0.1.0 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect @@ -124,6 +127,9 @@ require ( github.com/mitchellh/go-ps v1.0.0 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pion/dtls/v3 v3.1.5 // indirect + github.com/pion/logging v0.2.4 // indirect + github.com/pion/transport/v4 v4.0.2 // indirect github.com/pires/go-proxyproto v0.8.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus-community/pro-bing v0.4.0 // indirect @@ -158,6 +164,7 @@ require ( github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260712142643-1e5048bd5587 // indirect github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260712142643-1e5048bd5587 // indirect github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/smallstep/pkcs7 v0.1.1 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect @@ -171,13 +178,14 @@ require ( github.com/tidwall/sjson v1.2.5 // indirect github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/zeebo/blake3 v0.2.4 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap/exp v0.3.0 // indirect go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/term v0.40.0 // indirect - golang.org/x/time v0.11.0 // indirect + golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.42.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect diff --git a/go.sum b/go.sum index aec72e0af..7128a7658 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs= +github.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= @@ -97,6 +99,7 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -174,6 +177,12 @@ github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pion/dtls/v3 v3.1.5 h1:9xJtVsHwMYeSjPp5Hh1FTis4DchnQWtnOa5o+6ygqfc= +github.com/pion/dtls/v3 v3.1.5/go.mod h1:gz1K4jg6c+fq86oQMH4pilpCEOEPwmEr2jY+VcF/mkU= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk= +github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM= github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= @@ -276,6 +285,10 @@ 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-20260717081856-cf2c71a71aba h1:S87Ej/jFssn0qhPF1ExF0YIV0USfGZF2If6kSjGLPt8= +github.com/sagernet/sing-openconnect v0.0.0-20260717081856-cf2c71a71aba/go.mod h1:EIzh5HtImfQJxPKXFwS9lyMnmMy4aCQCx7ntQ4u41Gs= +github.com/sagernet/sing-openvpn v0.0.0-20260717092601-0db6ebb53109 h1:j1cyRquNhaFXHkkbQ5oyldS75jhv0WrBToDXUYPVSOw= +github.com/sagernet/sing-openvpn v0.0.0-20260717092601-0db6ebb53109/go.mod h1:CmTGnS5ijVSqFQV1dTq4WvFLUoz7bk9xasBPsX8NcYo= github.com/sagernet/sing-quic v0.6.4-0.20260709034545-e23afe1172dc h1:zdc0fj4JdAdgAmQIoh7ZF+B/wPTEF2X75lYDqTmvlaw= github.com/sagernet/sing-quic v0.6.4-0.20260709034545-e23afe1172dc/go.mod h1:9k+dzGsWMttUGldBzq3dU792YHXzW6NgfbOGltnXq+0= github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE= @@ -300,6 +313,8 @@ github.com/sagernet/wireguard-go v0.0.5-0.20260717024847-6f5e8b1947ae h1:GmxlXWn github.com/sagernet/wireguard-go v0.0.5-0.20260717024847-6f5e8b1947ae/go.mod h1:hEqi4y5czEg6LYtX2Bpjg+lV0b/J1n+5rA885Z66Mx0= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= +github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU= +github.com/smallstep/pkcs7 v0.1.1/go.mod h1:dL6j5AIz9GHjVEBTXtW+QliALcgM19RtXaTeyxI+AfA= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= @@ -343,6 +358,9 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= @@ -377,6 +395,11 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/W golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= @@ -385,18 +408,35 @@ golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -405,24 +445,54 @@ golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= diff --git a/include/openconnect.go b/include/openconnect.go new file mode 100644 index 000000000..af48a8ea6 --- /dev/null +++ b/include/openconnect.go @@ -0,0 +1,12 @@ +//go:build with_openconnect + +package include + +import ( + "github.com/sagernet/sing-box/adapter/endpoint" + "github.com/sagernet/sing-box/protocol/openconnect" +) + +func registerOpenConnectEndpoint(registry *endpoint.Registry) { + openconnect.RegisterEndpoint(registry) +} diff --git a/include/openconnect_stub.go b/include/openconnect_stub.go new file mode 100644 index 000000000..93b9fa481 --- /dev/null +++ b/include/openconnect_stub.go @@ -0,0 +1,23 @@ +//go:build !with_openconnect + +package include + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/endpoint" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func registerOpenConnectEndpoint(registry *endpoint.Registry) { + endpoint.Register[option.OpenConnectEndpointOptions](registry, C.TypeOpenConnect, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.OpenConnectEndpointOptions) (adapter.Endpoint, error) { + if !options.System { + return nil, E.New(`OpenConnect is not included in this build, rebuild with -tags with_openconnect,with_gvisor for system:false`) + } + return nil, E.New(`OpenConnect is not included in this build, rebuild with -tags with_openconnect`) + }) +} diff --git a/include/openvpn.go b/include/openvpn.go new file mode 100644 index 000000000..ba3b0d086 --- /dev/null +++ b/include/openvpn.go @@ -0,0 +1,12 @@ +//go:build with_openvpn + +package include + +import ( + "github.com/sagernet/sing-box/adapter/endpoint" + "github.com/sagernet/sing-box/protocol/openvpn" +) + +func registerOpenVPNEndpoints(registry *endpoint.Registry) { + openvpn.RegisterEndpoint(registry) +} diff --git a/include/openvpn_stub.go b/include/openvpn_stub.go new file mode 100644 index 000000000..559577a89 --- /dev/null +++ b/include/openvpn_stub.go @@ -0,0 +1,29 @@ +//go:build !with_openvpn + +package include + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/endpoint" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func registerOpenVPNEndpoints(registry *endpoint.Registry) { + endpoint.Register[option.OpenVPNClientEndpointOptions](registry, C.TypeOpenVPNClient, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.OpenVPNClientEndpointOptions) (adapter.Endpoint, error) { + if !options.System { + return nil, E.New(`OpenVPN is not included in this build, rebuild with -tags with_openvpn,with_gvisor for system:false`) + } + return nil, E.New(`OpenVPN is not included in this build, rebuild with -tags with_openvpn`) + }) + endpoint.Register[option.OpenVPNServerEndpointOptions](registry, C.TypeOpenVPNServer, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.OpenVPNServerEndpointOptions) (adapter.Endpoint, error) { + if !options.System { + return nil, E.New(`OpenVPN is not included in this build, rebuild with -tags with_openvpn,with_gvisor for system:false`) + } + return nil, E.New(`OpenVPN is not included in this build, rebuild with -tags with_openvpn`) + }) +} diff --git a/include/registry.go b/include/registry.go index 28cbdd3cf..49e6b1a3e 100644 --- a/include/registry.go +++ b/include/registry.go @@ -111,6 +111,8 @@ func EndpointRegistry() *endpoint.Registry { registry := endpoint.NewRegistry() registerWireGuardEndpoint(registry) + registerOpenConnectEndpoint(registry) + registerOpenVPNEndpoints(registry) registerTailscaleEndpoint(registry) return registry diff --git a/mkdocs.yml b/mkdocs.yml index 4a5b061e0..31ba90b7d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -151,6 +151,9 @@ nav: - configuration/endpoint/index.md - WireGuard: configuration/endpoint/wireguard.md - Tailscale: configuration/endpoint/tailscale.md + - OpenConnect Client: configuration/endpoint/openconnect.md + - OpenVPN Client: configuration/endpoint/openvpn-client.md + - OpenVPN Server: configuration/endpoint/openvpn-server.md - Inbound: - configuration/inbound/index.md - Direct: configuration/inbound/direct.md diff --git a/option/openconnect.go b/option/openconnect.go new file mode 100644 index 000000000..121542ff6 --- /dev/null +++ b/option/openconnect.go @@ -0,0 +1,77 @@ +package option + +import "github.com/sagernet/sing/common/json/badoption" + +type OpenConnectEndpointOptions struct { + DialerOptions + System bool `json:"system,omitempty"` + Name string `json:"name,omitempty"` + Server string `json:"server"` + Flavor string `json:"flavor,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + AuthGroup string `json:"auth_group,omitempty"` + Token *OpenConnectTokenOptions `json:"token,omitempty"` + ReportedOS string `json:"reported_os,omitempty"` + UserAgent string `json:"user_agent,omitempty"` + CSD *OpenConnectCSDOptions `json:"csd,omitempty"` + HIP *OpenConnectHIPOptions `json:"hip,omitempty"` + TNCC *OpenConnectTNCCOptions `json:"tncc,omitempty"` + NoUDP bool `json:"no_udp,omitempty"` + AllowInsecureCrypto bool `json:"allow_insecure_crypto,omitempty"` + TLS OpenConnectTLSOptions `json:"tls,omitempty"` + FormEntries []OpenConnectFormEntryOptions `json:"form_entries,omitempty"` +} + +type OpenConnectTokenOptions struct { + Mode string `json:"mode,omitempty"` + Secret string `json:"secret,omitempty"` + PIN string `json:"pin,omitempty"` + Password string `json:"password,omitempty"` + DeviceID string `json:"device_id,omitempty"` + Counter uint64 `json:"counter,omitempty"` +} + +type OpenConnectCSDOptions struct { + WrapperPath string `json:"wrapper_path,omitempty"` +} + +type OpenConnectHIPOptions struct { + WrapperPath string `json:"wrapper_path,omitempty"` +} + +type OpenConnectTNCCOptions struct { + WrapperPath string `json:"wrapper_path,omitempty"` + DeviceID string `json:"device_id,omitempty"` + UserAgent string `json:"user_agent,omitempty"` + MachineIdentificationEnabled bool `json:"machine_identification_enabled,omitempty"` + Certificates []OpenConnectTNCCCertificateOptions `json:"certificates,omitempty"` +} + +type OpenConnectTNCCCertificateOptions struct { + Certificate badoption.Listable[string] `json:"certificate,omitempty"` + CertificatePath string `json:"certificate_path,omitempty"` +} + +type OpenConnectTLSOptions struct { + CertificateAuthority badoption.Listable[string] `json:"certificate_authority,omitempty"` + CertificateAuthorityPath string `json:"certificate_authority_path,omitempty"` + ClientCertificate badoption.Listable[string] `json:"client_certificate,omitempty"` + ClientCertificatePath string `json:"client_certificate_path,omitempty"` + ClientKey badoption.Listable[string] `json:"client_key,omitempty"` + ClientKeyPath string `json:"client_key_path,omitempty"` + ClientKeyPassword string `json:"client_key_password,omitempty"` + MCACertificate badoption.Listable[string] `json:"mca_certificate,omitempty"` + MCACertificatePath string `json:"mca_certificate_path,omitempty"` + MCAKey badoption.Listable[string] `json:"mca_key,omitempty"` + MCAKeyPath string `json:"mca_key_path,omitempty"` + MCAKeyPassword string `json:"mca_key_password,omitempty"` +} + +type OpenConnectFormEntryOptions struct { + FormID string `json:"form_id,omitempty"` + SubmissionKey string `json:"submission_key,omitempty"` + Name string `json:"name,omitempty"` + Value string `json:"value,omitempty"` + Promote bool `json:"promote,omitempty"` +} diff --git a/option/openvpn.go b/option/openvpn.go new file mode 100644 index 000000000..68bde24d1 --- /dev/null +++ b/option/openvpn.go @@ -0,0 +1,123 @@ +package option + +import ( + "net/netip" + + "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/json/badoption" +) + +type OpenVPNEndpointOptions struct { + System bool `json:"system,omitempty"` + Name string `json:"name,omitempty"` + MTU uint32 `json:"mtu,omitempty"` +} + +type OpenVPNClientEndpointOptions struct { + DialerOptions + ServerOptions + OpenVPNEndpointOptions + Network string `json:"network,omitempty"` + Servers []OpenVPNRemoteOptions `json:"servers,omitempty"` + RemoteRandom bool `json:"remote_random,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + AuthRetry string `json:"auth_retry,omitempty"` + StaticChallenge string `json:"static_challenge,omitempty"` + StaticChallengeEcho bool `json:"static_challenge_echo,omitempty"` + TLS *OpenVPNOutboundTLSOptions `json:"tls,omitempty"` + DataCiphers badoption.Listable[string] `json:"data_ciphers,omitempty"` + DataCiphersFallback string `json:"data_ciphers_fallback,omitempty"` + Auth string `json:"auth,omitempty"` + MSSFix uint32 `json:"mss_fix,omitempty"` + Fragment uint32 `json:"fragment,omitempty"` + Compression string `json:"compression,omitempty"` + CompressionLZO string `json:"compression_lzo,omitempty"` + AllowCompression string `json:"allow_compression,omitempty"` + RouteNoPull bool `json:"route_no_pull,omitempty"` + PullFilters []OpenVPNPullFilterOptions `json:"pull_filters,omitempty"` + Routes badoption.Listable[netip.Prefix] `json:"routes,omitempty"` + RouteGateway *badoption.Addr `json:"route_gateway,omitempty"` + RouteMetric int `json:"route_metric,omitempty"` + RedirectGateway bool `json:"redirect_gateway,omitempty"` + RedirectGatewayFlags badoption.Listable[string] `json:"redirect_gateway_flags,omitempty"` + KeepaliveInterval badoption.Duration `json:"keepalive_interval,omitempty"` + KeepaliveTimeout badoption.Duration `json:"keepalive_timeout,omitempty"` + RenegotiateInterval badoption.Duration `json:"renegotiate_interval,omitempty"` + ExplicitExitNotify uint32 `json:"explicit_exit_notify,omitempty"` + UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` +} + +type OpenVPNServerEndpointOptions struct { + ListenOptions + OpenVPNEndpointOptions + Network string `json:"network,omitempty"` + MaxClients int `json:"max_clients,omitempty"` + Address badoption.Listable[netip.Prefix] `json:"address"` + Topology string `json:"topology,omitempty"` + Users []auth.User `json:"users,omitempty"` + TLS *OpenVPNInboundTLSOptions `json:"tls,omitempty"` + DataCiphers badoption.Listable[string] `json:"data_ciphers,omitempty"` + DataCiphersFallback string `json:"data_ciphers_fallback,omitempty"` + Auth string `json:"auth,omitempty"` + Push *OpenVPNPushOptions `json:"push,omitempty"` + KeepaliveInterval badoption.Duration `json:"keepalive_interval,omitempty"` + KeepaliveTimeout badoption.Duration `json:"keepalive_timeout,omitempty"` + RenegotiateInterval badoption.Duration `json:"renegotiate_interval,omitempty"` +} + +type OpenVPNRemoteOptions struct { + ServerOptions + Network string `json:"network,omitempty"` +} + +type OpenVPNPullFilterOptions struct { + Action string `json:"action"` + Text string `json:"text"` +} + +type OpenVPNOutboundTLSOptions struct { + ServerName string `json:"server_name,omitempty"` + ServerNameType string `json:"server_name_type,omitempty"` + Certificate badoption.Listable[string] `json:"certificate,omitempty"` + CertificatePath string `json:"certificate_path,omitempty"` + ClientCertificate badoption.Listable[string] `json:"client_certificate,omitempty"` + ClientCertificatePath string `json:"client_certificate_path,omitempty"` + ClientKey badoption.Listable[string] `json:"client_key,omitempty"` + ClientKeyPath string `json:"client_key_path,omitempty"` + PeerFingerprint badoption.Listable[string] `json:"peer_fingerprint,omitempty"` + CRLPath string `json:"crl_path,omitempty"` + RemoteCertificateKU badoption.Listable[string] `json:"remote_certificate_ku,omitempty"` + RemoteCertificateEKU string `json:"remote_certificate_eku,omitempty"` + VersionMin string `json:"version_min,omitempty"` + VersionMax string `json:"version_max,omitempty"` + Cipher string `json:"cipher,omitempty"` + Groups string `json:"groups,omitempty"` + ControlWrap *OpenVPNControlWrapOptions `json:"control_wrap,omitempty"` +} + +type OpenVPNInboundTLSOptions struct { + Certificate badoption.Listable[string] `json:"certificate,omitempty"` + CertificatePath string `json:"certificate_path,omitempty"` + Key badoption.Listable[string] `json:"key,omitempty"` + KeyPath string `json:"key_path,omitempty"` + ClientCertificate badoption.Listable[string] `json:"client_certificate,omitempty"` + ClientCertificatePath string `json:"client_certificate_path,omitempty"` + VerifyClientCertificate string `json:"verify_client_certificate,omitempty"` + ControlWrap *OpenVPNControlWrapOptions `json:"control_wrap,omitempty"` +} + +type OpenVPNControlWrapOptions struct { + Type string `json:"type,omitempty"` + Key badoption.Listable[string] `json:"key,omitempty"` + KeyPath string `json:"key_path,omitempty"` + Direction string `json:"direction,omitempty"` +} + +type OpenVPNPushOptions struct { + Routes badoption.Listable[netip.Prefix] `json:"routes,omitempty"` + DNS badoption.Listable[netip.Addr] `json:"dns,omitempty"` + RedirectGateway bool `json:"redirect_gateway,omitempty"` + RedirectGatewayFlags badoption.Listable[string] `json:"redirect_gateway_flags,omitempty"` + BlockOutsideDNS bool `json:"block_outside_dns,omitempty"` +} diff --git a/protocol/openconnect/client.go b/protocol/openconnect/client.go new file mode 100644 index 000000000..b39b6cff6 --- /dev/null +++ b/protocol/openconnect/client.go @@ -0,0 +1,513 @@ +package openconnect + +import ( + "context" + "net" + "net/netip" + "net/url" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/endpoint" + "github.com/sagernet/sing-box/common/dialer" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + openconnecttransport "github.com/sagernet/sing-box/transport/openconnect" + "github.com/sagernet/sing-openconnect" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" + + "go4.org/netipx" +) + +var ( + _ adapter.OutboundWithPreferredRoutes = (*Endpoint)(nil) + _ adapter.FlowOutbound = (*Endpoint)(nil) + _ dialer.PacketDialerWithDestination = (*Endpoint)(nil) + _ tun.Port = (*Endpoint)(nil) +) + +type Endpoint struct { + endpointBase + loopContext context.Context + cancelLoop context.CancelFunc + dnsRouter adapter.DNSRouter + client *openconnect.Client + device openconnecttransport.Device + server string + flavor string + stateAccess sync.Mutex + state atomic.Pointer[clientState] + deviceStarted bool + readLoopDone chan struct{} + statusAccess sync.Mutex + statusUpdated chan struct{} + terminalError string + authFormLoopDone chan struct{} + activeTransportLoopDone chan struct{} + hotpCounter atomic.Uint64 +} + +type clientState struct { + started bool + tunnelConfigured bool + localAddresses []netip.Prefix + routeSet *netipx.IPSet + tunnelInfo adapter.OpenConnectTunnelInfo +} + +func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.OpenConnectEndpointOptions) (adapter.Endpoint, error) { + loopContext, cancelLoop := context.WithCancel(ctx) + openConnectEndpoint := &Endpoint{ + endpointBase: endpointBase{ + Adapter: endpoint.NewAdapterWithDialerOptions(C.TypeOpenConnect, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, options.DialerOptions), + router: router, + logger: logger, + }, + loopContext: loopContext, + cancelLoop: cancelLoop, + dnsRouter: service.FromContext[adapter.DNSRouter](ctx), + statusUpdated: make(chan struct{}), + } + openConnectEndpoint.state.Store(new(clientState)) + success := false + defer func() { + if success { + return + } + if openConnectEndpoint.device != nil { + _ = openConnectEndpoint.device.Close() + } + cancelLoop() + }() + server := options.Server + if !strings.Contains(server, "://") { + server = "https://" + server + } + serverURL, err := url.Parse(server) + if err != nil { + return nil, E.Cause(err, "parse OpenConnect server") + } + serverPort := serverURL.Port() + if serverPort == "" { + serverPort = "443" + } + openConnectEndpoint.server = net.JoinHostPort(serverURL.Hostname(), serverPort) + openConnectEndpoint.flavor = options.Flavor + if openConnectEndpoint.flavor == "" { + openConnectEndpoint.flavor = openconnect.FlavorAnyConnect + } + serverAddress, serverAddressErr := netip.ParseAddr(serverURL.Hostname()) + remoteIsDomain := serverURL.Hostname() != "" && serverAddressErr != nil && !serverAddress.IsValid() + outboundDialer, err := dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: options.DialerOptions, + RemoteIsDomain: remoteIsDomain, + ResolverOnDetour: true, + NewDialer: true, + }) + if err != nil { + return nil, err + } + device, err := openconnecttransport.NewDevice(openconnecttransport.DeviceOptions{ + Context: ctx, + Logger: logger, + System: options.System, + Handler: openConnectEndpoint, + UDPTimeout: C.UDPTimeout, + ICMPTimeout: C.ICMPTimeout, + Name: options.Name, + MTU: openconnecttransport.DefaultMTU, + Configuration: openconnecttransport.Configuration{ + MTU: openconnecttransport.DefaultMTU, + }, + }) + if err != nil { + return nil, err + } + openConnectEndpoint.device = device + device.SetPacketWriter(openConnectEndpoint.writePacketBuffers) + clientOptions, err := openConnectEndpoint.buildClientOptions(options, outboundDialer) + if err != nil { + return nil, err + } + client, err := openconnect.NewClient(clientOptions) + if err != nil { + return nil, err + } + openConnectEndpoint.client = client + success = true + return openConnectEndpoint, nil +} + +func (e *Endpoint) buildClientOptions(options option.OpenConnectEndpointOptions, outboundDialer N.Dialer) (openconnect.ClientOptions, error) { + certificateAuthority, err := materialSource("tls.certificate_authority", options.TLS.CertificateAuthority, options.TLS.CertificateAuthorityPath) + if err != nil { + return openconnect.ClientOptions{}, err + } + clientCertificate, err := materialSource("tls.client_certificate", options.TLS.ClientCertificate, options.TLS.ClientCertificatePath) + if err != nil { + return openconnect.ClientOptions{}, err + } + clientKey, err := materialSource("tls.client_key", options.TLS.ClientKey, options.TLS.ClientKeyPath) + if err != nil { + return openconnect.ClientOptions{}, err + } + mcaCertificate, err := materialSource("tls.mca_certificate", options.TLS.MCACertificate, options.TLS.MCACertificatePath) + if err != nil { + return openconnect.ClientOptions{}, err + } + mcaKey, err := materialSource("tls.mca_key", options.TLS.MCAKey, options.TLS.MCAKeyPath) + if err != nil { + return openconnect.ClientOptions{}, err + } + var tokenOptions *openconnect.TokenOptions + if options.Token != nil { + tokenOptions = &openconnect.TokenOptions{ + Mode: options.Token.Mode, + Secret: options.Token.Secret, + PIN: options.Token.PIN, + Password: options.Token.Password, + DeviceID: options.Token.DeviceID, + Counter: options.Token.Counter, + } + if tokenOptions.Mode == openconnect.TokenModeHOTP { + e.hotpCounter.Store(tokenOptions.Counter) + tokenOptions.UpdateCounter = func(_ context.Context, counter uint64) error { + e.hotpCounter.Store(counter) + return nil + } + } + } + var csdOptions *openconnect.CSDOptions + if options.CSD != nil { + csdOptions = &openconnect.CSDOptions{WrapperPath: options.CSD.WrapperPath} + } + var hipOptions *openconnect.HIPOptions + if options.HIP != nil { + hipOptions = &openconnect.HIPOptions{WrapperPath: options.HIP.WrapperPath} + } + var tnccOptions *openconnect.TNCCOptions + if options.TNCC != nil { + tnccCertificates := make([]openconnect.Material, 0, len(options.TNCC.Certificates)) + for i, certificateOptions := range options.TNCC.Certificates { + certificate, certificateErr := materialSource("tncc.certificates["+strconv.Itoa(i)+"].certificate", certificateOptions.Certificate, certificateOptions.CertificatePath) + if certificateErr != nil { + return openconnect.ClientOptions{}, certificateErr + } + tnccCertificates = append(tnccCertificates, certificate) + } + tnccOptions = &openconnect.TNCCOptions{ + WrapperPath: options.TNCC.WrapperPath, + DeviceID: options.TNCC.DeviceID, + UserAgent: options.TNCC.UserAgent, + MachineIdentificationEnabled: options.TNCC.MachineIdentificationEnabled, + Certificates: tnccCertificates, + } + } + formEntries := common.Map(options.FormEntries, func(entry option.OpenConnectFormEntryOptions) openconnect.FormEntry { + return openconnect.FormEntry{ + FormID: entry.FormID, + SubmissionKey: entry.SubmissionKey, + Name: entry.Name, + Value: entry.Value, + Promote: entry.Promote, + } + }) + return openconnect.ClientOptions{ + Context: e.loopContext, + Server: options.Server, + Flavor: options.Flavor, + Username: options.Username, + Password: options.Password, + AuthGroup: options.AuthGroup, + Token: tokenOptions, + ReportedOS: options.ReportedOS, + UserAgent: options.UserAgent, + CSD: csdOptions, + HIP: hipOptions, + TNCC: tnccOptions, + NoUDP: options.NoUDP, + AllowInsecureCrypto: options.AllowInsecureCrypto, + TLSConfig: openconnect.ClientTLSOptions{ + CertificateAuthority: certificateAuthority, + Certificate: clientCertificate, + Key: clientKey, + KeyPassword: options.TLS.ClientKeyPassword, + MCACertificate: mcaCertificate, + MCAKey: mcaKey, + MCAKeyPassword: options.TLS.MCAKeyPassword, + }, + FormEntries: formEntries, + Dialer: outboundDialer, + Logger: e.logger, + OnTunnelConfiguration: e.handleTunnelConfiguration, + }, nil +} + +func (e *Endpoint) handleTunnelConfiguration(event openconnect.TunnelConfigurationEvent) error { + configuration := configurationFromClientEvent(event) + defer e.notifyStatusUpdated() + e.stateAccess.Lock() + defer e.stateAccess.Unlock() + e.updateState(func(state *clientState) { + state.tunnelConfigured = false + }) + err := e.device.UpdateConfiguration(configuration) + if err != nil { + return E.Cause(err, "update device configuration") + } + if !e.deviceStarted { + err = e.device.Start() + if err != nil { + return E.Cause(err, "start device") + } + e.deviceStarted = true + } + routeSet, err := buildIPSet(configuration.Routes, configuration.ExcludedRoutes) + if err != nil { + return E.Cause(err, "build route set") + } + var ipv4Addresses []netip.Prefix + var ipv6Addresses []netip.Prefix + for _, address := range configuration.Addresses { + if address.Addr().Is4() { + ipv4Addresses = append(ipv4Addresses, address) + } else if address.Addr().Is6() { + ipv6Addresses = append(ipv6Addresses, address) + } + } + e.updateState(func(state *clientState) { + connectedSince := state.tunnelInfo.ConnectedSince + if event.Reason == openconnect.TunnelConfigurationEventInitial || + event.Reason == openconnect.TunnelConfigurationEventReestablishment || + connectedSince.IsZero() { + connectedSince = time.Now() + } + state.tunnelConfigured = true + state.localAddresses = configuration.Addresses + state.routeSet = routeSet + state.tunnelInfo = adapter.OpenConnectTunnelInfo{ + Server: e.server, + Flavor: e.flavor, + Transport: state.tunnelInfo.Transport, + IPv4: ipv4Addresses, + IPv6: ipv6Addresses, + DNS: configuration.DNS, + MTU: configuration.MTU, + ConnectedSince: connectedSince, + } + }) + return nil +} + +func (e *Endpoint) updateState(update func(state *clientState)) { + newState := *e.state.Load() + update(&newState) + e.state.Store(&newState) +} + +func (e *Endpoint) Start(stage adapter.StartStage) error { + if stage != adapter.StartStatePostStart { + return nil + } + err := e.client.Start() + if err != nil { + return err + } + e.stateAccess.Lock() + e.updateState(func(state *clientState) { + state.started = true + }) + e.readLoopDone = make(chan struct{}) + e.authFormLoopDone = make(chan struct{}) + e.activeTransportLoopDone = make(chan struct{}) + e.stateAccess.Unlock() + go e.readLoop() + go e.watchAuthForms() + go e.watchActiveTransport() + return nil +} + +func (e *Endpoint) readLoop() { + defer close(e.readLoopDone) + for { + packetBuffers, err := e.client.ReadDataPackets(e.loopContext) + if err != nil { + if E.IsClosedOrCanceled(err) || e.loopContext.Err() != nil { + return + } + e.logger.Error(E.Cause(err, "OpenConnect client terminated")) + e.setTerminalError(err) + return + } + err = e.device.WriteInboundBuffers(packetBuffers) + buf.ReleaseMulti(packetBuffers) + if err != nil { + e.logger.Error(E.Cause(err, "write OpenConnect packet to device")) + } + } +} + +func (e *Endpoint) Close() error { + e.stateAccess.Lock() + e.updateState(func(state *clientState) { + state.started = false + }) + readLoopDone := e.readLoopDone + authFormLoopDone := e.authFormLoopDone + activeTransportLoopDone := e.activeTransportLoopDone + e.stateAccess.Unlock() + e.cancelLoop() + err := E.Errors(e.client.Close(), e.device.Close()) + if readLoopDone != nil { + <-readLoopDone + } + if authFormLoopDone != nil { + <-authFormLoopDone + } + if activeTransportLoopDone != nil { + <-activeTransportLoopDone + } + e.notifyStatusUpdated() + return err +} + +func (e *Endpoint) PreMatchFlow(network string, destination netip.Addr) adapter.PreMatchAction { + return adapter.PreMatchFlow +} + +func (e *Endpoint) PortAddresses() (netip.Addr, netip.Addr) { + return e.device.PortAddresses() +} + +func (e *Endpoint) PortMTU() uint32 { + return e.device.PortMTU() +} + +func (e *Endpoint) AttachReturn(returnPath tun.Return) error { + return e.device.AttachReturn(returnPath) +} + +func (e *Endpoint) DetachReturn(returnPath tun.Return) error { + return e.device.DetachReturn(returnPath) +} + +func (e *Endpoint) JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) tun.FlowVerdict { + return judgeOpenConnectFlow(e.router, e.Tag(), e.Type(), e.state.Load().localAddresses, network, source, destination, firstPacket) +} + +func (e *Endpoint) ready() bool { + state := e.state.Load() + return state.started && state.tunnelConfigured +} + +func (e *Endpoint) WritePackets(packets [][]byte) error { + if !e.ready() { + return E.New("OpenConnect client is not ready yet") + } + err := e.client.WriteDataPackets(packets) + if E.IsMulti(err, openconnect.ErrDataChannelNotReady) { + return E.New("OpenConnect client is not ready yet") + } + return err +} + +func (e *Endpoint) writePacketBuffers(packetBuffers []*buf.Buffer) error { + if !e.ready() { + buf.ReleaseMulti(packetBuffers) + return nil + } + err := e.client.WriteDataPacketBuffers(packetBuffers) + if E.IsMulti(err, openconnect.ErrDataChannelNotReady) { + return nil + } + return err +} + +func (e *Endpoint) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + e.newConnection(ctx, e, e.state.Load().localAddresses, conn, source, destination, onClose) +} + +func (e *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + e.newPacketConnection(ctx, e, e.state.Load().localAddresses, conn, source, destination, onClose) +} + +func (e *Endpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + switch network { + case N.NetworkTCP: + e.logger.InfoContext(ctx, "outbound connection to ", destination) + case N.NetworkUDP: + e.logger.InfoContext(ctx, "outbound packet connection to ", destination) + } + if !e.ready() || !e.client.Ready() { + return nil, E.New("OpenConnect client is not ready yet") + } + if destination.IsDomain() { + destinationAddresses, err := e.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) + if err != nil { + return nil, err + } + return N.DialSerial(ctx, e.device, network, destination, destinationAddresses) + } + if !destination.Addr.IsValid() { + return nil, E.New("invalid destination: ", destination) + } + return e.device.DialContext(ctx, network, destination) +} + +func (e *Endpoint) ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) { + e.logger.InfoContext(ctx, "outbound packet connection to ", destination) + if !e.ready() || !e.client.Ready() { + return nil, netip.Addr{}, E.New("OpenConnect client is not ready yet") + } + if destination.IsDomain() { + destinationAddresses, err := e.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) + if err != nil { + return nil, netip.Addr{}, err + } + return N.ListenSerial(ctx, e.device, destination, destinationAddresses) + } + packetConn, err := e.device.ListenPacket(ctx, destination) + if err != nil { + return nil, netip.Addr{}, err + } + if destination.IsIP() { + return packetConn, destination.Addr, nil + } + return packetConn, netip.Addr{}, nil +} + +func (e *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + packetConn, destinationAddress, err := e.ListenPacketWithDestination(ctx, destination) + if err != nil { + return nil, err + } + if destinationAddress.IsValid() && destination != M.SocksaddrFrom(destinationAddress, destination.Port) { + return bufio.NewNATPacketConn(bufio.NewPacketConn(packetConn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil + } + return packetConn, nil +} + +func (e *Endpoint) PreferredDomain(metadata *adapter.InboundContext, domain string) bool { + return false +} + +func (e *Endpoint) PreferredAddress(metadata *adapter.InboundContext, address netip.Addr) bool { + state := e.state.Load() + if !state.started || !state.tunnelConfigured || state.routeSet == nil || !e.client.Ready() { + return false + } + return state.routeSet.Contains(address) +} diff --git a/protocol/openconnect/endpoint.go b/protocol/openconnect/endpoint.go new file mode 100644 index 000000000..62096692d --- /dev/null +++ b/protocol/openconnect/endpoint.go @@ -0,0 +1,158 @@ +package openconnect + +import ( + "context" + "net" + "net/netip" + "slices" + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/endpoint" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + openconnecttransport "github.com/sagernet/sing-box/transport/openconnect" + "github.com/sagernet/sing-openconnect" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/bufio" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "go4.org/netipx" +) + +func RegisterEndpoint(registry *endpoint.Registry) { + endpoint.Register[option.OpenConnectEndpointOptions](registry, C.TypeOpenConnect, NewEndpoint) +} + +type endpointBase struct { + endpoint.Adapter + router adapter.Router + logger log.ContextLogger +} + +func (e *endpointBase) SupportsFlow(network string) bool { + return slices.Contains(e.Network(), network) +} + +func (e *endpointBase) newConnection(ctx context.Context, endpoint adapter.Endpoint, localAddresses []netip.Prefix, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Inbound = endpoint.Tag() + metadata.InboundType = endpoint.Type() + metadata.Source = source + if isEndpointLocalAddress(localAddresses, destination.Addr) { + metadata.OriginDestination = destination + destination.Addr = loopbackAddressFor(destination.Addr) + } + metadata.Destination = destination + e.logger.InfoContext(ctx, "inbound connection from ", source) + e.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + e.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (e *endpointBase) newPacketConnection(ctx context.Context, endpoint adapter.Endpoint, localAddresses []netip.Prefix, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Inbound = endpoint.Tag() + metadata.InboundType = endpoint.Type() + metadata.Source = source + if isEndpointLocalAddress(localAddresses, destination.Addr) { + metadata.OriginDestination = destination + destination.Addr = loopbackAddressFor(destination.Addr) + conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, destination) + } + metadata.Destination = destination + e.logger.InfoContext(ctx, "inbound packet connection from ", source) + e.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + e.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} + +func isEndpointLocalAddress(localAddresses []netip.Prefix, address netip.Addr) bool { + for _, localPrefix := range localAddresses { + if address == localPrefix.Addr() { + return true + } + } + return false +} + +func loopbackAddressFor(address netip.Addr) netip.Addr { + if address.Is4() { + return netip.AddrFrom4([4]uint8{127, 0, 0, 1}) + } + return netip.IPv6Loopback() +} + +func judgeOpenConnectFlow(router adapter.Router, tag string, endpointType string, localAddresses []netip.Prefix, network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) tun.FlowVerdict { + for _, localPrefix := range localAddresses { + if destination.Addr() == localPrefix.Addr() { + return tun.FlowVerdict{Action: tun.ActionAccept} + } + } + return adapter.JudgeFlow(router, tag, endpointType, network, source, destination, firstPacket) +} + +func materialSource(name string, inlineValues []string, path string) (openconnect.Material, error) { + material := openconnect.Material{Path: path} + if len(inlineValues) > 0 { + material.Content = []byte(strings.Join(inlineValues, "\n")) + } + return material, material.Validate(name) +} + +func configurationFromClientEvent(event openconnect.TunnelConfigurationEvent) openconnecttransport.Configuration { + configuration := event.Configuration + mtu := configuration.MTU + if mtu == 0 { + mtu = openconnecttransport.DefaultMTU + } + routes := common.Map(configuration.Routes, func(route openconnect.TunnelRoute) openconnecttransport.Route { + return openconnecttransport.Route{ + Prefix: route.Prefix, + Gateway: route.Gateway, + Metric: route.Metric, + } + }) + excludedRoutes := common.Map(configuration.ExcludedRoutes, func(route openconnect.TunnelRoute) openconnecttransport.Route { + return openconnecttransport.Route{ + Prefix: route.Prefix, + Gateway: route.Gateway, + Metric: route.Metric, + } + }) + splitDNSRules := common.Map(configuration.SplitDNSRules, func(rule openconnect.TunnelSplitDNSRule) openconnecttransport.SplitDNSRule { + return openconnecttransport.SplitDNSRule{ + Domains: rule.Domains, + Servers: rule.Servers, + } + }) + return openconnecttransport.Configuration{ + MTU: mtu, + Addresses: configuration.Addresses, + Routes: routes, + ExcludedRoutes: excludedRoutes, + DNS: configuration.DNS, + NBNS: configuration.NBNS, + SearchDomains: configuration.SearchDomains, + SplitDNS: configuration.SplitDNS, + SplitDNSRules: splitDNSRules, + ProxyAutoConfigURL: configuration.ProxyAutoConfigURL, + Banner: configuration.Banner, + TunnelAllDNS: configuration.TunnelAllDNS, + ClientBypassProtocol: configuration.ClientBypassProtocol, + IdleTimeout: configuration.IdleTimeout, + AuthenticationExpiration: configuration.AuthenticationExpiration, + } +} + +func buildIPSet(routes []openconnecttransport.Route, excludedRoutes []openconnecttransport.Route) (*netipx.IPSet, error) { + var builder netipx.IPSetBuilder + for _, route := range routes { + builder.AddPrefix(route.Prefix) + } + for _, route := range excludedRoutes { + builder.RemovePrefix(route.Prefix) + } + return builder.IPSet() +} diff --git a/protocol/openconnect/status.go b/protocol/openconnect/status.go new file mode 100644 index 000000000..13eff5059 --- /dev/null +++ b/protocol/openconnect/status.go @@ -0,0 +1,135 @@ +package openconnect + +import ( + "slices" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-openconnect" + "github.com/sagernet/sing/common" +) + +var _ adapter.OpenConnectEndpoint = (*Endpoint)(nil) + +func (e *Endpoint) OpenConnectStatus() adapter.OpenConnectStatus { + var status adapter.OpenConnectStatus + clientState := e.state.Load() + authForm := e.client.PendingAuthForm() + e.statusAccess.Lock() + status.Error = e.terminalError + e.statusAccess.Unlock() + if authForm != nil { + fields := common.Map(authForm.Fields, func(field openconnect.AuthFormField) adapter.OpenConnectAuthFormField { + return adapter.OpenConnectAuthFormField{ + SubmissionKey: field.SubmissionKey, + Name: field.Name, + Label: field.Label, + Kind: field.Kind, + Value: field.Value, + Options: common.Map(field.Options, func(choice openconnect.AuthFormChoice) adapter.OpenConnectAuthFormChoice { + return adapter.OpenConnectAuthFormChoice{ + Value: choice.Value, + Label: choice.Label, + } + }), + } + }) + status.AuthForm = &adapter.OpenConnectAuthForm{ + ID: authForm.ID, + Banner: authForm.Banner, + Message: authForm.Message, + Error: authForm.Error, + URL: authForm.URL, + Fields: fields, + } + } + switch { + case status.AuthForm != nil: + status.State = adapter.OpenConnectStateAuthPending + case status.Error != "": + status.State = adapter.OpenConnectStateError + case clientState.started && clientState.tunnelConfigured && e.client.Ready(): + status.State = adapter.OpenConnectStateConnected + tunnelInfo := clientState.tunnelInfo + tunnelInfo.IPv4 = slices.Clone(tunnelInfo.IPv4) + tunnelInfo.IPv6 = slices.Clone(tunnelInfo.IPv6) + tunnelInfo.DNS = slices.Clone(tunnelInfo.DNS) + status.TunnelInfo = &tunnelInfo + default: + status.State = adapter.OpenConnectStateConnecting + } + return status +} + +func (e *Endpoint) StatusUpdated() <-chan struct{} { + e.statusAccess.Lock() + defer e.statusAccess.Unlock() + return e.statusUpdated +} + +func (e *Endpoint) CompleteAuthForm(formID string, values map[string]string) error { + return e.client.CompleteAuthForm(formID, values) +} + +func (e *Endpoint) CancelAuthForm(formID string) error { + return e.client.CancelAuthForm(formID) +} + +func (e *Endpoint) notifyStatusUpdated() { + e.statusAccess.Lock() + e.notifyStatusUpdatedLocked() + e.statusAccess.Unlock() +} + +func (e *Endpoint) notifyStatusUpdatedLocked() { + close(e.statusUpdated) + e.statusUpdated = make(chan struct{}) +} + +func (e *Endpoint) setTerminalError(err error) { + e.statusAccess.Lock() + e.terminalError = err.Error() + e.notifyStatusUpdatedLocked() + e.statusAccess.Unlock() +} + +func (e *Endpoint) watchAuthForms() { + defer close(e.authFormLoopDone) + var loggedAuthFormID string + for { + authFormUpdated := e.client.AuthFormUpdated() + authForm := e.client.PendingAuthForm() + if authForm != nil && authForm.ID != loggedAuthFormID { + loggedAuthFormID = authForm.ID + if authForm.URL != "" { + e.logger.Info("waiting for authentication: ", authForm.URL) + } else { + e.logger.Info("waiting for authentication") + } + } + e.notifyStatusUpdated() + select { + case <-e.loopContext.Done(): + return + case <-authFormUpdated: + } + } +} + +func (e *Endpoint) watchActiveTransport() { + defer close(e.activeTransportLoopDone) + for { + transportUpdated := e.client.ActiveTransportUpdated() + transport := e.client.ActiveTransport() + e.stateAccess.Lock() + e.updateState(func(state *clientState) { + state.tunnelInfo.Transport = transport + }) + e.stateAccess.Unlock() + e.notifyStatusUpdated() + select { + case <-e.loopContext.Done(): + return + case <-transportUpdated: + } + } +} diff --git a/protocol/openvpn/client.go b/protocol/openvpn/client.go new file mode 100644 index 000000000..25334535e --- /dev/null +++ b/protocol/openvpn/client.go @@ -0,0 +1,607 @@ +package openvpn + +import ( + "context" + "net" + "net/netip" + "sync" + "sync/atomic" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/endpoint" + "github.com/sagernet/sing-box/common/dialer" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + ovpntransport "github.com/sagernet/sing-box/transport/openvpn" + ovpn "github.com/sagernet/sing-openvpn" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun/gtcpip/header" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" + + "go4.org/netipx" +) + +var ( + _ adapter.OutboundWithPreferredRoutes = (*ClientEndpoint)(nil) + _ adapter.FlowOutbound = (*ClientEndpoint)(nil) + _ dialer.PacketDialerWithDestination = (*ClientEndpoint)(nil) + _ tun.Port = (*ClientEndpoint)(nil) +) + +type ClientEndpoint struct { + endpointBase + ctx context.Context + loopContext context.Context + cancelLoop context.CancelFunc + dnsRouter adapter.DNSRouter + outboundDialer N.Dialer + queryOptions adapter.DNSQueryOptions + client *ovpn.Client + device ovpntransport.Device + stateAccess sync.Mutex + state atomic.Pointer[clientState] + deviceStarted bool + readLoopDone chan struct{} + statusAccess sync.Mutex + statusUpdated chan struct{} + terminalError string + challengeLoopDone chan struct{} +} + +type clientState struct { + started bool + tunnelConfigured bool + localAddresses []netip.Prefix + routeSet *netipx.IPSet + blockIPv6 bool + tunnelInfo adapter.OpenVPNTunnelInfo +} + +func NewClientEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.OpenVPNClientEndpointOptions) (adapter.Endpoint, error) { + loopContext, cancelLoop := context.WithCancel(ctx) + clientEndpoint := &ClientEndpoint{ + endpointBase: endpointBase{ + Adapter: endpoint.NewAdapterWithDialerOptions(C.TypeOpenVPNClient, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, options.DialerOptions), + router: router, + logger: logger, + }, + ctx: ctx, + loopContext: loopContext, + cancelLoop: cancelLoop, + dnsRouter: service.FromContext[adapter.DNSRouter](ctx), + statusUpdated: make(chan struct{}), + } + success := false + defer func() { + if success { + return + } + if clientEndpoint.device != nil { + _ = clientEndpoint.device.Close() + } + cancelLoop() + }() + clientOptions, err := clientEndpoint.buildClientOptions(options) + if err != nil { + return nil, err + } + clientEndpoint.state.Store(&clientState{localAddresses: clientOptions.Tunnel.LocalAddress}) + outboundDialer, err := dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: options.DialerOptions, + RemoteIsDomain: openVPNClientRemoteIsDomain(options), + ResolverOnDetour: true, + NewDialer: true, + }) + if err != nil { + return nil, err + } + var queryOptions adapter.DNSQueryOptions + resolveDialer, isResolveDialer := outboundDialer.(dialer.ResolveDialer) + if isResolveDialer { + queryOptions = resolveDialer.QueryOptions() + } + clientEndpoint.outboundDialer = outboundDialer + clientEndpoint.queryOptions = queryOptions + udpTimeout := C.UDPTimeout + if options.UDPTimeout != 0 { + udpTimeout = time.Duration(options.UDPTimeout) + } + device, err := ovpntransport.NewDevice(ovpntransport.DeviceOptions{ + Context: ctx, + Logger: logger, + System: options.System, + Handler: clientEndpoint, + UDPTimeout: udpTimeout, + ICMPTimeout: C.ICMPTimeout, + Name: options.Name, + MTU: options.MTU, + Configuration: ovpntransport.Configuration{ + MTU: options.MTU, + Address: clientOptions.Tunnel.LocalAddress, + }, + }) + if err != nil { + return nil, err + } + clientEndpoint.device = device + device.SetPacketWriter(clientEndpoint.writePacketBuffers) + client, err := ovpn.NewClient(clientOptions) + if err != nil { + return nil, err + } + clientEndpoint.client = client + success = true + return clientEndpoint, nil +} + +func (c *ClientEndpoint) buildClientOptions(options option.OpenVPNClientEndpointOptions) (ovpn.ClientOptions, error) { + if options.TLS == nil { + return ovpn.ClientOptions{}, E.New("missing `tls` options") + } + if options.Server != "" && len(options.Servers) > 0 { + return ovpn.ClientOptions{}, E.New("`server` is conflict with `servers`") + } + if options.Server == "" && len(options.Servers) == 0 { + return ovpn.ClientOptions{}, E.New("missing `server` or `servers`") + } + certificateAuthority, err := materialSource("tls.certificate", options.TLS.Certificate, options.TLS.CertificatePath) + if err != nil { + return ovpn.ClientOptions{}, err + } + clientCertificate, err := materialSource("tls.client_certificate", options.TLS.ClientCertificate, options.TLS.ClientCertificatePath) + if err != nil { + return ovpn.ClientOptions{}, err + } + clientKey, err := materialSource("tls.client_key", options.TLS.ClientKey, options.TLS.ClientKeyPath) + if err != nil { + return ovpn.ClientOptions{}, err + } + keyDirection := -1 + var controlAuth ovpn.Material + var controlCrypt ovpn.Material + var controlCryptV2 ovpn.Material + controlWrap := options.TLS.ControlWrap + if controlWrap != nil && (controlWrap.Type != "" || len(controlWrap.Key) > 0 || controlWrap.KeyPath != "" || controlWrap.Direction != "") { + controlKey, controlErr := requiredMaterialSource("tls.control_wrap.key", controlWrap.Key, controlWrap.KeyPath) + if controlErr != nil { + return ovpn.ClientOptions{}, controlErr + } + switch controlWrap.Type { + case "tls_auth": + keyDirection, err = keyDirectionValue(controlWrap.Direction) + if err != nil { + return ovpn.ClientOptions{}, err + } + controlAuth = controlKey + case "tls_crypt": + if controlWrap.Direction != "" { + return ovpn.ClientOptions{}, E.New("`tls.control_wrap.direction` is only supported by `tls_auth`") + } + controlCrypt = controlKey + case "tls_crypt_v2": + if controlWrap.Direction != "" { + return ovpn.ClientOptions{}, E.New("`tls.control_wrap.direction` is only supported by `tls_auth`") + } + controlCryptV2 = controlKey + case "": + return ovpn.ClientOptions{}, E.New("missing OpenVPN control wrap type") + default: + return ovpn.ClientOptions{}, E.New("unknown OpenVPN control wrap type: ", controlWrap.Type) + } + } + protocol := options.Network + if protocol == "" { + protocol = N.NetworkUDP + } + var remotes []ovpn.Remote + if options.Server != "" { + remotes = append(remotes, ovpn.Remote{ + Host: options.Server, + Port: options.ServerPort, + Protocol: protocol, + }) + } else { + remotes = make([]ovpn.Remote, 0, len(options.Servers)) + for _, remoteOptions := range options.Servers { + remoteProtocol := remoteOptions.Network + if remoteProtocol == "" { + remoteProtocol = protocol + } + remotes = append(remotes, ovpn.Remote{ + Host: remoteOptions.Server, + Port: remoteOptions.ServerPort, + Protocol: remoteProtocol, + }) + } + } + pullFilters := common.Map(options.PullFilters, func(filterOptions option.OpenVPNPullFilterOptions) ovpn.PullFilter { + return ovpn.PullFilter{ + Action: filterOptions.Action, + Text: filterOptions.Text, + } + }) + tunnelRoutes := common.Map(options.Routes, func(route netip.Prefix) ovpn.TunnelRoute { + return ovpn.TunnelRoute{Prefix: route} + }) + clientTLSOptions := ovpn.ClientTLSOptions{ + CertificateAuthority: certificateAuthority, + Certificate: clientCertificate, + Key: clientKey, + Auth: controlAuth, + Crypt: controlCrypt, + CryptV2: controlCryptV2, + VerifyX509Type: options.TLS.ServerNameType, + PeerFingerprint: options.TLS.PeerFingerprint, + CRLVerify: options.TLS.CRLPath, + RemoteCertificateKU: options.TLS.RemoteCertificateKU, + RemoteCertificateEKU: options.TLS.RemoteCertificateEKU, + RemoteCertificateTLS: "server", + VersionMin: options.TLS.VersionMin, + VersionMax: options.TLS.VersionMax, + Cipher: options.TLS.Cipher, + Groups: options.TLS.Groups, + } + if options.TLS.ServerName != "" { + clientTLSOptions.VerifyX509Name = options.TLS.ServerName + if options.TLS.ServerNameType == "" { + clientTLSOptions.VerifyX509Type = "name" + } + } + return ovpn.ClientOptions{ + Context: c.loopContext, + Mode: ovpn.ModeTLS, + Transport: ovpn.ClientTransportOptions{ + Remotes: remotes, + RemoteRandom: options.RemoteRandom, + Protocol: protocol, + ExplicitExitNotify: options.ExplicitExitNotify, + DialContext: c.transportDialContext, + }, + DataChannel: ovpn.ClientDataChannelOptions{ + MTU: options.MTU, + MSSFix: options.MSSFix, + Fragment: options.Fragment, + Ciphers: options.DataCiphers, + FallbackCipher: options.DataCiphersFallback, + Auth: options.Auth, + Compression: options.Compression, + CompressionLZO: options.CompressionLZO, + AllowCompression: options.AllowCompression, + PacketHeadroom: ovpntransport.PacketHeadroom, + }, + TLS: clientTLSOptions, + Authentication: ovpn.ClientAuthenticationOptions{ + Username: options.Username, + Password: options.Password, + AuthRetry: options.AuthRetry, + StaticChallenge: options.StaticChallenge, + StaticChallengeEcho: options.StaticChallengeEcho, + }, + Pull: ovpn.ClientPullOptions{ + Enabled: true, + Filters: pullFilters, + RouteNoPull: options.RouteNoPull, + }, + Tunnel: ovpn.ClientTunnelOptions{ + DevType: "tun", + RedirectGateway: options.RedirectGateway, + RedirectGatewayFlags: options.RedirectGatewayFlags, + RouteMetric: options.RouteMetric, + RouteGateway: options.RouteGateway.Build(netip.Addr{}), + Routes: tunnelRoutes, + }, + Timing: ovpn.ClientTimingOptions{ + RenegotiationInterval: time.Duration(options.RenegotiateInterval), + PingInterval: time.Duration(options.KeepaliveInterval), + PingRestart: time.Duration(options.KeepaliveTimeout), + }, + KeyDirection: keyDirection, + OnTunnelConfiguration: c.handleTunnelConfiguration, + Logger: c.logger, + }, nil +} + +func (c *ClientEndpoint) transportDialContext(ctx context.Context, network string, address string) (net.Conn, error) { + destination := M.ParseSocksaddr(address) + var ( + connection net.Conn + err error + ) + if destination.IsDomain() { + destinationAddresses, lookupErr := c.dnsRouter.Lookup(ctx, destination.Fqdn, c.queryOptions) + if lookupErr != nil { + return nil, lookupErr + } + connection, err = N.DialSerial(ctx, c.outboundDialer, network, destination, destinationAddresses) + } else { + connection, err = c.outboundDialer.DialContext(ctx, network, destination) + } + if err != nil { + return nil, err + } + if N.NetworkName(network) == N.NetworkUDP { + tuneOpenVPNUDPSocket(connection) + } + c.stateAccess.Lock() + c.updateState(func(state *clientState) { + state.tunnelInfo.Server = address + state.tunnelInfo.Network = N.NetworkName(network) + }) + c.stateAccess.Unlock() + return connection, nil +} + +func (c *ClientEndpoint) handleTunnelConfiguration(event ovpn.TunnelConfigurationEvent) error { + configuration := configurationFromClientEvent(event, c.logger) + defer c.notifyStatusUpdated() + c.stateAccess.Lock() + defer c.stateAccess.Unlock() + c.updateState(func(state *clientState) { + state.tunnelConfigured = false + }) + err := c.device.UpdateConfiguration(configuration) + if err != nil { + return E.Cause(err, "update device configuration") + } + if !c.deviceStarted { + err = c.device.Start() + if err != nil { + return E.Cause(err, "start device") + } + c.deviceStarted = true + } + routeSet, err := buildIPSet(configuration.Routes) + if err != nil { + return E.Cause(err, "build route set") + } + c.updateState(func(state *clientState) { + state.tunnelConfigured = true + state.localAddresses = configuration.Address + state.routeSet = routeSet + state.blockIPv6 = configuration.BlockIPv6 + state.tunnelInfo.Cipher = event.Configuration.SelectedCipher + state.tunnelInfo.IPv4 = event.Configuration.LocalIPv4 + state.tunnelInfo.IPv6 = event.Configuration.LocalIPv6 + state.tunnelInfo.DNS = event.Configuration.DNS + state.tunnelInfo.MTU = configuration.MTU + if event.Reason == ovpn.TunnelConfigurationEventInitial || state.tunnelInfo.ConnectedSince.IsZero() { + state.tunnelInfo.ConnectedSince = time.Now() + } + }) + return nil +} + +func (c *ClientEndpoint) updateState(update func(state *clientState)) { + newState := *c.state.Load() + update(&newState) + c.state.Store(&newState) +} + +func (c *ClientEndpoint) Start(stage adapter.StartStage) error { + if stage != adapter.StartStatePostStart { + return nil + } + err := c.client.Start() + if err != nil { + return err + } + c.stateAccess.Lock() + c.updateState(func(state *clientState) { + state.started = true + }) + c.readLoopDone = make(chan struct{}) + c.challengeLoopDone = make(chan struct{}) + c.stateAccess.Unlock() + go c.readLoop() + go c.watchChallenges() + return nil +} + +func (c *ClientEndpoint) readLoop() { + defer close(c.readLoopDone) + for { + packetBuffers, err := c.client.ReadDataPackets(c.loopContext) + if err != nil { + if E.IsClosedOrCanceled(err) || c.loopContext.Err() != nil { + return + } + c.logger.Error(E.Cause(err, "OpenVPN client terminated")) + c.setTerminalError(err) + return + } + err = c.device.WriteInboundBuffers(packetBuffers) + buf.ReleaseMulti(packetBuffers) + if err != nil { + c.logger.Error(E.Cause(err, "write OpenVPN packet to device")) + } + } +} + +func (c *ClientEndpoint) Close() error { + c.stateAccess.Lock() + c.updateState(func(state *clientState) { + state.started = false + }) + readLoopDone := c.readLoopDone + challengeLoopDone := c.challengeLoopDone + c.stateAccess.Unlock() + c.cancelLoop() + err := E.Errors(c.client.Close(), c.device.Close()) + if readLoopDone != nil { + <-readLoopDone + } + if challengeLoopDone != nil { + <-challengeLoopDone + } + c.notifyStatusUpdated() + return err +} + +func (c *ClientEndpoint) PreMatchFlow(network string, destination netip.Addr) adapter.PreMatchAction { + return adapter.PreMatchFlow +} + +func (c *ClientEndpoint) PortAddresses() (netip.Addr, netip.Addr) { + return c.device.PortAddresses() +} + +func (c *ClientEndpoint) PortMTU() uint32 { + return c.device.PortMTU() +} + +func (c *ClientEndpoint) AttachReturn(returnPath tun.Return) error { + return c.device.AttachReturn(returnPath) +} + +func (c *ClientEndpoint) DetachReturn(returnPath tun.Return) error { + return c.device.DetachReturn(returnPath) +} + +func (c *ClientEndpoint) JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) tun.FlowVerdict { + return judgeOpenVPNFlow(c.router, c.Tag(), c.Type(), c.state.Load().localAddresses, network, source, destination, firstPacket) +} + +func (c *ClientEndpoint) ready() bool { + state := c.state.Load() + return state.started && state.tunnelConfigured +} + +func (c *ClientEndpoint) WritePackets(packets [][]byte) error { + state := c.state.Load() + if !state.started || !state.tunnelConfigured { + return E.New("OpenVPN client is not ready yet") + } + if state.blockIPv6 { + outboundPackets := packets[:0] + for _, packet := range packets { + if header.IPVersion(packet) != header.IPv6Version { + outboundPackets = append(outboundPackets, packet) + } + } + packets = outboundPackets + if len(packets) == 0 { + return nil + } + } + packetBuffers := make([]*buf.Buffer, len(packets)) + for i, packet := range packets { + packetBuffers[i] = buf.As(packet) + } + err := c.client.WriteDataPacketBuffers(packetBuffers) + if E.IsMulti(err, ovpn.ErrDataChannelNotReady) { + return E.New("OpenVPN client is not ready yet") + } + return err +} + +func (c *ClientEndpoint) writePacketBuffers(packetBuffers []*buf.Buffer) error { + state := c.state.Load() + if !state.started || !state.tunnelConfigured { + buf.ReleaseMulti(packetBuffers) + return nil + } + if state.blockIPv6 { + outboundBuffers := packetBuffers[:0] + for _, packetBuffer := range packetBuffers { + if header.IPVersion(packetBuffer.Bytes()) == header.IPv6Version { + packetBuffer.Release() + continue + } + outboundBuffers = append(outboundBuffers, packetBuffer) + } + packetBuffers = outboundBuffers + if len(packetBuffers) == 0 { + return nil + } + } + err := c.client.WriteDataPacketBuffers(packetBuffers) + if E.IsMulti(err, ovpn.ErrDataChannelNotReady) { + return nil + } + return err +} + +func (c *ClientEndpoint) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + c.newConnection(ctx, c, c.state.Load().localAddresses, conn, source, destination, onClose) +} + +func (c *ClientEndpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + c.newPacketConnection(ctx, c, c.state.Load().localAddresses, conn, source, destination, onClose) +} + +func (c *ClientEndpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + switch network { + case N.NetworkTCP: + c.logger.InfoContext(ctx, "outbound connection to ", destination) + case N.NetworkUDP: + c.logger.InfoContext(ctx, "outbound packet connection to ", destination) + } + if !c.ready() || !c.client.Ready() { + return nil, E.New("OpenVPN client is not ready yet") + } + if destination.IsDomain() { + destinationAddresses, err := c.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) + if err != nil { + return nil, err + } + return N.DialSerial(ctx, c.device, network, destination, destinationAddresses) + } + if !destination.Addr.IsValid() { + return nil, E.New("invalid destination: ", destination) + } + return c.device.DialContext(ctx, network, destination) +} + +func (c *ClientEndpoint) ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) { + c.logger.InfoContext(ctx, "outbound packet connection to ", destination) + if !c.ready() || !c.client.Ready() { + return nil, netip.Addr{}, E.New("OpenVPN client is not ready yet") + } + if destination.IsDomain() { + destinationAddresses, err := c.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) + if err != nil { + return nil, netip.Addr{}, err + } + return N.ListenSerial(ctx, c.device, destination, destinationAddresses) + } + packetConn, err := c.device.ListenPacket(ctx, destination) + if err != nil { + return nil, netip.Addr{}, err + } + if destination.IsIP() { + return packetConn, destination.Addr, nil + } + return packetConn, netip.Addr{}, nil +} + +func (c *ClientEndpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + packetConn, destinationAddress, err := c.ListenPacketWithDestination(ctx, destination) + if err != nil { + return nil, err + } + if destinationAddress.IsValid() && destination != M.SocksaddrFrom(destinationAddress, destination.Port) { + return bufio.NewNATPacketConn(bufio.NewPacketConn(packetConn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil + } + return packetConn, nil +} + +func (c *ClientEndpoint) PreferredDomain(metadata *adapter.InboundContext, domain string) bool { + return false +} + +func (c *ClientEndpoint) PreferredAddress(metadata *adapter.InboundContext, address netip.Addr) bool { + state := c.state.Load() + if !state.started || !state.tunnelConfigured || state.routeSet == nil || !c.client.Ready() { + return false + } + return state.routeSet.Contains(address) +} diff --git a/protocol/openvpn/endpoint.go b/protocol/openvpn/endpoint.go new file mode 100644 index 000000000..4728ddc23 --- /dev/null +++ b/protocol/openvpn/endpoint.go @@ -0,0 +1,277 @@ +package openvpn + +import ( + "context" + "crypto/subtle" + "net" + "net/netip" + "slices" + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/endpoint" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + ovpntransport "github.com/sagernet/sing-box/transport/openvpn" + ovpn "github.com/sagernet/sing-openvpn" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun/gtcpip/header" + "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "go4.org/netipx" +) + +func RegisterEndpoint(registry *endpoint.Registry) { + endpoint.Register[option.OpenVPNClientEndpointOptions](registry, C.TypeOpenVPNClient, NewClientEndpoint) + endpoint.Register[option.OpenVPNServerEndpointOptions](registry, C.TypeOpenVPNServer, NewServerEndpoint) +} + +type endpointBase struct { + endpoint.Adapter + router adapter.Router + logger log.ContextLogger +} + +func (e *endpointBase) SupportsFlow(network string) bool { + return slices.Contains(e.Network(), network) +} + +func (e *endpointBase) newConnection(ctx context.Context, endpoint adapter.Endpoint, localAddresses []netip.Prefix, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Inbound = endpoint.Tag() + metadata.InboundType = endpoint.Type() + metadata.Source = source + if isEndpointLocalAddress(localAddresses, destination.Addr) { + metadata.OriginDestination = destination + destination.Addr = loopbackAddressFor(destination.Addr) + } + metadata.Destination = destination + e.logger.InfoContext(ctx, "inbound connection from ", source) + e.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + e.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (e *endpointBase) newPacketConnection(ctx context.Context, endpoint adapter.Endpoint, localAddresses []netip.Prefix, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Inbound = endpoint.Tag() + metadata.InboundType = endpoint.Type() + metadata.Source = source + if isEndpointLocalAddress(localAddresses, destination.Addr) { + metadata.OriginDestination = destination + destination.Addr = loopbackAddressFor(destination.Addr) + conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, destination) + } + metadata.Destination = destination + e.logger.InfoContext(ctx, "inbound packet connection from ", source) + e.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + e.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} + +func isEndpointLocalAddress(localAddresses []netip.Prefix, address netip.Addr) bool { + for _, localPrefix := range localAddresses { + if address == localPrefix.Addr() { + return true + } + } + return false +} + +func loopbackAddressFor(address netip.Addr) netip.Addr { + if address.Is4() { + return netip.AddrFrom4([4]uint8{127, 0, 0, 1}) + } + return netip.IPv6Loopback() +} + +func judgeOpenVPNFlow(router adapter.Router, tag string, endpointType string, localAddresses []netip.Prefix, network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) tun.FlowVerdict { + for _, localPrefix := range localAddresses { + if destination.Addr() == localPrefix.Addr() { + return tun.FlowVerdict{Action: tun.ActionAccept} + } + } + return adapter.JudgeFlow(router, tag, endpointType, network, source, destination, firstPacket) +} + +func keyDirectionValue(direction string) (int, error) { + switch direction { + case "": + return -1, nil + case "server": + return 0, nil + case "client": + return 1, nil + default: + return 0, E.New("unsupported OpenVPN key direction: ", direction, " (expected \"server\" or \"client\")") + } +} + +func openVPNClientRemoteIsDomain(options option.OpenVPNClientEndpointOptions) bool { + if options.Server != "" && options.ServerIsDomain() { + return true + } + for _, remoteOptions := range options.Servers { + if remoteOptions.Build().IsDomain() { + return true + } + } + return false +} + +func materialSource(name string, inlineValues []string, path string) (ovpn.Material, error) { + material := ovpn.Material{Path: path} + if len(inlineValues) > 0 { + material.Content = []byte(strings.Join(inlineValues, "\n")) + } + return material, material.Validate(name) +} + +func requiredMaterialSource(name string, inlineValues []string, path string) (ovpn.Material, error) { + material, err := materialSource(name, inlineValues, path) + if err != nil { + return ovpn.Material{}, err + } + if !material.IsSet() { + return ovpn.Material{}, E.New("missing `", name, "` or `", name, "_path`") + } + return material, nil +} + +func configurationFromClientEvent(event ovpn.TunnelConfigurationEvent, logger log.ContextLogger) ovpntransport.Configuration { + configuration := event.Configuration + var addresses []netip.Prefix + addresses = append(addresses, configuration.LocalIPv4...) + addresses = append(addresses, configuration.LocalIPv6...) + mtu := configuration.TunMTU + if mtu == 0 { + mtu = ovpntransport.DefaultMTU + } + var routes []ovpntransport.Route + inet4DefaultRoute := netip.PrefixFrom(netip.IPv4Unspecified(), 0) + inet6DefaultRoute := netip.PrefixFrom(netip.IPv6Unspecified(), 0) + var hasInet4DefaultRoute bool + var hasInet6DefaultRoute bool + for _, route := range configuration.IPv4Routes { + routes = append(routes, ovpntransport.Route{ + Prefix: route.Prefix, + Gateway: route.Gateway, + Metric: route.Metric, + }) + if route.Prefix == inet4DefaultRoute { + hasInet4DefaultRoute = true + } + } + for _, route := range configuration.IPv6Routes { + routes = append(routes, ovpntransport.Route{ + Prefix: route.Prefix, + Gateway: route.Gateway, + Metric: route.Metric, + }) + if route.Prefix == inet6DefaultRoute { + hasInet6DefaultRoute = true + } + } + if configuration.RedirectGateway { + if !hasOpenVPNFlag(configuration.RedirectGatewayFlags, "!ipv4") && !hasInet4DefaultRoute { + routes = append(routes, ovpntransport.Route{ + Prefix: inet4DefaultRoute, + Gateway: configuration.VPNGateway, + Metric: configuration.RouteMetric, + }) + } + if hasOpenVPNFlag(configuration.RedirectGatewayFlags, "ipv6") && !hasInet6DefaultRoute { + routes = append(routes, ovpntransport.Route{ + Prefix: inet6DefaultRoute, + Gateway: configuration.VPNGatewayIPv6, + Metric: configuration.RouteMetric, + }) + hasInet6DefaultRoute = true + } + } + if configuration.BlockIPv6 && !hasInet6DefaultRoute { + routes = append(routes, ovpntransport.Route{ + Prefix: inet6DefaultRoute, + Gateway: configuration.VPNGatewayIPv6, + Metric: configuration.RouteMetric, + }) + } + var ignoredOptions []string + for _, flag := range configuration.RedirectGatewayFlags { + switch strings.ToLower(flag) { + case "!ipv4", "ipv6": + default: + if flag != "" { + ignoredOptions = append(ignoredOptions, "redirect-gateway "+flag) + } + } + } + if configuration.RedirectPrivate { + ignoredOptions = append(ignoredOptions, "redirect-private") + } + if configuration.BlockOutsideDNS { + ignoredOptions = append(ignoredOptions, "block-outside-dns") + } + for _, dhcpOption := range configuration.DHCPOptions { + fields := strings.Fields(dhcpOption) + if len(fields) == 0 || strings.EqualFold(fields[0], "DNS") || strings.EqualFold(fields[0], "DNS6") { + continue + } + ignoredOptions = append(ignoredOptions, "dhcp-option "+strings.TrimSpace(dhcpOption)) + } + if len(ignoredOptions) > 0 && logger != nil { + logger.Debug("ignored pushed OpenVPN options: ", strings.Join(ignoredOptions, ", ")) + } + return ovpntransport.Configuration{ + MTU: mtu, + Address: addresses, + Routes: routes, + DNS: configuration.DNS, + Topology: configuration.Topology, + BlockIPv6: configuration.BlockIPv6, + } +} + +func buildIPSet(routes []ovpntransport.Route) (*netipx.IPSet, error) { + var builder netipx.IPSetBuilder + for _, route := range routes { + builder.AddPrefix(route.Prefix) + } + return builder.IPSet() +} + +func hasOpenVPNFlag(flags []string, flag string) bool { + for _, value := range flags { + if strings.EqualFold(value, flag) { + return true + } + } + return false +} + +func packetSourceAddress(packet []byte, inet4Address netip.Addr, inet6Address netip.Addr) netip.Addr { + if header.IPVersion(packet) == header.IPv6Version { + return inet6Address + } + return inet4Address +} + +func authenticatorFromUsers(users []auth.User) ovpn.UserPassAuthenticator { + if len(users) == 0 { + return nil + } + passwordByUsername := make(map[string]string, len(users)) + for _, user := range users { + passwordByUsername[user.Username] = user.Password + } + return func(ctx context.Context, username string, password string) error { + expectedPassword, found := passwordByUsername[username] + if !found || subtle.ConstantTimeCompare([]byte(expectedPassword), []byte(password)) != 1 { + return E.New("invalid username or password") + } + return nil + } +} diff --git a/protocol/openvpn/server.go b/protocol/openvpn/server.go new file mode 100644 index 000000000..98138d2ca --- /dev/null +++ b/protocol/openvpn/server.go @@ -0,0 +1,571 @@ +package openvpn + +import ( + "context" + "net" + "net/netip" + "slices" + "sync/atomic" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/endpoint" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/listener" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + ovpntransport "github.com/sagernet/sing-box/transport/openvpn" + ovpn "github.com/sagernet/sing-openvpn" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" +) + +var ( + _ adapter.FlowOutbound = (*ServerEndpoint)(nil) + _ dialer.PacketDialerWithDestination = (*ServerEndpoint)(nil) +) + +type ServerEndpoint struct { + endpointBase + ctx context.Context + loopContext context.Context + cancelLoop context.CancelFunc + options option.OpenVPNServerEndpointOptions + serverOptions ovpn.ServerOptions + dnsRouter adapter.DNSRouter + listener *listener.Listener + server *ovpn.Server + device ovpntransport.Device + localAddresses []netip.Prefix + started atomic.Bool + readLoopDone chan struct{} +} + +type udpEgressPacketConn struct { + *tun.UDPEgressConn +} + +func (c *udpEgressPacketConn) ReadFrom(buffer []byte) (int, net.Addr, error) { + dataLength, source, err := c.ReadFromUDPAddrPort(buffer) + if err != nil { + return 0, nil, err + } + return dataLength, net.UDPAddrFromAddrPort(source), nil +} + +func (c *udpEgressPacketConn) WriteTo(buffer []byte, destination net.Addr) (int, error) { + destinationAddress := M.SocksaddrFromNet(destination) + if !destinationAddress.IsIP() { + return 0, E.New("invalid UDP destination: ", destination) + } + return c.WriteToUDPAddrPort(buffer, destinationAddress.AddrPort()) +} + +func NewServerEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.OpenVPNServerEndpointOptions) (adapter.Endpoint, error) { + if options.MTU == 0 { + options.MTU = ovpntransport.DefaultMTU + } + loopContext, cancelLoop := context.WithCancel(ctx) + serverEndpoint := &ServerEndpoint{ + endpointBase: endpointBase{ + Adapter: endpoint.NewAdapter(C.TypeOpenVPNServer, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, nil), + router: router, + logger: logger, + }, + ctx: ctx, + loopContext: loopContext, + cancelLoop: cancelLoop, + options: options, + dnsRouter: service.FromContext[adapter.DNSRouter](ctx), + localAddresses: options.Address, + } + serverOptions, err := buildServerOptions(options) + if err != nil { + cancelLoop() + return nil, err + } + serverOptions.Context = loopContext + serverOptions.Authentication.Authenticator = authenticatorFromUsers(options.Users) + serverOptions.Logger = logger + serverEndpoint.serverOptions = serverOptions + udpTimeout := C.UDPTimeout + if options.UDPTimeout != 0 { + udpTimeout = time.Duration(options.UDPTimeout) + } + deviceRoutes := make([]ovpntransport.Route, 0, len(options.Address)) + for _, prefix := range options.Address { + deviceRoutes = append(deviceRoutes, ovpntransport.Route{Prefix: prefix.Masked()}) + } + device, err := ovpntransport.NewDevice(ovpntransport.DeviceOptions{ + Context: ctx, + Logger: logger, + System: options.System, + Handler: serverEndpoint, + UDPTimeout: udpTimeout, + ICMPTimeout: C.ICMPTimeout, + Name: options.Name, + MTU: options.MTU, + Configuration: ovpntransport.Configuration{ + MTU: options.MTU, + Address: options.Address, + Routes: deviceRoutes, + Topology: options.Topology, + }, + }) + if err != nil { + cancelLoop() + return nil, err + } + serverEndpoint.device = device + device.SetPacketWriter(serverEndpoint.writePacketBuffersByDestination) + return serverEndpoint, nil +} + +func validateServerAddresses(addresses []netip.Prefix) error { + var hasIPv4 bool + var hasIPv6 bool + for _, prefix := range addresses { + if prefix.Addr().Is4() { + if hasIPv4 { + return E.New("multiple IPv4 OpenVPN server address pools are not supported") + } + hasIPv4 = true + } else { + if hasIPv6 { + return E.New("multiple IPv6 OpenVPN server address pools are not supported") + } + hasIPv6 = true + } + } + return nil +} + +func validateServerTopology(topology string) error { + switch topology { + case "", "subnet", "p2p", "net30": + return nil + default: + return E.New("invalid OpenVPN topology ", topology, ", allowed values: subnet, p2p, net30") + } +} + +func (s *ServerEndpoint) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + protocol := s.serverOptions.Transport.Protocol + s.listener = listener.New(listener.Options{ + Context: s.ctx, + Logger: s.logger, + Network: []string{protocol}, + Listen: s.options.ListenOptions, + }) + var ( + streamListener net.Listener + packetConn net.PacketConn + err error + ) + if protocol == N.NetworkTCP { + streamListener, err = s.listener.ListenTCP() + } else { + var listenConfig net.ListenConfig + var egressEnabled bool + listenAddress := s.options.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})) + if listenAddress.IsUnspecified() && s.options.BindInterface == "" && s.options.RoutingMark == 0 && s.options.NetNs == "" { + udpDialer, dialerErr := dialer.NewDefault(s.ctx, option.DialerOptions{ + ReuseAddr: s.options.ReuseAddr, + UDPFragment: s.options.UDPFragment, + UDPFragmentDefault: s.options.UDPFragmentDefault, + }) + if dialerErr != nil { + return dialerErr + } + listenConfig.Control, egressEnabled = udpDialer.UDPListenerControl() + } + packetConn, err = s.listener.ListenUDPWithConfig(listenConfig) + if err == nil { + tuneOpenVPNUDPSocket(packetConn) + if egressEnabled { + udpConn := packetConn.(*net.UDPConn) + networkManager := service.FromContext[adapter.NetworkManager](s.ctx) + egressPool := tun.NewUDPEgressPool(tun.UDPEgressPoolOptions{ + Logger: s.logger, + Network: M.NetworkFromNetAddr(N.NetworkUDP, listenAddress), + Control: listenConfig.Control, + InterfaceFinder: networkManager.InterfaceFinder(), + InterfaceMonitor: networkManager.InterfaceMonitor(), + ExcludeInterface: s.options.Name, + IsExempt: func() bool { + return networkManager.AutoRedirectOutputMark() != 0 + }, + }) + listenPort := udpConn.LocalAddr().(*net.UDPAddr).AddrPort().Port() + if egressPool.SetEgressPort(listenPort) { + packetConn = &udpEgressPacketConn{tun.NewUDPEgressConn(udpConn, egressPool)} + } else { + egressPool.Close() + } + } + } + } + if err != nil { + return err + } + serverOptions := s.serverOptions + if streamListener != nil { + serverOptions.Transport.ListenAddress = streamListener.Addr().String() + } else if packetConn != nil { + serverOptions.Transport.ListenAddress = packetConn.LocalAddr().String() + } + serverOptions.Transport.Listener = streamListener + serverOptions.Transport.PacketConn = packetConn + server, err := ovpn.NewServer(serverOptions) + if err != nil { + if packetConn != nil { + _ = packetConn.Close() + } + s.listener.Close() + return err + } + s.server = server + err = s.device.Start() + if err != nil { + s.listener.Close() + server.Close() + return err + } + err = server.Start() + if err != nil { + s.device.Close() + s.listener.Close() + server.Close() + return err + } + s.started.Store(true) + s.readLoopDone = make(chan struct{}) + go s.readLoop() + return nil +} + +func buildServerOptions(options option.OpenVPNServerEndpointOptions) (ovpn.ServerOptions, error) { + if len(options.Address) == 0 { + return ovpn.ServerOptions{}, E.New("missing OpenVPN server address") + } + if options.TLS == nil { + return ovpn.ServerOptions{}, E.New("missing `tls` options") + } + err := validateServerAddresses(options.Address) + if err != nil { + return ovpn.ServerOptions{}, err + } + err = validateServerTopology(options.Topology) + if err != nil { + return ovpn.ServerOptions{}, err + } + protocol := options.Network + if protocol == "" { + protocol = N.NetworkUDP + } + switch protocol { + case N.NetworkTCP, N.NetworkUDP: + default: + return ovpn.ServerOptions{}, E.New("unsupported OpenVPN network: ", protocol) + } + tlsOptions, keyDirection, err := buildServerTLSOptions(*options.TLS) + if err != nil { + return ovpn.ServerOptions{}, err + } + serverOptions := ovpn.ServerOptions{ + Mode: ovpn.ModeTLS, + KeyDirection: keyDirection, + Transport: ovpn.ServerTransportOptions{ + Protocol: protocol, + }, + Resources: ovpn.ServerResourceOptions{ + MaxClients: options.MaxClients, + }, + DataChannel: ovpn.ServerDataChannelOptions{ + MTU: options.MTU, + Ciphers: []string(options.DataCiphers), + FallbackCipher: options.DataCiphersFallback, + Auth: options.Auth, + PacketHeadroom: ovpntransport.PacketHeadroom, + }, + TLS: tlsOptions, + Timing: ovpn.ServerTimingOptions{ + RenegotiationInterval: time.Duration(options.RenegotiateInterval), + }, + } + applyServerPushOptions(&serverOptions, options) + return serverOptions, nil +} + +func buildServerTLSOptions(options option.OpenVPNInboundTLSOptions) (ovpn.ServerTLSOptions, int, error) { + switch options.VerifyClientCertificate { + case "", "require", "optional", "none": + default: + return ovpn.ServerTLSOptions{}, 0, E.New("invalid OpenVPN client certificate policy ", options.VerifyClientCertificate, ", allowed values: require, optional, none") + } + certificate, err := requiredMaterialSource("tls.certificate", options.Certificate, options.CertificatePath) + if err != nil { + return ovpn.ServerTLSOptions{}, 0, err + } + key, err := requiredMaterialSource("tls.key", options.Key, options.KeyPath) + if err != nil { + return ovpn.ServerTLSOptions{}, 0, err + } + certificateAuthority, err := requiredMaterialSource("tls.client_certificate", options.ClientCertificate, options.ClientCertificatePath) + if err != nil { + return ovpn.ServerTLSOptions{}, 0, err + } + tlsOptions := ovpn.ServerTLSOptions{ + CertificateAuthority: certificateAuthority, + Certificate: certificate, + Key: key, + VerifyClientCertificate: options.VerifyClientCertificate, + } + keyDirection := -1 + controlWrap := options.ControlWrap + if controlWrap != nil && (controlWrap.Type != "" || len(controlWrap.Key) > 0 || controlWrap.KeyPath != "" || controlWrap.Direction != "") { + wrapKey, wrapErr := requiredMaterialSource("tls.control_wrap.key", controlWrap.Key, controlWrap.KeyPath) + if wrapErr != nil { + return ovpn.ServerTLSOptions{}, 0, wrapErr + } + switch controlWrap.Type { + case "tls_auth": + keyDirection, err = keyDirectionValue(controlWrap.Direction) + if err != nil { + return ovpn.ServerTLSOptions{}, 0, err + } + tlsOptions.Auth = wrapKey + case "tls_crypt", "tls_crypt_v2": + if controlWrap.Direction != "" { + return ovpn.ServerTLSOptions{}, 0, E.New("`tls.control_wrap.direction` is only supported by `tls_auth`") + } + if controlWrap.Type == "tls_crypt" { + tlsOptions.Crypt = wrapKey + } else { + tlsOptions.CryptV2 = wrapKey + } + case "": + return ovpn.ServerTLSOptions{}, 0, E.New("missing OpenVPN control wrap type") + default: + return ovpn.ServerTLSOptions{}, 0, E.New("unknown OpenVPN control wrap type: ", controlWrap.Type) + } + } + return tlsOptions, keyDirection, nil +} + +func applyServerPushOptions(serverOptions *ovpn.ServerOptions, options option.OpenVPNServerEndpointOptions) { + topology := options.Topology + if topology == "" { + topology = "subnet" + } + localAddresses := make([]netip.Prefix, 0, len(options.Address)) + for _, prefix := range options.Address { + if !prefix.IsValid() { + continue + } + if prefix.Addr().Is4() { + localAddresses = append(localAddresses, netip.PrefixFrom(prefix.Addr(), 32)) + } else { + localAddresses = append(localAddresses, netip.PrefixFrom(prefix.Addr(), 128)) + } + } + serverOptions.Tunnel = ovpn.ServerTunnelOptions{ + AddressPools: slices.Clone(options.Address), + Topology: topology, + LocalAddress: localAddresses, + } + serverOptions.Push = ovpn.ServerPushOptions{ + PingInterval: time.Duration(options.KeepaliveInterval), + PingRestart: time.Duration(options.KeepaliveTimeout), + } + if options.Push == nil { + return + } + serverOptions.Push.Routes = slices.Clone(options.Push.Routes) + serverOptions.Push.DNS = slices.Clone(options.Push.DNS) + serverOptions.Push.BlockOutsideDNS = options.Push.BlockOutsideDNS + if options.Push.RedirectGateway { + serverOptions.Push.RedirectGateway = true + if len(options.Push.RedirectGatewayFlags) > 0 { + serverOptions.Push.RedirectGatewayFlags = slices.Clone(options.Push.RedirectGatewayFlags) + } else { + serverOptions.Push.RedirectGatewayFlags = []string{"def1"} + } + } +} + +func (s *ServerEndpoint) readLoop() { + defer close(s.readLoopDone) + for { + serverPacketBuffers, err := s.server.ReadDataPackets(s.loopContext) + if err != nil { + if E.IsClosedOrCanceled(err) || s.loopContext.Err() != nil { + return + } + s.logger.Error(E.Cause(err, "OpenVPN server terminated")) + return + } + packetBuffers := make([]*buf.Buffer, len(serverPacketBuffers)) + for i, packetBuffer := range serverPacketBuffers { + packetBuffers[i] = packetBuffer.Buffer + } + err = s.device.WriteInboundBuffers(packetBuffers) + buf.ReleaseMulti(packetBuffers) + if err != nil { + s.logger.Error(E.Cause(err, "write OpenVPN packet to device")) + } + } +} + +func (s *ServerEndpoint) Close() error { + s.started.Store(false) + s.cancelLoop() + var serverErr error + if s.server != nil { + serverErr = s.server.Close() + } + if s.readLoopDone != nil { + <-s.readLoopDone + } + var deviceErr error + if s.device != nil { + deviceErr = s.device.Close() + } + var listenerErr error + if s.listener != nil { + listenerErr = s.listener.Close() + } + return E.Errors(serverErr, deviceErr, listenerErr) +} + +func (s *ServerEndpoint) PreMatchFlow(network string, destination netip.Addr) adapter.PreMatchAction { + return adapter.PreMatchFlow +} + +func (s *ServerEndpoint) PortAddresses() (netip.Addr, netip.Addr) { + return s.device.PortAddresses() +} + +func (s *ServerEndpoint) PortMTU() uint32 { + return s.device.PortMTU() +} + +func (s *ServerEndpoint) AttachReturn(returnPath tun.Return) error { + return s.device.AttachReturn(returnPath) +} + +func (s *ServerEndpoint) DetachReturn(returnPath tun.Return) error { + return s.device.DetachReturn(returnPath) +} + +func (s *ServerEndpoint) JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) tun.FlowVerdict { + return judgeOpenVPNFlow(s.router, s.Tag(), s.Type(), s.localAddresses, network, source, destination, firstPacket) +} + +func (s *ServerEndpoint) WritePackets(packets [][]byte) error { + if !s.started.Load() { + return E.New("OpenVPN server is not ready yet") + } + packetBuffers := make([]*buf.Buffer, len(packets)) + for i, packet := range packets { + packetBuffers[i] = buf.As(packet) + } + routeMisses, err := s.server.WriteDataPacketBuffersByDestination(packetBuffers) + if len(routeMisses) > 0 { + s.writeRouteMisses(routeMisses) + } + return err +} + +func (s *ServerEndpoint) writePacketBuffersByDestination(packetBuffers []*buf.Buffer) error { + routeMisses, err := s.server.WriteDataPacketBuffersByDestination(packetBuffers) + if len(routeMisses) > 0 { + s.writeRouteMisses(routeMisses) + } + return err +} + +func (s *ServerEndpoint) writeRouteMisses(routeMisses []*ovpn.RouteMissError) { + returnPath, headroom := s.device.ReturnPath() + if returnPath == nil { + return + } + inet4Address, inet6Address := s.PortAddresses() + replies := make([][]byte, 0, len(routeMisses)) + for _, routeMiss := range routeMisses { + sourceAddress := packetSourceAddress(routeMiss.Packet, inet4Address, inet6Address) + reply, built := tun.BuildUnreachable(routeMiss.Packet, sourceAddress, headroom) + if built { + replies = append(replies, reply) + } + } + if len(replies) > 0 { + returnPath.ReturnPackets(replies) + } +} + +func (s *ServerEndpoint) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + s.newConnection(ctx, s, s.localAddresses, conn, source, destination, onClose) +} + +func (s *ServerEndpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + s.newPacketConnection(ctx, s, s.localAddresses, conn, source, destination, onClose) +} + +func (s *ServerEndpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + switch network { + case N.NetworkTCP: + s.logger.InfoContext(ctx, "outbound connection to ", destination) + case N.NetworkUDP: + s.logger.InfoContext(ctx, "outbound packet connection to ", destination) + } + if !s.started.Load() { + return nil, E.New("OpenVPN server is not ready yet") + } + if destination.IsDomain() { + destinationAddresses, err := s.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) + if err != nil { + return nil, err + } + return N.DialSerial(ctx, s.device, network, destination, destinationAddresses) + } + if !destination.Addr.IsValid() { + return nil, E.New("invalid destination: ", destination) + } + return s.device.DialContext(ctx, network, destination) +} + +func (s *ServerEndpoint) ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) { + s.logger.InfoContext(ctx, "outbound packet connection to ", destination) + if !s.started.Load() { + return nil, netip.Addr{}, E.New("OpenVPN server is not ready yet") + } + if destination.IsDomain() { + destinationAddresses, err := s.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) + if err != nil { + return nil, netip.Addr{}, err + } + return N.ListenSerial(ctx, s.device, destination, destinationAddresses) + } + packetConn, err := s.device.ListenPacket(ctx, destination) + if err != nil { + return nil, netip.Addr{}, err + } + if destination.IsIP() { + return packetConn, destination.Addr, nil + } + return packetConn, netip.Addr{}, nil +} + +func (s *ServerEndpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + packetConn, _, err := s.ListenPacketWithDestination(ctx, destination) + return packetConn, err +} diff --git a/protocol/openvpn/socket_buffer.go b/protocol/openvpn/socket_buffer.go new file mode 100644 index 000000000..e0263bfaa --- /dev/null +++ b/protocol/openvpn/socket_buffer.go @@ -0,0 +1,21 @@ +//go:build !linux + +package openvpn + +import "github.com/sagernet/sing/common" + +const openVPNUDPSocketBufferSize = 7 << 20 + +type openVPNUDPSocketBufferSetter interface { + SetReadBuffer(bytes int) error + SetWriteBuffer(bytes int) error +} + +func tuneOpenVPNUDPSocket(connection any) { + bufferSetter, loaded := common.Cast[openVPNUDPSocketBufferSetter](connection) + if !loaded { + return + } + _ = bufferSetter.SetReadBuffer(openVPNUDPSocketBufferSize) + _ = bufferSetter.SetWriteBuffer(openVPNUDPSocketBufferSize) +} diff --git a/protocol/openvpn/socket_buffer_linux.go b/protocol/openvpn/socket_buffer_linux.go new file mode 100644 index 000000000..bdc22b47f --- /dev/null +++ b/protocol/openvpn/socket_buffer_linux.go @@ -0,0 +1,28 @@ +package openvpn + +import ( + "syscall" + + "github.com/sagernet/sing/common" + + "golang.org/x/sys/unix" +) + +const openVPNUDPSocketBufferSize = 7 << 20 + +func tuneOpenVPNUDPSocket(connection any) { + syscallConnection, loaded := common.Cast[syscall.Conn](connection) + if !loaded { + return + } + rawConnection, err := syscallConnection.SyscallConn() + if err != nil { + return + } + _ = rawConnection.Control(func(fd uintptr) { + _ = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_RCVBUF, openVPNUDPSocketBufferSize) + _ = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_SNDBUF, openVPNUDPSocketBufferSize) + _ = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_RCVBUFFORCE, openVPNUDPSocketBufferSize) + _ = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_SNDBUFFORCE, openVPNUDPSocketBufferSize) + }) +} diff --git a/protocol/openvpn/status.go b/protocol/openvpn/status.go new file mode 100644 index 000000000..b39a22e48 --- /dev/null +++ b/protocol/openvpn/status.go @@ -0,0 +1,114 @@ +package openvpn + +import ( + "slices" + + "github.com/sagernet/sing-box/adapter" + ovpn "github.com/sagernet/sing-openvpn" +) + +var _ adapter.OpenVPNEndpoint = (*ClientEndpoint)(nil) + +func (c *ClientEndpoint) OpenVPNStatus() adapter.OpenVPNStatus { + var status adapter.OpenVPNStatus + challenge := c.client.PendingChallenge() + state := c.state.Load() + c.statusAccess.Lock() + status.Error = c.terminalError + c.statusAccess.Unlock() + switch { + case challenge != nil: + status.State = adapter.OpenVPNStateAuthPending + status.Challenge = &adapter.OpenVPNChallenge{ + ID: challenge.ID, + Kind: string(challenge.Kind), + Username: challenge.Username, + Message: challenge.Message, + URL: challenge.URL, + SecretMessage: challenge.SecretMessage, + Echo: challenge.Echo, + PreviousError: challenge.PreviousError, + Deadline: challenge.Deadline, + } + case status.Error != "": + status.State = adapter.OpenVPNStateError + case state.started && state.tunnelConfigured && c.client.Ready(): + status.State = adapter.OpenVPNStateConnected + tunnelInfo := state.tunnelInfo + tunnelInfo.IPv4 = slices.Clone(tunnelInfo.IPv4) + tunnelInfo.IPv6 = slices.Clone(tunnelInfo.IPv6) + tunnelInfo.DNS = slices.Clone(tunnelInfo.DNS) + status.TunnelInfo = &tunnelInfo + default: + status.State = adapter.OpenVPNStateConnecting + } + return status +} + +func (c *ClientEndpoint) StatusUpdated() <-chan struct{} { + c.statusAccess.Lock() + defer c.statusAccess.Unlock() + return c.statusUpdated +} + +func (c *ClientEndpoint) CompleteChallenge(challengeID string, response adapter.OpenVPNChallengeResponse) error { + return c.client.CompleteChallenge(challengeID, ovpn.ChallengeResponse{ + Username: response.Username, + Password: response.Password, + Secret: response.Secret, + }) +} + +func (c *ClientEndpoint) CancelChallenge(challengeID string) error { + return c.client.CancelChallenge(challengeID) +} + +func (c *ClientEndpoint) notifyStatusUpdated() { + c.statusAccess.Lock() + c.notifyStatusUpdatedLocked() + c.statusAccess.Unlock() +} + +func (c *ClientEndpoint) notifyStatusUpdatedLocked() { + close(c.statusUpdated) + c.statusUpdated = make(chan struct{}) +} + +func (c *ClientEndpoint) setTerminalError(err error) { + c.statusAccess.Lock() + c.terminalError = err.Error() + c.notifyStatusUpdatedLocked() + c.statusAccess.Unlock() +} + +func (c *ClientEndpoint) watchChallenges() { + defer close(c.challengeLoopDone) + var loggedChallengeID string + for { + challengeUpdated := c.client.ChallengeUpdated() + challenge := c.client.PendingChallenge() + if challenge != nil && challenge.ID != loggedChallengeID { + loggedChallengeID = challenge.ID + c.logChallenge(challenge) + } + c.notifyStatusUpdated() + select { + case <-c.loopContext.Done(): + return + case <-challengeUpdated: + } + } +} + +func (c *ClientEndpoint) logChallenge(challenge *ovpn.Challenge) { + switch challenge.Kind { + case ovpn.ChallengeCredentials: + c.logger.Info("waiting for credentials") + case ovpn.ChallengeSecret: + c.logger.Info("waiting for challenge response: ", challenge.Message) + case ovpn.ChallengeMessage: + c.logger.Info("authentication message: ", challenge.Message) + case ovpn.ChallengeOpenURL: + c.logger.Info("waiting for authentication: ", challenge.URL) + } +} diff --git a/protocol/wireguard/endpoint.go b/protocol/wireguard/endpoint.go index bae1d36a1..4783b3539 100644 --- a/protocol/wireguard/endpoint.go +++ b/protocol/wireguard/endpoint.go @@ -75,9 +75,9 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL } networkManager := service.FromContext[adapter.NetworkManager](ctx) var egressPool *tun.UDPEgressPool - wireGuardListener, isWireGuardListener := common.Cast[dialer.WireGuardListener](outboundDialer) - if isWireGuardListener { - anchorControl, egressEnabled := wireGuardListener.WireGuardControl() + udpListener, isUDPListener := common.Cast[dialer.UDPListener](outboundDialer) + if isUDPListener { + anchorControl, egressEnabled := udpListener.UDPListenerControl() if egressEnabled { egressPool = tun.NewUDPEgressPool(tun.UDPEgressPoolOptions{ Logger: logger, diff --git a/release/DEFAULT_BUILD_TAGS b/release/DEFAULT_BUILD_TAGS index 4754ec449..a588309b2 100644 --- a/release/DEFAULT_BUILD_TAGS +++ b/release/DEFAULT_BUILD_TAGS @@ -1 +1 @@ -with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_cloudflared,with_naive_outbound,with_usbip,badlinkname,tfogo_checklinkname0 \ No newline at end of file +with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_cloudflared,with_naive_outbound,with_usbip,with_openvpn,with_openconnect,badlinkname,tfogo_checklinkname0 diff --git a/release/DEFAULT_BUILD_TAGS_OTHERS b/release/DEFAULT_BUILD_TAGS_OTHERS index db8a0c340..60b1be3eb 100644 --- a/release/DEFAULT_BUILD_TAGS_OTHERS +++ b/release/DEFAULT_BUILD_TAGS_OTHERS @@ -1 +1 @@ -with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_cloudflared,with_usbip,badlinkname,tfogo_checklinkname0 \ No newline at end of file +with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_cloudflared,with_usbip,with_openvpn,with_openconnect,badlinkname,tfogo_checklinkname0 diff --git a/release/DEFAULT_BUILD_TAGS_WINDOWS b/release/DEFAULT_BUILD_TAGS_WINDOWS index d63a3967f..6a5d4d03f 100644 --- a/release/DEFAULT_BUILD_TAGS_WINDOWS +++ b/release/DEFAULT_BUILD_TAGS_WINDOWS @@ -1 +1 @@ -with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_cloudflared,with_naive_outbound,with_purego,with_usbip,badlinkname,tfogo_checklinkname0 \ No newline at end of file +with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_cloudflared,with_naive_outbound,with_purego,with_usbip,with_openvpn,with_openconnect,badlinkname,tfogo_checklinkname0 diff --git a/test/go.mod b/test/go.mod index e141e9aac..7fb11fcd0 100644 --- a/test/go.mod +++ b/test/go.mod @@ -10,9 +10,10 @@ require ( github.com/docker/docker v27.3.1+incompatible github.com/docker/go-connections v0.5.0 github.com/gofrs/uuid/v5 v5.4.0 + github.com/opencontainers/image-spec v1.1.0 github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 - github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d - github.com/sagernet/sing-quic v0.6.2-0.20260525051024-9467ede27fb7 + github.com/sagernet/sing v0.8.12-0.20260717023913-84ab32b56cb8 + github.com/sagernet/sing-quic v0.6.4-0.20260709034545-e23afe1172dc github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/spyzhov/ajson v0.9.4 @@ -23,10 +24,12 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ajg/form v1.5.1 // indirect github.com/akutz/memconn v0.1.0 // indirect github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect + github.com/anchore/go-lzo v0.1.0 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/anthropics/anthropic-sdk-go v1.26.0 // indirect @@ -66,20 +69,26 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/btree v1.1.3 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/gopacket v1.1.19 // indirect github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect + github.com/huin/goupnp v1.2.0 // indirect github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91 // indirect + github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jsimonetti/rtnetlink v1.4.0 // indirect github.com/keybase/go-keychain v0.0.1 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/koron/go-ssdp v0.0.4 // indirect github.com/kr/fs v0.1.0 // indirect github.com/libdns/acmedns v0.5.0 // indirect github.com/libdns/alidns v1.0.6 // indirect github.com/libdns/cloudflare v0.2.2 // indirect github.com/libdns/libdns v1.1.1 // indirect + github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138 // indirect + github.com/libp2p/go-netroute v0.2.1 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect github.com/mdlayher/netlink v1.9.0 // indirect github.com/mdlayher/socket v0.5.1 // indirect @@ -92,8 +101,10 @@ require ( github.com/morikuni/aec v1.0.0 // indirect github.com/openai/openai-go/v3 v3.26.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pion/dtls/v3 v3.1.5 // indirect + github.com/pion/logging v0.2.4 // indirect + github.com/pion/transport/v4 v4.0.2 // indirect github.com/pires/go-proxyproto v0.8.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkg/sftp v1.13.10 // indirect @@ -103,53 +114,56 @@ require ( github.com/safchain/ethtool v0.3.0 // indirect github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect github.com/sagernet/cors v1.2.1 // indirect - github.com/sagernet/cronet-go v0.0.0-20260620140045-05ab0dc17597 // indirect - github.com/sagernet/cronet-go/all v0.0.0-20260620140045-05ab0dc17597 // indirect - github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260620135226-def9ff0fb992 // indirect + github.com/sagernet/cronet-go v0.0.0-20260712143338-d22f2ea3630e // indirect + github.com/sagernet/cronet-go/all v0.0.0-20260712143338-d22f2ea3630e // indirect + github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260712142643-1e5048bd5587 // indirect github.com/sagernet/fswatch v0.1.2 // indirect github.com/sagernet/gliderssh v0.3.4-0.20260531100337-2194faca5648 // indirect github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect - github.com/sagernet/nftables v0.3.0-mod.3 // indirect + github.com/sagernet/nftables v0.3.0-mod.4 // indirect github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 // indirect github.com/sagernet/sing-mux v0.3.5 // indirect + github.com/sagernet/sing-openconnect v0.0.0-20260717061548-458a8732933e // indirect + github.com/sagernet/sing-openvpn v0.0.0-20260717055507-7e569eca5e4d // indirect github.com/sagernet/sing-shadowtls v0.2.1 // indirect - github.com/sagernet/sing-snell v0.0.0-20260705044717-4e9e73be7814 // indirect - github.com/sagernet/sing-tun v0.8.12-0.20260708091449-be1a05a4c962 // indirect + github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb // indirect + github.com/sagernet/sing-tun v0.8.12-0.20260717024008-39eed1f6361d // indirect github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb // indirect github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 // indirect github.com/sagernet/smux v1.5.50-sing-box-mod.1 // indirect - github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260706062137-ae2dde1295a3 // indirect - github.com/sagernet/wireguard-go v0.0.5-0.20260706153856-2c27bbf4f97f // indirect + github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260717024045-1edfbb9ee544 // indirect + github.com/sagernet/wireguard-go v0.0.5-0.20260717024847-6f5e8b1947ae // indirect github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect + github.com/smallstep/pkcs7 v0.1.1 // indirect github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect @@ -164,6 +178,7 @@ require ( github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect github.com/vishvananda/netns v0.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/zeebo/blake3 v0.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect @@ -184,7 +199,7 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/term v0.40.0 // indirect golang.org/x/text v0.34.0 // indirect - golang.org/x/time v0.11.0 // indirect + golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.42.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect diff --git a/test/go.sum b/test/go.sum index 0b49c32c4..8b2e18e6d 100644 --- a/test/go.sum +++ b/test/go.sum @@ -12,6 +12,8 @@ github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs= +github.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= @@ -106,8 +108,11 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -118,8 +123,12 @@ github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8 github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= +github.com/huin/goupnp v1.2.0 h1:uOKW26NG1hsSSbXIZ1IR7XP9Gjd1U8pnLaCMgntmkmY= +github.com/huin/goupnp v1.2.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91 h1:u9i04mGE3iliBh0EFuWaKsmcwrLacqGmq1G3XoaM7gY= github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91/go.mod h1:qfvBmyDNp+/liLEYWRvqny/PEz9hGe2Dz833eXILSmo= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I= github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= @@ -130,6 +139,8 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= +github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -150,6 +161,10 @@ github.com/libdns/cloudflare v0.2.2 h1:XWHv+C1dDcApqazlh08Q6pjytYLgR2a+Y3xrXFu0v github.com/libdns/cloudflare v0.2.2/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60= github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U= github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= +github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138 h1:YohuNPT/1k3VcThCQlBZ43PCPWPfMRS1zcxWBF2SLK8= +github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138/go.mod h1:TXQg5tfSy+bUjnhT5728j5j/MBj7keIYqqZ1+8k/ui8= +github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU= +github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/mdlayher/netlink v1.9.0 h1:G8+GLq2x3v4D4MVIqDdNUhTUC7TKiCy/6MDkmItfKco= @@ -182,6 +197,12 @@ github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pion/dtls/v3 v3.1.5 h1:9xJtVsHwMYeSjPp5Hh1FTis4DchnQWtnOa5o+6ygqfc= +github.com/pion/dtls/v3 v3.1.5/go.mod h1:gz1K4jg6c+fq86oQMH4pilpCEOEPwmEr2jY+VcF/mkU= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk= +github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM= github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -203,68 +224,68 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= -github.com/sagernet/cronet-go v0.0.0-20260620140045-05ab0dc17597 h1:QkwE/ZFnShDuPF+ExmAyZlQaMwFNgkYZMekrabiStfg= -github.com/sagernet/cronet-go v0.0.0-20260620140045-05ab0dc17597/go.mod h1:T/mwtrpC4JlWfScw73CmSBvHzIvc7BatQ1MhRr+cYNw= -github.com/sagernet/cronet-go/all v0.0.0-20260620140045-05ab0dc17597 h1:cLALmGKP9eOS8622gWQIiVbZlOfH29PGNsoxbEloIdk= -github.com/sagernet/cronet-go/all v0.0.0-20260620140045-05ab0dc17597/go.mod h1:zVHZ5tgDTwbNvUGffAgLmouYs4in0grEzhSdaggoZOw= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260620135226-def9ff0fb992 h1:J9l8PP4vb79Wm5zKaMO6LNZ/AiP1FvyAWJBlkKHrRBU= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260620135226-def9ff0fb992 h1:wXDjUNeKuihv85Kg51FomkiEH7xGsDgRcfRLiyZxacQ= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260620135226-def9ff0fb992 h1:IF04nGyY3Q6Nbk9XJwTX1mckwhf12iIx4RhZ4TLOZIU= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260620135226-def9ff0fb992/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260620135226-def9ff0fb992 h1:dlnG1E42xx8ms2fyZigYwsYJ1Gqoj2QT8WeGlpOAWK8= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260620135226-def9ff0fb992 h1:aYAQQN3jZP89MmT1ZzpJz52jAsXx1WApmv5Qidc+ez4= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260620135226-def9ff0fb992 h1:n81+aLphvjLpL2M6lI+BC8Ldw4S/FPA3CFDXTWL4g3I= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260620135226-def9ff0fb992 h1:mOseesF+CxgPG2U1a2Yh2fUUMdOaPxSuk4eIL6g7EU8= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260620135226-def9ff0fb992/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260620135226-def9ff0fb992 h1:JK9kP72IxAoyVQTnh9gGkh8S9RjP4FBkj95WjrLPyKs= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260620135226-def9ff0fb992 h1:3EtgLRsUpmeRaOBynTARxVC8tDegykigutmpAow2ayc= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260620135226-def9ff0fb992/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260620135226-def9ff0fb992 h1:Vr3I2sC9E/1FurpZwJXAL29C7jJROyN3JfulQWjhKuA= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260620135226-def9ff0fb992 h1:flxwC8loz0C4LQ/tLK7LvNMKx4iqaXayPggzzCxCevI= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260620135226-def9ff0fb992/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260620135226-def9ff0fb992 h1:G4vwPmOVR/jXDngIUC9owbEtMKXLZl/BgUHhEWWl8ec= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260620135226-def9ff0fb992 h1:zHIxR2FlJOW5GRmgwyA2Gjgx7potOCtlOmdl8k4mwJI= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260620135226-def9ff0fb992/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260620135226-def9ff0fb992 h1:U1OrR5zP+lkOPqrDpZsn8sPK1XBWZ84isXaFBZLsOfs= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260620135226-def9ff0fb992/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260620135226-def9ff0fb992 h1:F59ptY4AdtKEg73OWaL+lTb5yoxp5b/gTuDbvA6xMyg= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260620135226-def9ff0fb992 h1:Y5axK4sCWXH+2OCpKYPI8nX3OSBuRb/6yC+5xBi9/uo= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260620135226-def9ff0fb992/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260620135226-def9ff0fb992 h1:QCBwCdv9y+RJj7p0b9Db2p9fFt1wtnawD0sn9oV3vRs= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260620135226-def9ff0fb992/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= -github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260620135226-def9ff0fb992 h1:6gXFFaAMiGCPZdfUs64qzj9cl7EleVs9HsNfRi8jWWw= -github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= -github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260620135226-def9ff0fb992 h1:VGoIX2u4CWVg9kiyjQdIpFMaFUNCW3yz0pyrEKI5X0o= -github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260620135226-def9ff0fb992/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= -github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260620135226-def9ff0fb992 h1:SjPuqtUNCzIDaEm7iY86JZ7L+ixTmai4i2DIC++eUvw= -github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260620135226-def9ff0fb992/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= -github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260620135226-def9ff0fb992 h1:ao2FrDzTYhu2MYsMri9nzqIdnAL7ooUWQN6/FFr+Lbk= -github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260620135226-def9ff0fb992/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= -github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260620135226-def9ff0fb992 h1:5gPMu6EUlX6gqCRTOJrJk1FMILO6ugtnopQF1c2R/lY= -github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260620135226-def9ff0fb992/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= -github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260620135226-def9ff0fb992 h1:UaLOQKbjnLrO943Sm+ff/jm+NmemRuJXiImmtqShd8s= -github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= -github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260620135226-def9ff0fb992 h1:wxlsDfxDowVk99Ay2hzfuIXPpXH1lGQxSL/2sX/D7jw= -github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260620135226-def9ff0fb992/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260620135226-def9ff0fb992 h1:pgRpWh2JPE73mtedovPPu3gmmAqHz3Rfz79QVxciu2o= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260620135226-def9ff0fb992/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260620135226-def9ff0fb992 h1:9jtXyxukTS2ZdhMy3u1hg4StkZpgP48BOfgYhXf835w= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260620135226-def9ff0fb992 h1:gKgD1LJZbZzacaaqBQX/YKy4dyomhn8xtfmDKayVLW4= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260620135226-def9ff0fb992/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260620135226-def9ff0fb992 h1:Lv8gtKP4QRn6Yjv3h5L215aGQBgwCyAE5YqejmN9Bqc= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260620135226-def9ff0fb992 h1:2wsRAcqJHOTlj6zGc3oQyxAZHDnGwyB/pTdAOUtLgHY= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= +github.com/sagernet/cronet-go v0.0.0-20260712143338-d22f2ea3630e h1:Y5mhsZrYuZ9jraIqg7hg1fw4zoVreae81fWvmozbCsQ= +github.com/sagernet/cronet-go v0.0.0-20260712143338-d22f2ea3630e/go.mod h1:T/mwtrpC4JlWfScw73CmSBvHzIvc7BatQ1MhRr+cYNw= +github.com/sagernet/cronet-go/all v0.0.0-20260712143338-d22f2ea3630e h1:rdNlS1dRSi7jQe/ingFf7QmV9ZCUZMbxAfHnBrdVW7g= +github.com/sagernet/cronet-go/all v0.0.0-20260712143338-d22f2ea3630e/go.mod h1:WNl4xfTNuR+f7SObmuBtrk0p4MhlmvuuiWYoty3U52E= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260712142643-1e5048bd5587 h1:ENmDXbGH92/jsMwhjIxK2a0URkA8ILC3npjqmTGj0Yc= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260712142643-1e5048bd5587 h1:5xn/EZOO5LriSEih91thvTuR+gxb59jNxNQiB/KQPEc= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260712142643-1e5048bd5587 h1:2/UN0LvWnAM0Sc9B/zE2qWylGrX2hQXhknyQyTlvz4k= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260712142643-1e5048bd5587/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260712142643-1e5048bd5587 h1:SDULc9o0HkneJD38H1G+HRLby69zfdtyQk5qaKtn/tU= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260712142643-1e5048bd5587 h1:hfcM9YccWN4O2LENHN16Jgm4g1/1PRouV0RmvvrS1f0= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260712142643-1e5048bd5587 h1:arY9CL3C7lwJfG2Cdz2ZLvHLzT1wnzCVyAomNER6CrY= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260712142643-1e5048bd5587 h1:HrtCf6KPJsW9KJ4L4T8HMW1vx+3xefLRLObpjZEtzF4= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260712142643-1e5048bd5587/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260712142643-1e5048bd5587 h1:Inzzp4hyvcC0lawhndaK8iwEN0vGLDJLCThbaKZpksg= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260712142643-1e5048bd5587 h1:qN6vqr9nFnZqjZXhLez+7tMcbm3FMfhaJRWNMlK3SLI= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260712142643-1e5048bd5587/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260712142643-1e5048bd5587 h1:Zwzpw6555p3rWw844JxNZ+5iRqli6ZBOEtMH3qq1c7s= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260712142643-1e5048bd5587 h1:CwsGHfo1HwvEhA2Wnh3O8WxazN3d4Un44LXhiuIvA/w= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260712142643-1e5048bd5587/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260712142643-1e5048bd5587 h1:Pn6vsVFOJkj9q/XKBOZosfuDLB6luNMLkqq6YLUPXEk= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260712142643-1e5048bd5587 h1:vgRWcEr2jlgICj28XwDMXPQcy8mTob1ZcO+tPdiZjes= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260712142643-1e5048bd5587/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260712142643-1e5048bd5587 h1:uiO62HvSAdRJR3d1Jc4duxWig3kkdwHBSQ3TmUFqw48= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260712142643-1e5048bd5587/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260712142643-1e5048bd5587 h1:mXioRdq9h2YlIr9XGM51kXgIkToywwVfnGmJiaz/uzM= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260712142643-1e5048bd5587 h1:oOlfOL+sq0KNZRsk06+5KgvtKw2TEXl2EZnLakTx+WM= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260712142643-1e5048bd5587/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260712142643-1e5048bd5587 h1:UDCa0lYiSUXD4wbxA/G5pGPSXhJaesCQCh5IEiZKb3M= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260712142643-1e5048bd5587/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260712142643-1e5048bd5587 h1:Yg2Ut7mPs0GK4W6p9LDh8RrDOnhTe4YpvG/WeJyUqMo= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260712142643-1e5048bd5587 h1:NtagC/YHvucD0Azh86aVghOk4z5f7oOAAy25Uw4knXQ= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260712142643-1e5048bd5587/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260712142643-1e5048bd5587 h1:INzfLHBKjgJxoUDBeCaiB9hYqjhn5yfudkMz2phYypA= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260712142643-1e5048bd5587/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260712142643-1e5048bd5587 h1:IX5NCEV9nojHdjpOkxKVN4L5FUuA5nwawXlSTdFXjCE= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260712142643-1e5048bd5587/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260712142643-1e5048bd5587 h1:pzgA94sR7kvrP+H86/to2oN3efYORQmh0Q3b6AyfRJQ= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260712142643-1e5048bd5587/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260712142643-1e5048bd5587 h1:nkS6jhF90E24PW37Xemhs6+hUM0l2iRcQcZFhPJL8R8= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260712142643-1e5048bd5587 h1:EbydHlp6vWdqhb8e8zBPVJv/F3HPob+FgO6rsSdNSr0= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260712142643-1e5048bd5587/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260712142643-1e5048bd5587 h1:sSfVTswgqQZJqh9wTP0Acvvo5/qYARAoHegRYwZ+gyU= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260712142643-1e5048bd5587/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260712142643-1e5048bd5587 h1:lHqqbALbKdJdq/1rcI4yyg3zvibHDw7wmhDYVbjy498= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260712142643-1e5048bd5587 h1:23GyWjb58Nk9a7WXgRfFUiTrMD9kvYSRHx73AaBthYc= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260712142643-1e5048bd5587/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260712142643-1e5048bd5587 h1:64EMjgVuZMD4TX5b7oWUfWRVp8aVl3hg4aWrriQKOWo= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260712142643-1e5048bd5587 h1:x8cvgMQUs0EgVwt3iT/isRQ9KImvXvPobLKO5bHTN4o= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= github.com/sagernet/fswatch v0.1.2 h1:/TT7k4mkce1qFPxamLO842WjqBgbTBiXP2mlUjp9PFk= github.com/sagernet/fswatch v0.1.2/go.mod h1:5BpGmpUQVd3Mc5r313HRpvADHRg3/rKn5QbwFteB880= github.com/sagernet/gliderssh v0.3.4-0.20260531100337-2194faca5648 h1:IWVjKBARzVjdmH0VUaeTBOBli1qkwKmTG4XfbkpSS20= @@ -273,42 +294,48 @@ github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237 h1:SUPFNB+vSP4RBPr github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237/go.mod h1:QkkPEJLw59/tfxgapHta14UL5qMUah5NXhO0Kw2Kan4= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= -github.com/sagernet/nftables v0.3.0-mod.3 h1:CVfbVTd3Z/LQVc1Z3c1hpiriplJ4xDVHjfQCETiN9RA= -github.com/sagernet/nftables v0.3.0-mod.3/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ= +github.com/sagernet/nftables v0.3.0-mod.4 h1:vnOtcDYeSXv2e5RoRuGH0lrpttQFJ8iC4ICS2nhlDSo= +github.com/sagernet/nftables v0.3.0-mod.4/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= -github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d h1:BhsQU0Iug1tU4xR52cjm8Sc+LBo+KwdyLTRn3ie9moo= -github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA= +github.com/sagernet/sing v0.8.12-0.20260717023913-84ab32b56cb8 h1:dyRIj+MZ2rc9JVzJoG04jxu+MpvHrLIZLJr0QjNAMGg= +github.com/sagernet/sing v0.8.12-0.20260717023913-84ab32b56cb8/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA= github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 h1:3y6++yIa8XlDhxPkpR4p+7RUHVY2KTP9CPIGnWmOlO8= 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-quic v0.6.2-0.20260525051024-9467ede27fb7 h1:hFLPJ21uNZSbRnzhOKz4Zv0b4F93mpDorWyN93BeRcM= -github.com/sagernet/sing-quic v0.6.2-0.20260525051024-9467ede27fb7/go.mod h1:+oqD54aHel4ALKkp1hVXWCgLU/EjLojvm6AUzDfvj0I= +github.com/sagernet/sing-openconnect v0.0.0-20260717061548-458a8732933e h1:Kgcf16uKnxBNJMsR8MaWlORtOV2/qi+6yH830+b7Yfc= +github.com/sagernet/sing-openconnect v0.0.0-20260717061548-458a8732933e/go.mod h1:EIzh5HtImfQJxPKXFwS9lyMnmMy4aCQCx7ntQ4u41Gs= +github.com/sagernet/sing-openvpn v0.0.0-20260717055507-7e569eca5e4d h1:KGvybsWqE+Qkd9Ns2AzrrBSyNfbBJ7IZZqgj+oWa6SM= +github.com/sagernet/sing-openvpn v0.0.0-20260717055507-7e569eca5e4d/go.mod h1:CmTGnS5ijVSqFQV1dTq4WvFLUoz7bk9xasBPsX8NcYo= +github.com/sagernet/sing-quic v0.6.4-0.20260709034545-e23afe1172dc h1:zdc0fj4JdAdgAmQIoh7ZF+B/wPTEF2X75lYDqTmvlaw= +github.com/sagernet/sing-quic v0.6.4-0.20260709034545-e23afe1172dc/go.mod h1:9k+dzGsWMttUGldBzq3dU792YHXzW6NgfbOGltnXq+0= github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE= github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI= github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo= github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowtls v0.2.1 h1:ZiHZdnEnP+YS73NMsxiZmIFCwNd0M4k7PkGCKNXhbaM= github.com/sagernet/sing-shadowtls v0.2.1/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= -github.com/sagernet/sing-snell v0.0.0-20260705044717-4e9e73be7814 h1:xfnkRpjVRVeJhVvDZA8PzTLlKGTb1o2kdI4uv1YymXo= -github.com/sagernet/sing-snell v0.0.0-20260705044717-4e9e73be7814/go.mod h1:PcwzX/Xvqky0EP3kGt8OCjYb3R1pydenPHNQZcPZmXY= -github.com/sagernet/sing-tun v0.8.12-0.20260708091449-be1a05a4c962 h1:dmJoWdTQygt4P2rAwScy2IvHnFp1mKrW6OsI0qig6O8= -github.com/sagernet/sing-tun v0.8.12-0.20260708091449-be1a05a4c962/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ= +github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb h1:VvU2/PZqP5tbKTDq0BxkhRO8ZnKI4UJzziakgBiP2Qg= +github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb/go.mod h1:PcwzX/Xvqky0EP3kGt8OCjYb3R1pydenPHNQZcPZmXY= +github.com/sagernet/sing-tun v0.8.12-0.20260717024008-39eed1f6361d h1:UXUGxGTwotY+R+QkRGvhj/N148SDnwMh/d9Hefn5X7Y= +github.com/sagernet/sing-tun v0.8.12-0.20260717024008-39eed1f6361d/go.mod h1:F/gRq5VX1WN/OZtsvbN2JjXXuNl2ATJglHMSk1/iN9U= github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb h1:KEMbfexD4DvrQGYWwx6r+AwH9Veh8z6cnBZmtCS2G+0= github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb/go.mod h1:D4CnJX3MNAAANhbQUxfIRgBdnvlTEaV7h6ojedcs+pw= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8= -github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260706062137-ae2dde1295a3 h1:eczvica8YiS5j3GfpHg6JG1Icur4Z2D6ffSrLZTfD1E= -github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260706062137-ae2dde1295a3/go.mod h1:p8Ms8FbGlwQJyHb862XmdShTS50fFJ8C71VdO6xvWyk= -github.com/sagernet/wireguard-go v0.0.5-0.20260706153856-2c27bbf4f97f h1:TzN97RL07xWb3gZtmqFhsdkud4f6G/pohiaOLiqSBj4= -github.com/sagernet/wireguard-go v0.0.5-0.20260706153856-2c27bbf4f97f/go.mod h1:hEqi4y5czEg6LYtX2Bpjg+lV0b/J1n+5rA885Z66Mx0= +github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260717024045-1edfbb9ee544 h1:j2tab0dGHutfclhwZxrkSDMXwGXtozIo5BV4DgwS+1Q= +github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260717024045-1edfbb9ee544/go.mod h1:p8Ms8FbGlwQJyHb862XmdShTS50fFJ8C71VdO6xvWyk= +github.com/sagernet/wireguard-go v0.0.5-0.20260717024847-6f5e8b1947ae h1:GmxlXWnRmeNfPE1tWXRZIFgKJd5BH5okoDHKZkkI5bw= +github.com/sagernet/wireguard-go v0.0.5-0.20260717024847-6f5e8b1947ae/go.mod h1:hEqi4y5czEg6LYtX2Bpjg+lV0b/J1n+5rA885Z66Mx0= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU= +github.com/smallstep/pkcs7 v0.1.1/go.mod h1:dL6j5AIz9GHjVEBTXtW+QliALcgM19RtXaTeyxI+AfA= github.com/spyzhov/ajson v0.9.4 h1:MVibcTCgO7DY4IlskdqIlCmDOsUOZ9P7oKj8ifdcf84= github.com/spyzhov/ajson v0.9.4/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -350,8 +377,11 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= @@ -394,14 +424,26 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -410,6 +452,12 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= @@ -418,6 +466,12 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -427,24 +481,54 @@ golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/test/openconnect_test.go b/test/openconnect_test.go new file mode 100644 index 000000000..771730434 --- /dev/null +++ b/test/openconnect_test.go @@ -0,0 +1,575 @@ +package main + +import ( + "bytes" + "context" + "crypto/rand" + "io" + "net" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/sagernet/sing-box" + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "github.com/stretchr/testify/require" +) + +const ( + openConnectInteropEnvironment = "OPENCONNECT_IT" + openConnectOcservVersion = "1.3.0-2" + openConnectOcservImage = "sing-box-openconnect-ocserv:" + openConnectOcservVersion + openConnectUsername = "test" + openConnectPassword = "test" + openConnectTunnelAddress = "192.168.77.1" + openConnectEchoPort = 18080 +) + +const openConnectOcservPasswordFile = "test:tost,group1,group2:$5$i6SNmLDCgBNjyJ7q$SZ4bVJb7I/DLgXo3txHBVohRFBjOtdbxGQZp.DOnrA.\n" + +const openConnectOcservConfiguration = `auth = "plain[passwd=/fixture/ocpasswd]" + +tcp-port = 443 +udp-port = 443 + +run-as-user = nobody +run-as-group = nogroup +socket-file = /run/ocserv-socket +use-occtl = true +occtl-socket-file = /run/occtl.socket + +server-cert = /fixture/server-cert.pem +server-key = /fixture/server-key.pem +tls-priorities = "NORMAL:%SERVER_PRECEDENCE:%COMPAT" + +isolate-workers = false +max-clients = 4 +max-same-clients = 2 +rate-limit-ms = 0 +max-ban-score = 0 +auth-timeout = 30 +cookie-timeout = 300 +keepalive = 1 +dpd = 2 +try-mtu-discovery = false + +device = vpns +ipv4-network = 192.168.77.0 +ipv4-netmask = 255.255.255.0 +route = 192.168.77.0/255.255.255.0 +ping-leases = false +mtu = 1400 + +cisco-client-compat = false +dtls-psk = true +dtls-legacy = false +match-tls-dtls-ciphers = false +rekey-time = 0 +rekey-method = new-tunnel +` + +type openConnectOcservContainer struct { + name string + tcpAddress string + serverAddress string + certificateAuthorityPath string + passwordPath string +} + +type openConnectTCPProxy struct { + listener net.Listener + target string + access sync.Mutex + connections map[*openConnectTCPProxyConnection]struct{} + closed bool + accepted atomic.Uint64 +} + +type openConnectTCPProxyConnection struct { + proxy *openConnectTCPProxy + downstream net.Conn + upstream net.Conn + closeOnce sync.Once +} + +func TestOpenConnectDockerInterop(t *testing.T) { + if testing.Short() || strings.TrimSpace(os.Getenv(openConnectInteropEnvironment)) == "" { + t.Skip(openConnectInteropEnvironment + " is not set or short testing is enabled") + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + t.Cleanup(cancel) + requireOpenConnectDockerImage(t, ctx) + + t.Run("prefilled_credentials_and_tcp_echo", func(subtest *testing.T) { + container := startOpenConnectOcservContainer(subtest, ctx) + instance := startInstance(subtest, openConnectInstanceOptions( + container.serverAddress, + container.certificateAuthorityPath, + openConnectUsername, + openConnectPassword, + )) + endpoint := requireOpenConnectEndpoint(subtest, instance) + status := waitForOpenConnectState(subtest, endpoint, adapter.OpenConnectStateConnected, 45*time.Second) + require.Nil(subtest, status.AuthForm) + err := exchangeOpenConnectTCPEcho(endpoint, 256*1024, 30*time.Second) + require.NoError(subtest, err) + }) + + t.Run("interactive_password_auth", func(subtest *testing.T) { + container := startOpenConnectOcservContainer(subtest, ctx) + instance := startInstance(subtest, openConnectInstanceOptions( + container.serverAddress, + container.certificateAuthorityPath, + "", + "", + )) + endpoint := requireOpenConnectEndpoint(subtest, instance) + driveOpenConnectInteractiveAuthentication(subtest, endpoint, 45*time.Second) + err := exchangeOpenConnectTCPEcho(endpoint, 64*1024, 30*time.Second) + require.NoError(subtest, err) + }) + + t.Run("cstp_reconnect_reuses_cookie", func(subtest *testing.T) { + container := startOpenConnectOcservContainer(subtest, ctx) + proxy := startOpenConnectTCPProxy(subtest, container.tcpAddress) + instance := startInstance(subtest, openConnectInstanceOptions( + openConnectLocalhostAddress(subtest, proxy.listener.Addr().String()), + container.certificateAuthorityPath, + openConnectUsername, + openConnectPassword, + )) + endpoint := requireOpenConnectEndpoint(subtest, instance) + waitForOpenConnectState(subtest, endpoint, adapter.OpenConnectStateConnected, 45*time.Second) + waitForOpenConnectTCPEcho(subtest, endpoint, 30*time.Second) + + acceptedBeforeDrop := proxy.accepted.Load() + err := os.WriteFile(container.passwordPath, []byte("test:tost,group1,group2:!\n"), 0o644) + require.NoError(subtest, err) + droppedConnections := proxy.dropConnections() + require.Positive(subtest, droppedConnections) + waitForOpenConnectProxyAccept(subtest, proxy, acceptedBeforeDrop, 30*time.Second) + waitForOpenConnectTCPEcho(subtest, endpoint, 60*time.Second) + + status := endpoint.OpenConnectStatus() + require.Equal(subtest, adapter.OpenConnectStateConnected, status.State, status.Error) + require.Nil(subtest, status.AuthForm) + logs, err := openConnectDockerOutput(ctx, "logs", container.name) + require.NoError(subtest, err) + require.GreaterOrEqual(subtest, strings.Count(logs, "HTTP CONNECT /CSCOSSLC/tunnel"), 2, logs) + require.Equal(subtest, 1, strings.Count(logs, "user '"+openConnectUsername+"' obtained cookie"), logs) + }) +} + +func openConnectInstanceOptions(server string, certificateAuthorityPath string, username string, password string) option.Options { + endpointOptions := option.OpenConnectEndpointOptions{ + Server: server, + Flavor: "anyconnect", + Username: username, + Password: password, + NoUDP: true, + TLS: option.OpenConnectTLSOptions{ + CertificateAuthorityPath: certificateAuthorityPath, + }, + } + return option.Options{ + Endpoints: []option.Endpoint{ + { + Type: C.TypeOpenConnect, + Tag: "openconnect-client", + Options: &endpointOptions, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + }, + } +} + +func requireOpenConnectEndpoint(t *testing.T, instance *box.Box) adapter.OpenConnectEndpoint { + t.Helper() + endpoint, loaded := instance.Endpoint().Get("openconnect-client") + require.True(t, loaded) + openConnectEndpoint, supported := endpoint.(adapter.OpenConnectEndpoint) + require.True(t, supported) + return openConnectEndpoint +} + +func waitForOpenConnectState(t *testing.T, endpoint adapter.OpenConnectEndpoint, expectedState string, timeout time.Duration) adapter.OpenConnectStatus { + t.Helper() + timeoutTimer := time.NewTimer(timeout) + defer timeoutTimer.Stop() + for { + statusUpdated := endpoint.StatusUpdated() + status := endpoint.OpenConnectStatus() + if status.State == expectedState { + return status + } + if status.State == adapter.OpenConnectStateError { + t.Fatalf("OpenConnect endpoint failed while waiting for %q: %s", expectedState, status.Error) + } + select { + case <-statusUpdated: + case <-timeoutTimer.C: + t.Fatalf("timed out waiting for OpenConnect state %q; last state %q, error %q", expectedState, status.State, status.Error) + } + } +} + +func driveOpenConnectInteractiveAuthentication(t *testing.T, endpoint adapter.OpenConnectEndpoint, timeout time.Duration) { + t.Helper() + timeoutTimer := time.NewTimer(timeout) + defer timeoutTimer.Stop() + completedForms := make(map[string]struct{}) + sawUsername := false + sawPassword := false + for { + statusUpdated := endpoint.StatusUpdated() + status := endpoint.OpenConnectStatus() + switch status.State { + case adapter.OpenConnectStateConnected: + require.True(t, sawUsername) + require.True(t, sawPassword) + return + case adapter.OpenConnectStateError: + t.Fatal(status.Error) + case adapter.OpenConnectStateAuthPending: + form := status.AuthForm + require.NotNil(t, form) + require.NotEmpty(t, form.ID) + _, completed := completedForms[form.ID] + if !completed { + values := make(map[string]string, len(form.Fields)) + for _, field := range form.Fields { + require.NotEmpty(t, field.SubmissionKey) + switch field.Name { + case "username": + sawUsername = true + values[field.SubmissionKey] = openConnectUsername + case "password": + sawPassword = true + values[field.SubmissionKey] = openConnectPassword + default: + t.Fatalf("unexpected ocserv authentication field: %#v", field) + } + } + require.NotEmpty(t, values) + err := endpoint.CompleteAuthForm(form.ID, values) + require.NoError(t, err) + completedForms[form.ID] = struct{}{} + continue + } + } + select { + case <-statusUpdated: + case <-timeoutTimer.C: + t.Fatalf("timed out driving OpenConnect authentication; last state %q, error %q", status.State, status.Error) + } + } +} + +func exchangeOpenConnectTCPEcho(endpoint adapter.OpenConnectEndpoint, payloadSize int, timeout time.Duration) error { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + conn, err := endpoint.DialContext(ctx, N.NetworkTCP, M.ParseSocksaddrHostPort(openConnectTunnelAddress, openConnectEchoPort)) + if err != nil { + return E.Cause(err, "dial ocserv tunnel echo") + } + defer conn.Close() + err = conn.SetDeadline(time.Now().Add(timeout)) + if err != nil { + return E.Cause(err, "set ocserv tunnel echo deadline") + } + payload := make([]byte, payloadSize) + _, err = rand.Read(payload) + if err != nil { + return E.Cause(err, "generate ocserv tunnel echo payload") + } + written := 0 + for written < len(payload) { + var n int + n, err = conn.Write(payload[written:]) + if err != nil { + return E.Cause(err, "write ocserv tunnel echo payload") + } + written += n + } + response := make([]byte, len(payload)) + _, err = io.ReadFull(conn, response) + if err != nil { + return E.Cause(err, "read ocserv tunnel echo payload") + } + if !bytes.Equal(response, payload) { + return E.New("ocserv tunnel echo payload mismatch") + } + return nil +} + +func waitForOpenConnectTCPEcho(t *testing.T, endpoint adapter.OpenConnectEndpoint, timeout time.Duration) { + t.Helper() + deadline := time.Now().Add(timeout) + var lastErr error + for time.Now().Before(deadline) { + lastErr = exchangeOpenConnectTCPEcho(endpoint, 4096, 3*time.Second) + if lastErr == nil { + return + } + time.Sleep(100 * time.Millisecond) + } + if lastErr == nil { + t.Fatal("timed out before attempting OpenConnect tunnel echo") + } + t.Fatal(E.Cause(lastErr, "timed out waiting for OpenConnect tunnel echo")) +} + +func requireOpenConnectDockerImage(t *testing.T, ctx context.Context) { + t.Helper() + _, err := openConnectDockerOutput(ctx, "version", "--format", "{{.Server.Version}}") + require.NoError(t, err) + buildContext, err := filepath.Abs(filepath.Join("testdata", "openconnect", "ocserv")) + require.NoError(t, err) + _, err = openConnectDockerOutput(ctx, "build", "--pull=false", "--tag", openConnectOcservImage, buildContext) + require.NoError(t, err) +} + +func startOpenConnectOcservContainer(t *testing.T, ctx context.Context) openConnectOcservContainer { + t.Helper() + certificateAuthorityPath, certificatePath, keyPath := createSelfSignedCertificate(t, "localhost") + workspace := t.TempDir() + err := os.Chmod(workspace, 0o755) + require.NoError(t, err) + certificate, err := os.ReadFile(certificatePath) + require.NoError(t, err) + key, err := os.ReadFile(keyPath) + require.NoError(t, err) + serverCertificatePath := filepath.Join(workspace, "server-cert.pem") + serverKeyPath := filepath.Join(workspace, "server-key.pem") + passwordPath := filepath.Join(workspace, "ocpasswd") + err = os.WriteFile(serverCertificatePath, certificate, 0o644) + require.NoError(t, err) + err = os.WriteFile(serverKeyPath, key, 0o600) + require.NoError(t, err) + err = os.WriteFile(passwordPath, []byte(openConnectOcservPasswordFile), 0o644) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(workspace, "ocserv.conf"), []byte(openConnectOcservConfiguration), 0o644) + require.NoError(t, err) + + containerName := "sing-box-openconnect-" + strconv.FormatInt(time.Now().UnixNano(), 10) + _, err = openConnectDockerOutput( + ctx, + "run", "--detach", "--rm", "--name", containerName, + "--cap-add", "NET_ADMIN", "--device", "/dev/net/tun", + "--publish", "127.0.0.1::443/tcp", + "--mount", "type=bind,source="+workspace+",target=/fixture", + "--entrypoint", "sh", + openConnectOcservImage, + "-c", "python3 /usr/local/bin/openconnect-echo-server & exec ocserv -f -d 4 -c /fixture/ocserv.conf", + ) + require.NoError(t, err) + t.Cleanup(func() { + if t.Failed() { + logsContext, cancelLogs := context.WithTimeout(context.Background(), 5*time.Second) + logs, logsErr := openConnectDockerOutput(logsContext, "logs", containerName) + cancelLogs() + if logsErr == nil { + t.Log("ocserv logs:\n" + logs) + } + } + removeContext, cancelRemove := context.WithTimeout(context.Background(), 5*time.Second) + _, _ = openConnectDockerOutput(removeContext, "rm", "--force", containerName) + cancelRemove() + }) + waitForOpenConnectContainerLog(t, ctx, containerName, "openconnect echo ready") + tcpAddress := openConnectDockerPublishedAddress(t, ctx, containerName, "443/tcp") + waitForOpenConnectTCP(t, ctx, containerName, tcpAddress) + versionOutput, err := openConnectDockerOutput(ctx, "exec", containerName, "dpkg-query", "-W", "-f=${Version}", "ocserv") + require.NoError(t, err) + require.Equal(t, openConnectOcservVersion, strings.TrimSpace(versionOutput)) + return openConnectOcservContainer{ + name: containerName, + tcpAddress: tcpAddress, + serverAddress: openConnectLocalhostAddress(t, tcpAddress), + certificateAuthorityPath: certificateAuthorityPath, + passwordPath: passwordPath, + } +} + +func openConnectDockerPublishedAddress(t *testing.T, ctx context.Context, containerName string, port string) string { + t.Helper() + for { + output, err := openConnectDockerOutput(ctx, "port", containerName, port) + if err == nil { + address := strings.TrimSpace(output) + _, _, splitErr := net.SplitHostPort(address) + if splitErr == nil { + return address + } + } + select { + case <-ctx.Done(): + t.Fatal(E.Cause(ctx.Err(), "wait for Docker published address")) + case <-time.After(100 * time.Millisecond): + } + } +} + +func openConnectLocalhostAddress(t *testing.T, address string) string { + t.Helper() + _, port, err := net.SplitHostPort(address) + require.NoError(t, err) + return net.JoinHostPort("localhost", port) +} + +func waitForOpenConnectContainerLog(t *testing.T, ctx context.Context, containerName string, expected string) { + t.Helper() + for { + logs, logsErr := openConnectDockerOutput(ctx, "logs", containerName) + if logsErr == nil && strings.Contains(logs, expected) { + return + } + running, inspectErr := openConnectDockerOutput(ctx, "inspect", "--format", "{{.State.Running}}", containerName) + if inspectErr == nil && strings.TrimSpace(running) != "true" { + t.Fatalf("ocserv container exited while waiting for %q:\n%s", expected, logs) + } + select { + case <-ctx.Done(): + t.Fatal(E.Cause(ctx.Err(), "wait for ocserv container log ", expected)) + case <-time.After(100 * time.Millisecond): + } + } +} + +func waitForOpenConnectTCP(t *testing.T, ctx context.Context, containerName string, address string) { + t.Helper() + for { + conn, err := net.DialTimeout("tcp", address, 250*time.Millisecond) + if err == nil { + _ = conn.Close() + return + } + running, inspectErr := openConnectDockerOutput(ctx, "inspect", "--format", "{{.State.Running}}", containerName) + if inspectErr == nil && strings.TrimSpace(running) != "true" { + logs, _ := openConnectDockerOutput(ctx, "logs", containerName) + t.Fatalf("ocserv container exited before TCP readiness:\n%s", logs) + } + select { + case <-ctx.Done(): + t.Fatal(E.Cause(ctx.Err(), "wait for ocserv TCP listener")) + case <-time.After(100 * time.Millisecond): + } + } +} + +func openConnectDockerOutput(ctx context.Context, arguments ...string) (string, error) { + command := exec.CommandContext(ctx, "docker", arguments...) + output, err := command.CombinedOutput() + if err != nil { + return "", E.Cause(err, "docker ", strings.Join(arguments, " "), ": ", strings.TrimSpace(string(output))) + } + return string(output), nil +} + +func startOpenConnectTCPProxy(t *testing.T, target string) *openConnectTCPProxy { + t.Helper() + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + proxy := &openConnectTCPProxy{ + listener: listener, + target: target, + connections: make(map[*openConnectTCPProxyConnection]struct{}), + } + go proxy.acceptLoop() + t.Cleanup(proxy.close) + return proxy +} + +func (p *openConnectTCPProxy) acceptLoop() { + for { + downstream, err := p.listener.Accept() + if err != nil { + return + } + upstream, err := net.DialTimeout("tcp", p.target, 5*time.Second) + if err != nil { + _ = downstream.Close() + continue + } + connection := &openConnectTCPProxyConnection{ + proxy: p, + downstream: downstream, + upstream: upstream, + } + p.access.Lock() + if p.closed { + p.access.Unlock() + connection.close() + return + } + p.connections[connection] = struct{}{} + p.accepted.Add(1) + p.access.Unlock() + go connection.copy(upstream, downstream) + go connection.copy(downstream, upstream) + } +} + +func (c *openConnectTCPProxyConnection) copy(destination net.Conn, source net.Conn) { + _, _ = io.Copy(destination, source) + c.close() +} + +func (c *openConnectTCPProxyConnection) close() { + c.closeOnce.Do(func() { + _ = c.downstream.Close() + _ = c.upstream.Close() + c.proxy.access.Lock() + delete(c.proxy.connections, c) + c.proxy.access.Unlock() + }) +} + +func (p *openConnectTCPProxy) dropConnections() int { + p.access.Lock() + connections := make([]*openConnectTCPProxyConnection, 0, len(p.connections)) + for connection := range p.connections { + connections = append(connections, connection) + } + p.access.Unlock() + for _, connection := range connections { + connection.close() + } + return len(connections) +} + +func (p *openConnectTCPProxy) close() { + p.access.Lock() + p.closed = true + p.access.Unlock() + _ = p.listener.Close() + p.dropConnections() +} + +func waitForOpenConnectProxyAccept(t *testing.T, proxy *openConnectTCPProxy, previous uint64, timeout time.Duration) { + t.Helper() + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + if proxy.accepted.Load() > previous { + return + } + time.Sleep(50 * time.Millisecond) + } + t.Fatalf("OpenConnect proxy accepted %d connections, expected more than %d after CSTP drop", proxy.accepted.Load(), previous) +} diff --git a/test/openvpn_test.go b/test/openvpn_test.go new file mode 100644 index 000000000..1d1b02983 --- /dev/null +++ b/test/openvpn_test.go @@ -0,0 +1,1797 @@ +package main + +import ( + "archive/tar" + "bytes" + "context" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/hex" + "encoding/pem" + "io" + "net" + "net/netip" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/sagernet/sing-box" + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json/badoption" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/protocol/socks" + + typesapi "github.com/docker/docker/api/types" + containerapi "github.com/docker/docker/api/types/container" + "github.com/docker/docker/client" + "github.com/docker/docker/errdefs" + "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/pkg/stdcopy" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/require" +) + +const ( + openVPNTLSUsername = "test-user" + openVPNTLSPassword = "test-password" + openVPNStaticChallengeText = "Enter OTP" + openVPNStaticChallengeResponse = "31337" + openVPNLargeDataTimeout = 3 * time.Minute + openVPNLargeTCPPackets = 1 + openVPNLargeTCPSize = 2048 + + openVPNDockerImage = "sing-box-openvpn-2.6.14-options:test" + openVPNDockerPackageVersion = "2.6.14-0+deb12u2" + openVPNDockerRoot = "/config" +) + +var ( + openVPNDockerImageOnce sync.Once + openVPNDockerImageErr error +) + +type openVPNCertificateBundle struct { + caPath string + serverCertPath string + serverKeyPath string + clientCertPath string + clientKeyPath string +} + +type openVPNDockerServerEnvironment struct { + certificates openVPNCertificateBundle + workspace string + openVPNPort uint16 + echoPort uint16 + container *openVPNDockerContainer +} + +type openVPNSelfCase struct { + name string + protocol string + tlsCrypt bool +} + +func TestOpenVPNSelfToSelf(t *testing.T) { + testCases := []openVPNSelfCase{ + { + name: "real_tls_udp", + protocol: N.NetworkUDP, + }, + { + name: "real_tls_tcp", + protocol: N.NetworkTCP, + }, + { + name: "real_tls_udp_tls_crypt", + protocol: N.NetworkUDP, + tlsCrypt: true, + }, + } + for i := range testCases { + currentTestCase := testCases[i] + t.Run(currentTestCase.name, func(t *testing.T) { + runOpenVPNSelfToSelf(t, currentTestCase) + }) + } +} + +func TestOpenVPNDockerInterop(t *testing.T) { + t.Run("official_server_to_sing_box_client", func(t *testing.T) { + testOpenVPNDockerOfficialServerToSingBoxClient(t) + }) + t.Run("official_client_to_sing_box_server", func(t *testing.T) { + testOpenVPNDockerOfficialClientToSingBoxServer(t) + }) +} + +func runOpenVPNSelfToSelf(t *testing.T, testCase openVPNSelfCase) { + t.Helper() + const serverAddress = "10.8.0.1" + serverPrefix := netip.MustParsePrefix(serverAddress + "/24") + proxyPort := reserveOpenVPNTCPPort(t) + openVPNPort := reserveOpenVPNProtocolPort(t, testCase.protocol) + echoPort := reserveOpenVPNEchoPort(t) + readinessPort := reserveOpenVPNEchoPort(t) + certificates := createOpenVPNCertificateBundle(t) + serverOptions := option.OpenVPNServerEndpointOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.MustParseAddr("127.0.0.1"))), + ListenPort: openVPNPort, + }, + Network: testCase.protocol, + Address: []netip.Prefix{serverPrefix}, + Users: []auth.User{ + { + Username: openVPNTLSUsername, + Password: openVPNTLSPassword, + }, + }, + TLS: &option.OpenVPNInboundTLSOptions{ + CertificatePath: certificates.serverCertPath, + KeyPath: certificates.serverKeyPath, + ClientCertificatePath: certificates.caPath, + }, + } + clientOptions := newOpenVPNTLSClientOptions(testCase.protocol, openVPNPort, certificates.caPath, certificates.clientCertPath, certificates.clientKeyPath) + if testCase.tlsCrypt { + tlsCryptKeyPath := writeOpenVPNStaticKeyFile(t, createOpenVPNStaticKey(t)) + serverOptions.TLS.ControlWrap = &option.OpenVPNControlWrapOptions{ + Type: "tls_crypt", + KeyPath: tlsCryptKeyPath, + } + clientOptions.TLS.ControlWrap = &option.OpenVPNControlWrapOptions{ + Type: "tls_crypt", + KeyPath: tlsCryptKeyPath, + } + } + clientOptions.Username = openVPNTLSUsername + clientOptions.Password = openVPNTLSPassword + + startInstance(t, openVPNServerInstanceOptions(serverOptions)) + startInstance(t, openVPNClientInstanceOptions(clientOptions, proxyPort)) + waitForOpenVPNClientReady(t, proxyPort, readinessPort, serverAddress) + testSuitOpenVPN(t, proxyPort, echoPort, serverAddress) +} + +func testOpenVPNDockerOfficialServerToSingBoxClient(t *testing.T) { + t.Helper() + environment := startOpenVPNDockerOfficialServer(t, "official-server") + proxyPort := reserveOpenVPNTCPPort(t) + + clientOptions := newOpenVPNTLSClientOptions( + N.NetworkUDP, + environment.openVPNPort, + filepath.Join(environment.workspace, "ca.crt"), + filepath.Join(environment.workspace, "client.crt"), + filepath.Join(environment.workspace, "client.key"), + ) + clientOptions.Username = openVPNTLSUsername + clientOptions.Password = openVPNTLSPassword + startInstance(t, openVPNClientInstanceOptions(clientOptions, proxyPort)) + waitForOpenVPNRemoteReady(t, proxyPort, "10.8.0.1", environment.echoPort, 30*time.Second) + testRemoteEchoThroughSocks(t, proxyPort, "10.8.0.1", environment.echoPort) +} + +func TestOpenVPNDockerMultiRemoteFailover(t *testing.T) { + environment := startOpenVPNDockerOfficialServer(t, "multi-remote-server") + deadPort := reserveOpenVPNTCPPort(t) + proxyPort := reserveOpenVPNTCPPort(t) + clientOptions := newOpenVPNTLSClientOptions( + N.NetworkUDP, + environment.openVPNPort, + filepath.Join(environment.workspace, "ca.crt"), + filepath.Join(environment.workspace, "client.crt"), + filepath.Join(environment.workspace, "client.key"), + ) + clientOptions.Server = "" + clientOptions.ServerPort = 0 + clientOptions.Servers = []option.OpenVPNRemoteOptions{ + { + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: deadPort, + }, + Network: N.NetworkTCP, + }, + { + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: environment.openVPNPort, + }, + Network: N.NetworkUDP, + }, + } + clientOptions.Username = openVPNTLSUsername + clientOptions.Password = openVPNTLSPassword + startInstance(t, openVPNClientInstanceOptions(clientOptions, proxyPort)) + waitForOpenVPNRemoteReady(t, proxyPort, "10.8.0.1", environment.echoPort, 30*time.Second) + testRemoteEchoThroughSocks(t, proxyPort, "10.8.0.1", environment.echoPort) +} + +func TestOpenVPNDockerMSSFix(t *testing.T) { + dockerClient := requireOpenVPNDockerEnvironment(t) + certificates := createOpenVPNCertificateBundle(t) + workspace := newOpenVPNDockerWorkspace(t, certificates) + openVPNPort := reserveOpenVPNUDPPort(t) + proxyPort := reserveOpenVPNTCPPort(t) + echoPort := reserveOpenVPNEchoPort(t) + writeOpenVPNDockerServerConfig(t, workspace, openVPNPort, "/config/check_userpass.sh", "mssfix 1200") + writeOpenVPNDockerEchoServer(t, workspace, "10.8.0.1", echoPort) + serverCommand := strings.Join([]string{ + "python3 /config/echo_server.py &", + "openvpn --config /config/server.conf &", + "openvpn_pid=$!", + "until grep -q 'Initialization Sequence Completed' /config/openvpn.log; do", + " if ! kill -0 \"$openvpn_pid\" 2>/dev/null; then cat /config/openvpn.log; exit 1; fi", + " sleep 0.1", + "done", + "tcpdump -i tun0 -l -nn -v 'dst host 10.8.0.1 and tcp[tcpflags] & tcp-syn != 0' > /config/mss.log 2>&1 &", + "wait \"$openvpn_pid\"", + }, "\n") + serverContainer := startOpenVPNDockerContainer(t, dockerClient, "mss-fix-server", workspace, serverCommand) + dumpOpenVPNDockerLogsOnFailure(t, serverContainer, workspace) + waitForOpenVPNDockerFile(t, serverContainer, filepath.Join(workspace, "openvpn.log"), "Initialization Sequence Completed", 30*time.Second) + waitForOpenVPNDockerFile(t, serverContainer, filepath.Join(workspace, "echo.ready"), "ready", 30*time.Second) + waitForOpenVPNDockerFile(t, serverContainer, filepath.Join(workspace, "mss.log"), "listening on tun0", 30*time.Second) + + clientOptions := newOpenVPNTLSClientOptions( + N.NetworkUDP, + openVPNPort, + filepath.Join(workspace, "ca.crt"), + filepath.Join(workspace, "client.crt"), + filepath.Join(workspace, "client.key"), + ) + clientOptions.Username = openVPNTLSUsername + clientOptions.Password = openVPNTLSPassword + clientOptions.MSSFix = 1200 + startInstance(t, openVPNClientInstanceOptions(clientOptions, proxyPort)) + waitForOpenVPNRemoteReady(t, proxyPort, "10.8.0.1", echoPort, 30*time.Second) + testRemoteEchoThroughSocks(t, proxyPort, "10.8.0.1", echoPort) + waitForOpenVPNDockerFile(t, serverContainer, filepath.Join(workspace, "mss.log"), "mss 1136", 30*time.Second) +} + +func TestOpenVPNDockerCompressionNegotiation(t *testing.T) { + environment := startOpenVPNDockerOfficialServer(t, "compression-server", + "allow-compression no", + "compress stub", + "push \"compress stub\"", + ) + proxyPort := reserveOpenVPNTCPPort(t) + clientOptions := newOpenVPNTLSClientOptions( + N.NetworkUDP, + environment.openVPNPort, + filepath.Join(environment.workspace, "ca.crt"), + filepath.Join(environment.workspace, "client.crt"), + filepath.Join(environment.workspace, "client.key"), + ) + clientOptions.Username = openVPNTLSUsername + clientOptions.Password = openVPNTLSPassword + clientOptions.Compression = "stub" + clientOptions.AllowCompression = "asym" + startInstance(t, openVPNClientInstanceOptions(clientOptions, proxyPort)) + waitForOpenVPNRemoteReady(t, proxyPort, "10.8.0.1", environment.echoPort, 30*time.Second) + testRemoteEchoThroughSocks(t, proxyPort, "10.8.0.1", environment.echoPort) + waitForOpenVPNDockerFile(t, environment.container, filepath.Join(environment.workspace, "openvpn.log"), "IV_LZO=1", 30*time.Second) +} + +func TestOpenVPNDockerPeerFingerprint(t *testing.T) { + environment := startOpenVPNDockerOfficialServer(t, "peer-fingerprint-server") + fingerprint := openVPNCertificateSHA256Fingerprint(t, environment.certificates.serverCertPath) + wrongFingerprint := "0" + fingerprint[1:] + if fingerprint[0] == '0' { + wrongFingerprint = "1" + fingerprint[1:] + } + + t.Run("matching", func(matchingTest *testing.T) { + matchingProxyPort := reserveOpenVPNTCPPort(matchingTest) + matchingClientOptions := newOpenVPNTLSClientOptions( + N.NetworkUDP, + environment.openVPNPort, + "", + filepath.Join(environment.workspace, "client.crt"), + filepath.Join(environment.workspace, "client.key"), + ) + matchingClientOptions.Username = openVPNTLSUsername + matchingClientOptions.Password = openVPNTLSPassword + matchingClientOptions.TLS.PeerFingerprint = []string{fingerprint} + startInstance(matchingTest, openVPNClientInstanceOptions(matchingClientOptions, matchingProxyPort)) + waitForOpenVPNRemoteReady(matchingTest, matchingProxyPort, "10.8.0.1", environment.echoPort, 30*time.Second) + testRemoteEchoThroughSocks(matchingTest, matchingProxyPort, "10.8.0.1", environment.echoPort) + }) + + t.Run("mismatch", func(mismatchTest *testing.T) { + mismatchProxyPort := reserveOpenVPNTCPPort(mismatchTest) + mismatchClientOptions := newOpenVPNTLSClientOptions( + N.NetworkUDP, + environment.openVPNPort, + "", + filepath.Join(environment.workspace, "client.crt"), + filepath.Join(environment.workspace, "client.key"), + ) + mismatchClientOptions.Username = openVPNTLSUsername + mismatchClientOptions.Password = openVPNTLSPassword + mismatchClientOptions.TLS.PeerFingerprint = []string{wrongFingerprint} + mismatchInstance := startInstance(mismatchTest, openVPNClientInstanceOptions(mismatchClientOptions, mismatchProxyPort)) + mismatchEndpoint := requireOpenVPNEndpoint(mismatchTest, mismatchInstance, "openvpn-client") + mismatchStatus := waitForOpenVPNStatus(mismatchTest, mismatchEndpoint, 30*time.Second, func(status adapter.OpenVPNStatus) bool { + return status.State == adapter.OpenVPNStateError + }) + require.Contains(mismatchTest, mismatchStatus.Error, "peer fingerprint mismatch") + }) +} + +func TestOpenVPNDockerClientRoutes(t *testing.T) { + environment := startOpenVPNDockerOfficialServer(t, "client-routes-server") + localRoute := netip.MustParsePrefix("192.0.2.0/24") + + t.Run("routes", func(routesTest *testing.T) { + routesProxyPort := reserveOpenVPNTCPPort(routesTest) + routesClientOptions := newOpenVPNTLSClientOptions( + N.NetworkUDP, + environment.openVPNPort, + filepath.Join(environment.workspace, "ca.crt"), + filepath.Join(environment.workspace, "client.crt"), + filepath.Join(environment.workspace, "client.key"), + ) + routesClientOptions.Username = openVPNTLSUsername + routesClientOptions.Password = openVPNTLSPassword + routesClientOptions.Routes = []netip.Prefix{localRoute} + routesInstance := startInstance(routesTest, openVPNClientInstanceOptions(routesClientOptions, routesProxyPort)) + waitForOpenVPNRemoteReady(routesTest, routesProxyPort, "10.8.0.1", environment.echoPort, 30*time.Second) + routesEndpoint, loaded := routesInstance.Endpoint().Get("openvpn-client") + require.True(routesTest, loaded) + preferredRoutes, supported := routesEndpoint.(adapter.OutboundWithPreferredRoutes) + require.True(routesTest, supported) + require.True(routesTest, preferredRoutes.PreferredAddress(nil, netip.MustParseAddr("192.0.2.1"))) + require.False(routesTest, preferredRoutes.PreferredAddress(nil, netip.MustParseAddr("198.51.100.1"))) + }) + + t.Run("redirect_gateway", func(redirectTest *testing.T) { + redirectProxyPort := reserveOpenVPNTCPPort(redirectTest) + redirectClientOptions := newOpenVPNTLSClientOptions( + N.NetworkUDP, + environment.openVPNPort, + filepath.Join(environment.workspace, "ca.crt"), + filepath.Join(environment.workspace, "client.crt"), + filepath.Join(environment.workspace, "client.key"), + ) + redirectClientOptions.Username = openVPNTLSUsername + redirectClientOptions.Password = openVPNTLSPassword + redirectClientOptions.Routes = []netip.Prefix{localRoute} + redirectClientOptions.RedirectGateway = true + redirectClientOptions.RedirectGatewayFlags = []string{"def1"} + redirectInstance := startInstance(redirectTest, openVPNClientInstanceOptions(redirectClientOptions, redirectProxyPort)) + waitForOpenVPNRemoteReady(redirectTest, redirectProxyPort, "10.8.0.1", environment.echoPort, 30*time.Second) + redirectEndpoint, loaded := redirectInstance.Endpoint().Get("openvpn-client") + require.True(redirectTest, loaded) + preferredRoutes, supported := redirectEndpoint.(adapter.OutboundWithPreferredRoutes) + require.True(redirectTest, supported) + require.True(redirectTest, preferredRoutes.PreferredAddress(nil, netip.MustParseAddr("192.0.2.1"))) + require.True(redirectTest, preferredRoutes.PreferredAddress(nil, netip.MustParseAddr("198.51.100.1"))) + require.False(redirectTest, preferredRoutes.PreferredAddress(nil, netip.MustParseAddr("2001:db8::1"))) + }) +} + +func startOpenVPNDockerOfficialServer(t *testing.T, nameSuffix string, serverDirectives ...string) openVPNDockerServerEnvironment { + t.Helper() + dockerClient := requireOpenVPNDockerEnvironment(t) + certificates := createOpenVPNCertificateBundle(t) + workspace := newOpenVPNDockerWorkspace(t, certificates) + openVPNPort := reserveOpenVPNUDPPort(t) + echoPort := reserveOpenVPNEchoPort(t) + writeOpenVPNDockerServerConfig(t, workspace, openVPNPort, "/config/check_userpass.sh", serverDirectives...) + writeOpenVPNDockerEchoServer(t, workspace, "10.8.0.1", echoPort) + serverContainer := startOpenVPNDockerContainer(t, dockerClient, nameSuffix, workspace, "python3 /config/echo_server.py & exec openvpn --config /config/server.conf") + dumpOpenVPNDockerLogsOnFailure(t, serverContainer, workspace) + waitForOpenVPNDockerFile(t, serverContainer, filepath.Join(workspace, "openvpn.log"), "Initialization Sequence Completed", 30*time.Second) + waitForOpenVPNDockerFile(t, serverContainer, filepath.Join(workspace, "echo.ready"), "ready", 30*time.Second) + return openVPNDockerServerEnvironment{ + certificates: certificates, + workspace: workspace, + openVPNPort: openVPNPort, + echoPort: echoPort, + container: serverContainer, + } +} + +func TestOpenVPNInteractiveAuth(t *testing.T) { + dockerClient := requireOpenVPNDockerEnvironment(t) + certificates := createOpenVPNCertificateBundle(t) + workspace := newOpenVPNDockerWorkspace(t, certificates) + openVPNPort := reserveOpenVPNUDPPort(t) + proxyPort := reserveOpenVPNTCPPort(t) + echoPort := reserveOpenVPNEchoPort(t) + + writeOpenVPNDockerStaticChallengeScript(t, workspace) + writeOpenVPNDockerServerConfig(t, workspace, openVPNPort, "/config/check_scrv1.sh") + writeOpenVPNDockerEchoServer(t, workspace, "10.8.0.1", echoPort) + serverContainer := startOpenVPNDockerContainer(t, dockerClient, "interactive-auth-server", workspace, "python3 /config/echo_server.py & exec openvpn --config /config/server.conf") + dumpOpenVPNDockerLogsOnFailure(t, serverContainer, workspace) + waitForOpenVPNDockerFile(t, serverContainer, filepath.Join(workspace, "openvpn.log"), "Initialization Sequence Completed", 30*time.Second) + waitForOpenVPNDockerFile(t, serverContainer, filepath.Join(workspace, "echo.ready"), "ready", 30*time.Second) + + clientOptions := newOpenVPNTLSClientOptions( + N.NetworkUDP, + openVPNPort, + filepath.Join(workspace, "ca.crt"), + filepath.Join(workspace, "client.crt"), + filepath.Join(workspace, "client.key"), + ) + clientOptions.StaticChallenge = openVPNStaticChallengeText + clientOptions.StaticChallengeEcho = true + clientInstance := startInstance(t, openVPNClientInstanceOptions(clientOptions, proxyPort)) + clientEndpoint := requireOpenVPNEndpoint(t, clientInstance, "openvpn-client") + + challengeStatus := waitForOpenVPNStatus(t, clientEndpoint, 30*time.Second, func(status adapter.OpenVPNStatus) bool { + require.NotEqual(t, adapter.OpenVPNStateConnected, status.State) + require.NotEqual(t, adapter.OpenVPNStateError, status.State, status.Error) + return status.State == adapter.OpenVPNStateAuthPending + }) + challenge := challengeStatus.Challenge + require.NotNil(t, challenge) + require.NotEmpty(t, challenge.ID) + require.Equal(t, "credentials", challenge.Kind) + require.Equal(t, openVPNStaticChallengeText, challenge.SecretMessage) + require.True(t, challenge.Echo) + require.Empty(t, challenge.PreviousError) + + err := clientEndpoint.CompleteChallenge(challenge.ID, adapter.OpenVPNChallengeResponse{ + Username: openVPNTLSUsername, + Password: openVPNTLSPassword, + Secret: openVPNStaticChallengeResponse, + }) + require.NoError(t, err) + + waitForOpenVPNStatus(t, clientEndpoint, time.Minute, func(status adapter.OpenVPNStatus) bool { + require.NotEqual(t, adapter.OpenVPNStateError, status.State, status.Error) + return status.State == adapter.OpenVPNStateConnected + }) + waitForOpenVPNRemoteReady(t, proxyPort, "10.8.0.1", echoPort, 30*time.Second) + testRemoteEchoThroughSocks(t, proxyPort, "10.8.0.1", echoPort) +} + +func writeOpenVPNDockerStaticChallengeScript(t *testing.T, workspace string) { + t.Helper() + script := strings.Join([]string{ + "#!/bin/bash", + "set -eu", + "credentials_file=\"$1\"", + "username=\"$(sed -n '1p' \"$credentials_file\")\"", + "password=\"$(sed -n '2p' \"$credentials_file\")\"", + "[ \"$username\" = \"" + openVPNTLSUsername + "\" ] || exit 1", + "case \"$password\" in", + "SCRV1:*) ;;", + "*) exit 1 ;;", + "esac", + "encoded_password=\"$(printf '%s' \"$password\" | cut -d: -f2)\"", + "encoded_response=\"$(printf '%s' \"$password\" | cut -d: -f3)\"", + "[ \"$(printf '%s' \"$encoded_password\" | base64 -d)\" = \"" + openVPNTLSPassword + "\" ] || exit 1", + "[ \"$(printf '%s' \"$encoded_response\" | base64 -d)\" = \"" + openVPNStaticChallengeResponse + "\" ] || exit 1", + "exit 0", + "", + }, "\n") + err := os.WriteFile(filepath.Join(workspace, "check_scrv1.sh"), []byte(script), 0o700) + require.NoError(t, err) +} + +func requireOpenVPNEndpoint(t *testing.T, instance *box.Box, tag string) adapter.OpenVPNEndpoint { + t.Helper() + endpoint, loaded := instance.Endpoint().Get(tag) + require.True(t, loaded) + openVPNEndpoint, supported := endpoint.(adapter.OpenVPNEndpoint) + require.True(t, supported) + return openVPNEndpoint +} + +func waitForOpenVPNStatus(t *testing.T, endpoint adapter.OpenVPNEndpoint, timeout time.Duration, predicate func(status adapter.OpenVPNStatus) bool) adapter.OpenVPNStatus { + t.Helper() + timeoutChannel := time.After(timeout) + for { + statusUpdated := endpoint.StatusUpdated() + status := endpoint.OpenVPNStatus() + if predicate(status) { + return status + } + select { + case <-statusUpdated: + case <-timeoutChannel: + t.Fatalf("timed out waiting for OpenVPN endpoint status, last state %q, error %q", status.State, status.Error) + } + } +} + +func TestOpenVPNClientReconnectSelfToSelf(t *testing.T) { + const serverAddress = "10.8.0.1" + serverPrefix := netip.MustParsePrefix(serverAddress + "/24") + proxyPort := reserveOpenVPNTCPPort(t) + openVPNPort := reserveOpenVPNUDPPort(t) + echoPort := reserveOpenVPNEchoPort(t) + readinessPort := reserveOpenVPNEchoPort(t) + certificates := createOpenVPNCertificateBundle(t) + serverOptions := option.OpenVPNServerEndpointOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.MustParseAddr("127.0.0.1"))), + ListenPort: openVPNPort, + }, + Network: N.NetworkUDP, + Address: []netip.Prefix{serverPrefix}, + TLS: &option.OpenVPNInboundTLSOptions{ + CertificatePath: certificates.serverCertPath, + KeyPath: certificates.serverKeyPath, + ClientCertificatePath: certificates.caPath, + }, + KeepaliveInterval: badoption.Duration(time.Second), + KeepaliveTimeout: badoption.Duration(2 * time.Second), + Users: []auth.User{ + { + Username: openVPNTLSUsername, + Password: openVPNTLSPassword, + }, + }, + } + clientOptions := newOpenVPNTLSClientOptions(N.NetworkUDP, openVPNPort, certificates.caPath, certificates.clientCertPath, certificates.clientKeyPath) + clientOptions.Username = openVPNTLSUsername + clientOptions.Password = openVPNTLSPassword + serverInstance := startInstance(t, openVPNServerInstanceOptions(serverOptions)) + startInstance(t, openVPNClientInstanceOptions(clientOptions, proxyPort)) + waitForOpenVPNClientReady(t, proxyPort, readinessPort, serverAddress) + err := serverInstance.Close() + require.NoError(t, err) + startInstance(t, openVPNServerInstanceOptions(serverOptions)) + waitForOpenVPNClientReady(t, proxyPort, readinessPort, serverAddress) + testSuitOpenVPN(t, proxyPort, echoPort, serverAddress) +} + +func testOpenVPNDockerOfficialClientToSingBoxServer(t *testing.T) { + t.Helper() + dockerClient := requireOpenVPNDockerEnvironment(t) + certificates := createOpenVPNCertificateBundle(t) + workspace := newOpenVPNDockerWorkspace(t, certificates) + openVPNPort := reserveOpenVPNUDPPort(t) + echoPort := reserveOpenVPNEchoPort(t) + + startOpenVPNHostEchoServers(t, echoPort) + serverOptions := option.OpenVPNServerEndpointOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.MustParseAddr("127.0.0.1"))), + ListenPort: openVPNPort, + }, + Network: N.NetworkUDP, + Address: []netip.Prefix{netip.MustParsePrefix("10.8.0.1/24")}, + TLS: &option.OpenVPNInboundTLSOptions{ + CertificatePath: filepath.Join(workspace, "server.crt"), + KeyPath: filepath.Join(workspace, "server.key"), + ClientCertificatePath: filepath.Join(workspace, "ca.crt"), + }, + Users: []auth.User{ + { + Username: openVPNTLSUsername, + Password: openVPNTLSPassword, + }, + }, + } + startInstance(t, openVPNServerInstanceOptions(serverOptions)) + + writeOpenVPNDockerClientConfig(t, workspace, openVPNPort) + writeOpenVPNDockerEchoClient(t, workspace, "10.8.0.1", echoPort) + clientCommand := strings.Join([]string{ + "openvpn --config /config/client.conf &", + "openvpn_pid=$!", + "until grep -q 'Initialization Sequence Completed' /config/openvpn.log; do", + " if ! kill -0 \"$openvpn_pid\" 2>/dev/null; then cat /config/openvpn.log; exit 1; fi", + " sleep 0.1", + "done", + "python3 /config/echo_client.py", + "kill \"$openvpn_pid\"", + "wait \"$openvpn_pid\" || true", + }, "\n") + clientContainer := startOpenVPNDockerContainer(t, dockerClient, "official-client", workspace, clientCommand) + dumpOpenVPNDockerLogsOnFailure(t, clientContainer, workspace) + waitResult := clientContainer.Wait(t, 60*time.Second) + require.Equal(t, int64(0), waitResult.exitCode, waitResult.logs) +} + +func waitForOpenVPNClientReady(t *testing.T, proxyPort uint16, echoPort uint16, tunnelAddress string) { + t.Helper() + closeEcho := startOpenVPNReadinessEcho(t, echoPort) + defer closeEcho() + waitForOpenVPNRemoteReady(t, proxyPort, tunnelAddress, echoPort, 3*time.Minute) +} + +func startOpenVPNReadinessEcho(t *testing.T, port uint16) func() { + t.Helper() + listener, err := listen("tcp", ":"+strconv.Itoa(int(port))) + require.NoError(t, err) + done := make(chan struct{}) + go func() { + defer close(done) + for { + conn, acceptErr := listener.Accept() + if acceptErr != nil { + return + } + go echoOpenVPNTCPConnection(conn) + } + }() + return func() { + listener.Close() + <-done + } +} + +func waitForOpenVPNRemoteReady(t *testing.T, proxyPort uint16, tunnelAddress string, tunnelPort uint16, timeout time.Duration) { + t.Helper() + deadline := time.Now().Add(timeout) + var lastErr error + for time.Now().Before(deadline) { + lastErr = probeOpenVPNTCPWithTimeout(proxyPort, tunnelAddress, tunnelPort, 3*time.Second) + if lastErr == nil { + return + } + time.Sleep(100 * time.Millisecond) + } + require.NoError(t, lastErr) +} + +func probeOpenVPNTCPWithTimeout(proxyPort uint16, tunnelAddress string, tunnelPort uint16, timeout time.Duration) error { + resultCh := make(chan error, 1) + go func() { + probeErr := probeOpenVPNTCP(proxyPort, tunnelAddress, tunnelPort) + resultCh <- probeErr + }() + select { + case resultErr := <-resultCh: + return resultErr + case <-time.After(timeout): + return E.New("timeout") + } +} + +func probeOpenVPNTCP(proxyPort uint16, tunnelAddress string, tunnelPort uint16) error { + dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", proxyPort), socks.Version5, "", "") + destination := M.ParseSocksaddrHostPort(tunnelAddress, tunnelPort) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + conn, err := dialer.DialContext(ctx, N.NetworkTCP, destination) + if err != nil { + return err + } + defer conn.Close() + err = conn.SetDeadline(time.Now().Add(time.Second)) + if err != nil { + return err + } + return writeAndReadEcho(conn, []byte("ready")) +} + +func openVPNServerInstanceOptions(serverOptions option.OpenVPNServerEndpointOptions) option.Options { + return option.Options{ + Endpoints: []option.Endpoint{ + { + Type: C.TypeOpenVPNServer, + Tag: "openvpn-server", + Options: &serverOptions, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + }, + } +} + +func openVPNClientInstanceOptions(clientOptions option.OpenVPNClientEndpointOptions, proxyPort uint16) option.Options { + return option.Options{ + Endpoints: []option.Endpoint{ + { + Type: C.TypeOpenVPNClient, + Tag: "openvpn-client", + Options: &clientOptions, + }, + }, + Inbounds: []option.Inbound{ + { + Type: C.TypeSOCKS, + Tag: "socks-in", + Options: &option.SocksInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.MustParseAddr("127.0.0.1"))), + ListenPort: proxyPort, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"socks-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.RouteActionOptions{ + Outbound: "openvpn-client", + }, + }, + }, + }, + }, + }, + } +} + +func newOpenVPNTLSClientOptions(protocol string, port uint16, certificatePath string, clientCertificatePath string, clientKeyPath string) option.OpenVPNClientEndpointOptions { + return option.OpenVPNClientEndpointOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: port, + }, + Network: protocol, + TLS: &option.OpenVPNOutboundTLSOptions{ + CertificatePath: certificatePath, + ClientCertificatePath: clientCertificatePath, + ClientKeyPath: clientKeyPath, + }, + } +} + +func writeOpenVPNStaticKeyFile(t *testing.T, staticKey string) string { + t.Helper() + path := filepath.Join(t.TempDir(), "static.key") + err := os.WriteFile(path, []byte(staticKey), 0o600) + require.NoError(t, err) + return path +} + +func newOpenVPNDockerWorkspace(t *testing.T, certificates openVPNCertificateBundle) string { + t.Helper() + workspace := t.TempDir() + copyOpenVPNFile(t, certificates.caPath, filepath.Join(workspace, "ca.crt"), 0o600) + copyOpenVPNFile(t, certificates.serverCertPath, filepath.Join(workspace, "server.crt"), 0o600) + copyOpenVPNFile(t, certificates.serverKeyPath, filepath.Join(workspace, "server.key"), 0o600) + copyOpenVPNFile(t, certificates.clientCertPath, filepath.Join(workspace, "client.crt"), 0o600) + copyOpenVPNFile(t, certificates.clientKeyPath, filepath.Join(workspace, "client.key"), 0o600) + authFileContent := openVPNTLSUsername + "\n" + openVPNTLSPassword + "\n" + err := os.WriteFile(filepath.Join(workspace, "auth-user-pass.txt"), []byte(authFileContent), 0o600) + require.NoError(t, err) + checkUserPassScript := strings.Join([]string{ + "#!/bin/sh", + "set -eu", + "credentials_file=\"$1\"", + "username=\"$(sed -n '1p' \"$credentials_file\")\"", + "password=\"$(sed -n '2p' \"$credentials_file\")\"", + "[ \"$username\" = \"" + openVPNTLSUsername + "\" ] && [ \"$password\" = \"" + openVPNTLSPassword + "\" ]", + "", + }, "\n") + err = os.WriteFile(filepath.Join(workspace, "check_userpass.sh"), []byte(checkUserPassScript), 0o700) + require.NoError(t, err) + return workspace +} + +func copyOpenVPNFile(t *testing.T, sourcePath string, targetPath string, mode os.FileMode) { + t.Helper() + content, err := os.ReadFile(sourcePath) + require.NoError(t, err) + err = os.WriteFile(targetPath, content, mode) + require.NoError(t, err) +} + +func openVPNCertificateSHA256Fingerprint(t *testing.T, certificatePath string) string { + t.Helper() + certificatePEM, err := os.ReadFile(certificatePath) + require.NoError(t, err) + certificateBlock, _ := pem.Decode(certificatePEM) + require.NotNil(t, certificateBlock) + require.Equal(t, "CERTIFICATE", certificateBlock.Type) + fingerprint := sha256.Sum256(certificateBlock.Bytes) + return hex.EncodeToString(fingerprint[:]) +} + +func writeOpenVPNDockerServerConfig(t *testing.T, workspace string, openVPNPort uint16, verifyScriptPath string, serverDirectives ...string) { + t.Helper() + configLines := []string{ + "port " + strconv.Itoa(int(openVPNPort)), + "proto udp4", + "dev tun", + "topology subnet", + "server 10.8.0.0 255.255.255.0", + "ca /config/ca.crt", + "cert /config/server.crt", + "key /config/server.key", + "dh none", + "persist-key", + "persist-tun", + "verb 4", + "script-security 2", + "auth-user-pass-verify " + verifyScriptPath + " via-file", + "explicit-exit-notify 1", + } + configLines = append(configLines, serverDirectives...) + configLines = append(configLines, + "log /config/openvpn.log", + "", + ) + config := strings.Join(configLines, "\n") + err := os.WriteFile(filepath.Join(workspace, "server.conf"), []byte(config), 0o600) + require.NoError(t, err) +} + +func writeOpenVPNDockerClientConfig(t *testing.T, workspace string, openVPNPort uint16) { + t.Helper() + config := strings.Join([]string{ + "client", + "dev tun", + "proto udp4", + "remote 127.0.0.1 " + strconv.Itoa(int(openVPNPort)), + "resolv-retry infinite", + "nobind", + "float", + "persist-key", + "persist-tun", + "remote-cert-tls server", + "ca /config/ca.crt", + "cert /config/client.crt", + "key /config/client.key", + "auth-user-pass /config/auth-user-pass.txt", + "verb 4", + "explicit-exit-notify 1", + "log /config/openvpn.log", + "", + }, "\n") + err := os.WriteFile(filepath.Join(workspace, "client.conf"), []byte(config), 0o600) + require.NoError(t, err) +} + +func writeOpenVPNDockerEchoServer(t *testing.T, workspace string, host string, port uint16) { + t.Helper() + script := strings.ReplaceAll(openVPNDockerEchoServerScript, "{{HOST}}", host) + script = strings.ReplaceAll(script, "{{PORT}}", strconv.Itoa(int(port))) + err := os.WriteFile(filepath.Join(workspace, "echo_server.py"), []byte(script), 0o700) + require.NoError(t, err) +} + +func writeOpenVPNDockerEchoClient(t *testing.T, workspace string, host string, port uint16) { + t.Helper() + script := strings.ReplaceAll(openVPNDockerEchoClientScript, "{{HOST}}", host) + script = strings.ReplaceAll(script, "{{PORT}}", strconv.Itoa(int(port))) + err := os.WriteFile(filepath.Join(workspace, "echo_client.py"), []byte(script), 0o700) + require.NoError(t, err) +} + +func testRemoteEchoThroughSocks(t *testing.T, proxyPort uint16, destinationAddress string, destinationPort uint16) { + t.Helper() + dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", proxyPort), socks.Version5, "", "") + destination := M.ParseSocksaddrHostPort(destinationAddress, destinationPort) + err := testRemoteTCPEcho(t, dialer, destination) + require.NoError(t, err) + err = testRemoteUDPEcho(t, dialer, destination) + require.NoError(t, err) +} + +func testRemoteTCPEcho(t *testing.T, dialer *socks.Client, destination M.Socksaddr) error { + t.Helper() + conn, err := dialer.DialContext(context.Background(), N.NetworkTCP, destination) + if err != nil { + return err + } + defer conn.Close() + err = conn.SetDeadline(time.Now().Add(30 * time.Second)) + if err != nil { + return err + } + err = writeAndReadEcho(conn, []byte("ping")) + if err != nil { + return err + } + payload := make([]byte, 64*1024) + for i := 0; i < 100; i++ { + _, err = rand.Read(payload[1:]) + if err != nil { + return err + } + payload[0] = byte(i) + err = writeAndReadEcho(conn, payload) + if err != nil { + return err + } + } + return nil +} + +func testRemoteUDPEcho(t *testing.T, dialer *socks.Client, destination M.Socksaddr) error { + t.Helper() + conn, err := dialer.DialContext(context.Background(), N.NetworkUDP, destination) + if err != nil { + return err + } + defer conn.Close() + err = conn.SetDeadline(time.Now().Add(30 * time.Second)) + if err != nil { + return err + } + err = writeAndReadPacketEcho(conn, []byte("ping")) + if err != nil { + return err + } + payload := make([]byte, 1500) + for i := 0; i < 50; i++ { + _, err = rand.Read(payload[1:]) + if err != nil { + return err + } + payload[0] = byte(i) + err = writeAndReadPacketEcho(conn, payload) + if err != nil { + return err + } + } + return nil +} + +func writeAndReadEcho(conn net.Conn, payload []byte) error { + _, err := conn.Write(payload) + if err != nil { + return err + } + response := make([]byte, len(payload)) + _, err = io.ReadFull(conn, response) + if err != nil { + return err + } + if !bytes.Equal(payload, response) { + return E.New("unexpected TCP echo response") + } + return nil +} + +func writeAndReadPacketEcho(conn net.Conn, payload []byte) error { + _, err := conn.Write(payload) + if err != nil { + return err + } + response := make([]byte, len(payload)+512) + n, err := conn.Read(response) + if err != nil { + return err + } + if !bytes.Equal(payload, response[:n]) { + return E.New("unexpected UDP echo response") + } + return nil +} + +func startOpenVPNHostEchoServers(t *testing.T, port uint16) { + t.Helper() + tcpListener, err := listen("tcp", ":"+strconv.Itoa(int(port))) + require.NoError(t, err) + udpConnection, err := listenPacket("udp", ":"+strconv.Itoa(int(port))) + require.NoError(t, err) + t.Cleanup(func() { + tcpListener.Close() + udpConnection.Close() + }) + go func() { + for { + conn, acceptErr := tcpListener.Accept() + if acceptErr != nil { + return + } + go echoOpenVPNTCPConnection(conn) + } + }() + go func() { + buffer := make([]byte, 64*1024) + for { + n, address, readErr := udpConnection.ReadFrom(buffer) + if readErr != nil { + return + } + _, _ = udpConnection.WriteTo(buffer[:n], address) + } + }() +} + +func echoOpenVPNTCPConnection(conn net.Conn) { + defer conn.Close() + buffer := make([]byte, 64*1024) + for { + n, err := conn.Read(buffer) + if err != nil { + return + } + _, err = conn.Write(buffer[:n]) + if err != nil { + return + } + } +} + +type openVPNDockerContainer struct { + dockerClient *client.Client + containerID string + name string +} + +type openVPNDockerWaitResult struct { + exitCode int64 + logs string +} + +func requireOpenVPNDockerEnvironment(t *testing.T) *client.Client { + t.Helper() + dockerClient := openVPNDockerClientForTest(t) + _, err := dockerClient.Ping(context.Background()) + if err != nil { + dockerClient.Close() + t.Skipf("Docker is unavailable: %v", err) + } + openVPNDockerImageOnce.Do(func() { + openVPNDockerImageErr = ensureOpenVPNDockerImage(dockerClient) + }) + require.NoError(t, openVPNDockerImageErr) + verifyOpenVPNDockerImage(t, dockerClient) + return dockerClient +} + +func openVPNDockerClientForTest(t *testing.T) *client.Client { + t.Helper() + clientOptions := []client.Opt{client.WithAPIVersionNegotiation()} + dockerHost := os.Getenv("DOCKER_HOST") + switch { + case dockerHost != "": + clientOptions = append(clientOptions, client.WithHost(dockerHost)) + case openVPNFileExists("/Users/sekai/.orbstack/run/docker.sock"): + clientOptions = append(clientOptions, client.WithHost("unix:///Users/sekai/.orbstack/run/docker.sock")) + case openVPNFileExists("/var/run/docker.sock"): + clientOptions = append(clientOptions, client.WithHost("unix:///var/run/docker.sock")) + default: + t.Skip("Docker is unavailable: docker socket not found") + } + dockerClient, err := client.NewClientWithOpts(clientOptions...) + require.NoError(t, err) + t.Cleanup(func() { + dockerClient.Close() + }) + return dockerClient +} + +func openVPNFileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +func ensureOpenVPNDockerImage(dockerClient *client.Client) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + _, _, err := dockerClient.ImageInspectWithRaw(ctx, openVPNDockerImage) + if err == nil { + return nil + } + if !errdefs.IsNotFound(err) { + return err + } + return buildOpenVPNDockerImage(dockerClient) +} + +func buildOpenVPNDockerImage(dockerClient *client.Client) error { + dockerfile := strings.Join([]string{ + "FROM debian:bookworm-slim", + "ARG OPENVPN_VERSION=" + openVPNDockerPackageVersion, + "RUN apt-get update \\", + " && apt-get install -y --no-install-recommends \\", + " bash \\", + " ca-certificates \\", + " grep \\", + " iproute2 \\", + " iputils-ping \\", + " openvpn=${OPENVPN_VERSION} \\", + " procps \\", + " python3 \\", + " tcpdump \\", + " && rm -rf /var/lib/apt/lists/*", + "RUN openvpn --version | head -n 1 | grep 'OpenVPN 2.6.14'", + "", + }, "\n") + var buffer bytes.Buffer + tarWriter := tar.NewWriter(&buffer) + header := &tar.Header{ + Name: "Dockerfile", + Mode: 0o644, + Size: int64(len(dockerfile)), + } + err := tarWriter.WriteHeader(header) + if err == nil { + _, err = tarWriter.Write([]byte(dockerfile)) + } + closeErr := tarWriter.Close() + if err != nil { + return err + } + if closeErr != nil { + return closeErr + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + buildOptions := typesapi.ImageBuildOptions{ + Tags: []string{openVPNDockerImage}, + Remove: true, + PullParent: true, + Platform: openVPNDockerPlatform(), + } + response, err := dockerClient.ImageBuild(ctx, bytes.NewReader(buffer.Bytes()), buildOptions) + if err != nil { + return err + } + defer response.Body.Close() + var output bytes.Buffer + err = jsonmessage.DisplayJSONMessagesStream(response.Body, &output, 0, false, nil) + if err != nil { + return E.Cause(err, "build OpenVPN docker image\n", output.String()) + } + return nil +} + +func verifyOpenVPNDockerImage(t *testing.T, dockerClient *client.Client) { + t.Helper() + containerConfig := &containerapi.Config{ + Image: openVPNDockerImage, + Cmd: []string{"openvpn", "--version"}, + } + createdContainer, err := dockerClient.ContainerCreate(context.Background(), containerConfig, &containerapi.HostConfig{}, nil, openVPNDockerOCIPlatform(), "") + require.NoError(t, err) + t.Cleanup(func() { + _ = removeOpenVPNDockerContainer(context.Background(), dockerClient, createdContainer.ID) + }) + err = dockerClient.ContainerStart(context.Background(), createdContainer.ID, containerapi.StartOptions{}) + require.NoError(t, err) + containerHandle := &openVPNDockerContainer{ + dockerClient: dockerClient, + containerID: createdContainer.ID, + name: "openvpn-version", + } + waitResult := containerHandle.Wait(t, 30*time.Second) + require.Equal(t, int64(0), waitResult.exitCode, waitResult.logs) + firstLine, _, _ := strings.Cut(waitResult.logs, "\n") + require.Contains(t, firstLine, "OpenVPN 2.6.14") +} + +func startOpenVPNDockerContainer(t *testing.T, dockerClient *client.Client, nameSuffix string, workspace string, command string) *openVPNDockerContainer { + t.Helper() + name := "sing-box-openvpn-" + nameSuffix + "-" + sanitizeOpenVPNDockerName(t.Name()) + containerConfig := &containerapi.Config{ + Image: openVPNDockerImage, + Cmd: []string{"bash", "-lc", command}, + } + hostConfig := &containerapi.HostConfig{ + NetworkMode: containerapi.NetworkMode("host"), + Binds: []string{workspace + ":" + openVPNDockerRoot}, + CapAdd: []string{"NET_ADMIN"}, + Resources: containerapi.Resources{ + Devices: []containerapi.DeviceMapping{ + { + PathOnHost: "/dev/net/tun", + PathInContainer: "/dev/net/tun", + CgroupPermissions: "rwm", + }, + }, + }, + } + createdContainer, err := dockerClient.ContainerCreate(context.Background(), containerConfig, hostConfig, nil, openVPNDockerOCIPlatform(), name) + require.NoError(t, err) + containerHandle := &openVPNDockerContainer{ + dockerClient: dockerClient, + containerID: createdContainer.ID, + name: name, + } + t.Cleanup(func() { + _ = removeOpenVPNDockerContainer(context.Background(), dockerClient, createdContainer.ID) + }) + err = dockerClient.ContainerStart(context.Background(), createdContainer.ID, containerapi.StartOptions{}) + require.NoError(t, err) + return containerHandle +} + +func waitForOpenVPNDockerFile(t *testing.T, containerHandle *openVPNDockerContainer, path string, content string, timeout time.Duration) { + t.Helper() + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + fileContent, err := os.ReadFile(path) + if err == nil && strings.Contains(string(fileContent), content) { + return + } + containerHandle.failIfExited(t) + time.Sleep(100 * time.Millisecond) + } + fileContent, _ := os.ReadFile(path) + t.Fatalf("timed out waiting for %q in %s\nfile:\n%s\ncontainer logs:\n%s", content, path, string(fileContent), containerHandle.Logs(context.Background())) +} + +func dumpOpenVPNDockerLogsOnFailure(t *testing.T, containerHandle *openVPNDockerContainer, workspace string) { + t.Helper() + t.Cleanup(func() { + if !t.Failed() { + return + } + t.Logf("container logs for %s:\n%s", containerHandle.name, containerHandle.Logs(context.Background())) + openVPNLog, err := os.ReadFile(filepath.Join(workspace, "openvpn.log")) + if err == nil { + t.Logf("openvpn log for %s:\n%s", containerHandle.name, string(openVPNLog)) + } + }) +} + +func (c *openVPNDockerContainer) failIfExited(t *testing.T) { + t.Helper() + containerInfo, err := c.dockerClient.ContainerInspect(context.Background(), c.containerID) + require.NoError(t, err) + if containerInfo.State != nil && !containerInfo.State.Running { + t.Fatalf("docker container %s exited with code %d\nlogs:\n%s", c.name, containerInfo.State.ExitCode, c.Logs(context.Background())) + } +} + +func (c *openVPNDockerContainer) Wait(t *testing.T, timeout time.Duration) openVPNDockerWaitResult { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + statusChannel, errChannel := c.dockerClient.ContainerWait(ctx, c.containerID, containerapi.WaitConditionNotRunning) + select { + case waitErr := <-errChannel: + if waitErr != nil { + t.Fatalf("wait docker container %s: %v\nlogs:\n%s", c.name, waitErr, c.Logs(context.Background())) + } + case status := <-statusChannel: + return openVPNDockerWaitResult{ + exitCode: status.StatusCode, + logs: c.Logs(context.Background()), + } + case <-ctx.Done(): + t.Fatalf("docker container %s timed out\nlogs:\n%s", c.name, c.Logs(context.Background())) + } + return openVPNDockerWaitResult{} +} + +func (c *openVPNDockerContainer) Logs(ctx context.Context) string { + logReader, err := c.dockerClient.ContainerLogs(ctx, c.containerID, containerapi.LogsOptions{ + ShowStdout: true, + ShowStderr: true, + }) + if err != nil { + return "read logs: " + err.Error() + } + defer logReader.Close() + var stdout bytes.Buffer + var stderr bytes.Buffer + _, err = stdcopy.StdCopy(&stdout, &stderr, logReader) + if err != nil { + return "decode logs: " + err.Error() + } + if stderr.Len() == 0 { + return stdout.String() + } + if stdout.Len() == 0 { + return stderr.String() + } + return stdout.String() + "\nSTDERR:\n" + stderr.String() +} + +func removeOpenVPNDockerContainer(ctx context.Context, dockerClient *client.Client, containerID string) error { + err := dockerClient.ContainerRemove(ctx, containerID, containerapi.RemoveOptions{ + Force: true, + RemoveVolumes: true, + }) + if err != nil && !errdefs.IsNotFound(err) { + return err + } + return nil +} + +func openVPNDockerPlatform() string { + switch runtime.GOARCH { + case "arm64": + return "linux/arm64" + case "amd64": + return "linux/amd64" + default: + return "" + } +} + +func openVPNDockerOCIPlatform() *ocispec.Platform { + switch runtime.GOARCH { + case "arm64", "amd64": + return &ocispec.Platform{ + OS: "linux", + Architecture: runtime.GOARCH, + } + default: + return nil + } +} + +func sanitizeOpenVPNDockerName(name string) string { + replacer := strings.NewReplacer("/", "-", "_", "-", " ", "-") + return replacer.Replace(name) +} + +const openVPNDockerEchoServerScript = `#!/usr/bin/env python3 +import os +import socket +import threading +import time + +HOST = "{{HOST}}" +PORT = {{PORT}} +READY = "/config/echo.ready" + + +def bind_with_retry(sock, address): + last_error = None + for _ in range(300): + try: + sock.bind(address) + return + except OSError as error: + last_error = error + time.sleep(0.1) + raise last_error + + +def handle_tcp(conn): + with conn: + while True: + data = conn.recv(65536) + if not data: + return + conn.sendall(data) + + +def tcp_server(): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + bind_with_retry(sock, (HOST, PORT)) + sock.listen(64) + print("tcp echo ready", flush=True) + while True: + conn, _ = sock.accept() + threading.Thread(target=handle_tcp, args=(conn,), daemon=True).start() + + +def udp_server(): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + bind_with_retry(sock, (HOST, PORT)) + print("udp echo ready", flush=True) + while True: + data, address = sock.recvfrom(65536) + sock.sendto(data, address) + + +threading.Thread(target=tcp_server, daemon=True).start() +threading.Thread(target=udp_server, daemon=True).start() +time.sleep(0.2) +with open(READY, "w", encoding="utf-8") as ready_file: + ready_file.write("ready\n") +while True: + time.sleep(3600) +` + +const openVPNDockerEchoClientScript = `#!/usr/bin/env python3 +import os +import socket + +HOST = "{{HOST}}" +PORT = {{PORT}} + + +def check_tcp(): + sock = socket.create_connection((HOST, PORT), timeout=10) + sock.settimeout(10) + with sock: + payload = b"ping" + sock.sendall(payload) + response = sock.recv(len(payload)) + if response != payload: + raise RuntimeError("unexpected tcp ping response") + for index in range(100): + payload = bytes([index]) + os.urandom(64 * 1024 - 1) + sock.sendall(payload) + response = bytearray() + while len(response) < len(payload): + chunk = sock.recv(len(payload) - len(response)) + if not chunk: + raise RuntimeError("tcp echo closed") + response.extend(chunk) + if bytes(response) != payload: + raise RuntimeError("unexpected tcp large response") + + +def check_udp(): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(10) + sock.connect((HOST, PORT)) + with sock: + payload = b"ping" + sock.send(payload) + response = sock.recv(65536) + if response != payload: + raise RuntimeError("unexpected udp ping response") + for index in range(50): + payload = bytes([index]) + os.urandom(1499) + sock.send(payload) + response = sock.recv(65536) + if response != payload: + raise RuntimeError("unexpected udp large response") + + +check_tcp() +check_udp() +print("echo client ok", flush=True) +` + +func testSuitOpenVPN(t *testing.T, proxyPort uint16, echoPort uint16, tunnelAddress string) { + t.Helper() + dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", proxyPort), socks.Version5, "", "") + destination := M.ParseSocksaddrHostPort(tunnelAddress, echoPort) + dialTCP := func() (net.Conn, error) { + return dialer.DialContext(context.Background(), N.NetworkTCP, destination) + } + dialUDP := func() (net.PacketConn, error) { + conn, err := dialer.DialContext(context.Background(), N.NetworkUDP, destination) + if err != nil { + return nil, err + } + return bufio.NewUnbindPacketConn(conn), nil + } + require.NoError(t, testOpenVPNEchoWithConn(echoPort, dialTCP)) + require.NoError(t, testOpenVPNEchoWithPacketConn(echoPort, dialUDP)) + require.NoError(t, testOpenVPNLargeDataWithConn(echoPort, dialTCP)) +} + +func testOpenVPNEchoWithConn(port uint16, dialTCP func() (net.Conn, error)) error { + listener, err := listen("tcp", ":"+strconv.Itoa(int(port))) + if err != nil { + return err + } + defer listener.Close() + serverErrCh := make(chan error, 1) + go func() { + serverConn, acceptErr := listener.Accept() + if acceptErr != nil { + serverErrCh <- acceptErr + return + } + defer serverConn.Close() + deadlineErr := serverConn.SetDeadline(time.Now().Add(openVPNLargeDataTimeout)) + if deadlineErr != nil { + serverErrCh <- deadlineErr + return + } + buffer := make([]byte, 4) + _, readErr := io.ReadFull(serverConn, buffer) + if readErr != nil { + serverErrCh <- readErr + return + } + _, writeErr := serverConn.Write(buffer) + if writeErr != nil { + serverErrCh <- writeErr + return + } + serverErrCh <- nil + }() + conn, err := dialTCP() + if err != nil { + return err + } + defer conn.Close() + err = conn.SetDeadline(time.Now().Add(openVPNLargeDataTimeout)) + if err != nil { + return err + } + err = writeAndReadEcho(conn, []byte("ping")) + if err != nil { + return err + } + return <-serverErrCh +} + +func testOpenVPNEchoWithPacketConn(port uint16, listenUDP func() (net.PacketConn, error)) error { + listener, err := listenPacket("udp", ":"+strconv.Itoa(int(port))) + if err != nil { + return err + } + defer listener.Close() + serverErrCh := make(chan error, 1) + go func() { + buffer := make([]byte, 1024) + readCount, address, readErr := listener.ReadFrom(buffer) + if readErr != nil { + serverErrCh <- readErr + return + } + _, writeErr := listener.WriteTo(buffer[:readCount], address) + if writeErr != nil { + serverErrCh <- writeErr + return + } + serverErrCh <- nil + }() + packetConn, err := listenUDP() + if err != nil { + return err + } + defer packetConn.Close() + remoteAddress := &net.UDPAddr{IP: localIP.AsSlice(), Port: int(port)} + payload := []byte("ping") + _, err = packetConn.WriteTo(payload, remoteAddress) + if err != nil { + return err + } + response := make([]byte, 1024) + readCount, err := readOpenVPNPacketWithTimeout(packetConn, response) + if err != nil { + return err + } + if !bytes.Equal(response[:readCount], payload) { + return E.New("unexpected UDP echo response") + } + return <-serverErrCh +} + +func testOpenVPNLargeDataWithConn(port uint16, dialTCP func() (net.Conn, error)) error { + listener, err := listen("tcp", ":"+strconv.Itoa(int(port))) + if err != nil { + return err + } + defer listener.Close() + serverErrCh := make(chan error, 1) + go func() { + serverConn, acceptErr := listener.Accept() + if acceptErr != nil { + serverErrCh <- acceptErr + return + } + defer serverConn.Close() + deadlineErr := serverConn.SetDeadline(time.Now().Add(openVPNLargeDataTimeout)) + if deadlineErr != nil { + serverErrCh <- deadlineErr + return + } + buffer := make([]byte, openVPNLargeTCPSize) + for range openVPNLargeTCPPackets { + _, readErr := io.ReadFull(serverConn, buffer) + if readErr != nil { + serverErrCh <- readErr + return + } + _, writeErr := serverConn.Write(buffer) + if writeErr != nil { + serverErrCh <- writeErr + return + } + } + serverErrCh <- nil + }() + conn, err := dialTCP() + if err != nil { + return err + } + defer conn.Close() + err = conn.SetDeadline(time.Now().Add(openVPNLargeDataTimeout)) + if err != nil { + return err + } + response := make([]byte, openVPNLargeTCPSize) + for index := range openVPNLargeTCPPackets { + payload := make([]byte, openVPNLargeTCPSize) + payload[0] = byte(index) + _, err = rand.Read(payload[1:]) + if err != nil { + return err + } + _, err = conn.Write(payload) + if err != nil { + return err + } + _, err = io.ReadFull(conn, response) + if err != nil { + return err + } + if !bytes.Equal(response, payload) { + return E.New("unexpected tcp large response") + } + } + return <-serverErrCh +} + +type openVPNPacketReadResult struct { + readCount int + err error +} + +func readOpenVPNPacketWithTimeout(packetConn net.PacketConn, buffer []byte) (int, error) { + resultCh := make(chan openVPNPacketReadResult, 1) + go func() { + readCount, _, readErr := packetConn.ReadFrom(buffer) + resultCh <- openVPNPacketReadResult{ + readCount: readCount, + err: readErr, + } + }() + select { + case result := <-resultCh: + return result.readCount, result.err + case <-time.After(openVPNLargeDataTimeout): + return 0, E.New("timeout") + } +} + +func createOpenVPNCertificateBundle(t *testing.T) openVPNCertificateBundle { + t.Helper() + tempDir := t.TempDir() + caKey, err := rsa.GenerateKey(rand.Reader, 3072) + require.NoError(t, err) + spkiASN1, err := x509.MarshalPKIXPublicKey(caKey.Public()) + require.NoError(t, err) + var spki struct { + Algorithm pkix.AlgorithmIdentifier + SubjectPublicKey asn1.BitString + } + _, err = asn1.Unmarshal(spkiASN1, &spki) + require.NoError(t, err) + subjectKeyID := sha1.Sum(spki.SubjectPublicKey.Bytes) + caTemplate := &x509.Certificate{ + SerialNumber: randomSerialNumber(t), + Subject: pkix.Name{ + Organization: []string{"sing-box OpenVPN test CA"}, + CommonName: "sing-box OpenVPN test CA", + }, + SubjectKeyId: subjectKeyID[:], + NotAfter: time.Now().AddDate(1, 0, 0), + NotBefore: time.Now().Add(-time.Minute), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLenZero: true, + } + caCertificate, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, caKey.Public(), caKey) + require.NoError(t, err) + caPath := filepath.Join(tempDir, "ca.crt") + writePEMFile(t, caPath, "CERTIFICATE", caCertificate) + serverCertPath, serverKeyPath := createOpenVPNLeafCertificate(t, tempDir, "server", x509.ExtKeyUsageServerAuth, caTemplate, caKey) + clientCertPath, clientKeyPath := createOpenVPNLeafCertificate(t, tempDir, "client", x509.ExtKeyUsageClientAuth, caTemplate, caKey) + return openVPNCertificateBundle{ + caPath: caPath, + serverCertPath: serverCertPath, + serverKeyPath: serverKeyPath, + clientCertPath: clientCertPath, + clientKeyPath: clientKeyPath, + } +} + +func createOpenVPNLeafCertificate(t *testing.T, tempDir string, commonName string, usage x509.ExtKeyUsage, caTemplate *x509.Certificate, caKey *rsa.PrivateKey) (string, string) { + t.Helper() + leafKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + leafTemplate := &x509.Certificate{ + SerialNumber: randomSerialNumber(t), + Subject: pkix.Name{ + Organization: []string{"sing-box OpenVPN test"}, + CommonName: commonName, + }, + NotBefore: time.Now().Add(-time.Minute), + NotAfter: time.Now().AddDate(0, 1, 0), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{ + usage, + }, + } + if usage == x509.ExtKeyUsageServerAuth { + leafTemplate.IPAddresses = append(leafTemplate.IPAddresses, net.ParseIP("127.0.0.1")) + leafTemplate.DNSNames = append(leafTemplate.DNSNames, "localhost") + } + leafCertificate, err := x509.CreateCertificate(rand.Reader, leafTemplate, caTemplate, leafKey.Public(), caKey) + require.NoError(t, err) + certPath := filepath.Join(tempDir, commonName+".crt") + keyPath := filepath.Join(tempDir, commonName+".key") + writePEMFile(t, certPath, "CERTIFICATE", leafCertificate) + privateKey, err := x509.MarshalPKCS8PrivateKey(leafKey) + require.NoError(t, err) + writePEMFile(t, keyPath, "PRIVATE KEY", privateKey) + return certPath, keyPath +} + +func writePEMFile(t *testing.T, path string, blockType string, bytes []byte) { + t.Helper() + content := pem.EncodeToMemory(&pem.Block{ + Type: blockType, + Bytes: bytes, + }) + err := os.WriteFile(path, content, 0o600) + require.NoError(t, err) +} + +func createOpenVPNStaticKey(t *testing.T) string { + t.Helper() + keyMaterial := make([]byte, 256) + _, err := rand.Read(keyMaterial) + require.NoError(t, err) + hexKey := hex.EncodeToString(keyMaterial) + lines := []string{"-----BEGIN OpenVPN Static key V1-----"} + for index := 0; index < len(hexKey); index += 32 { + lines = append(lines, hexKey[index:index+32]) + } + lines = append(lines, "-----END OpenVPN Static key V1-----", "") + return strings.Join(lines, "\n") +} + +func reserveOpenVPNProtocolPort(t *testing.T, protocol string) uint16 { + t.Helper() + if protocol == N.NetworkTCP { + return reserveOpenVPNTCPPort(t) + } + return reserveOpenVPNUDPPort(t) +} + +func reserveOpenVPNTCPPort(t *testing.T) uint16 { + t.Helper() + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer listener.Close() + tcpAddress := listener.Addr().(*net.TCPAddr) + return uint16(tcpAddress.Port) +} + +func reserveOpenVPNUDPPort(t *testing.T) uint16 { + t.Helper() + listener, err := net.ListenPacket("udp", "127.0.0.1:0") + require.NoError(t, err) + defer listener.Close() + udpAddress := listener.LocalAddr().(*net.UDPAddr) + return uint16(udpAddress.Port) +} + +func reserveOpenVPNEchoPort(t *testing.T) uint16 { + t.Helper() + for i := 0; i < 20; i++ { + tcpListener, err := net.Listen("tcp", ":0") + require.NoError(t, err) + tcpAddress := tcpListener.Addr().(*net.TCPAddr) + port := uint16(tcpAddress.Port) + udpListener, err := net.ListenPacket("udp", ":"+strconv.Itoa(int(port))) + if err == nil { + udpListener.Close() + tcpListener.Close() + return port + } + tcpListener.Close() + } + t.Fatal("reserve TCP and UDP echo port") + return 0 +} diff --git a/test/testdata/openconnect/ocserv/Dockerfile b/test/testdata/openconnect/ocserv/Dockerfile new file mode 100644 index 000000000..c10d965ce --- /dev/null +++ b/test/testdata/openconnect/ocserv/Dockerfile @@ -0,0 +1,11 @@ +FROM debian:trixie-slim@sha256:cedb1ef40439206b673ee8b33a46a03a0c9fa90bf3732f54704f99cb061d2c5a + +ARG OCSERV_VERSION=1.3.0-2 + +RUN apt-get update \ + && apt-get install --yes --no-install-recommends ocserv="${OCSERV_VERSION}" iproute2 python3 \ + && rm -rf /var/lib/apt/lists/* + +COPY echo_server.py /usr/local/bin/openconnect-echo-server + +EXPOSE 443/tcp 443/udp diff --git a/test/testdata/openconnect/ocserv/echo_server.py b/test/testdata/openconnect/ocserv/echo_server.py new file mode 100644 index 000000000..91099c792 --- /dev/null +++ b/test/testdata/openconnect/ocserv/echo_server.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +import socket +import threading + + +def echo(connection): + with connection: + while True: + data = connection.recv(65536) + if not data: + return + connection.sendall(data) + + +listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +listener.bind(("0.0.0.0", 18080)) +listener.listen() +print("openconnect echo ready", flush=True) +while True: + accepted, _ = listener.accept() + threading.Thread(target=echo, args=(accepted,), daemon=True).start() diff --git a/transport/openconnect/device.go b/transport/openconnect/device.go new file mode 100644 index 000000000..a3a9219ce --- /dev/null +++ b/transport/openconnect/device.go @@ -0,0 +1,200 @@ +package openconnect + +import ( + "context" + "net/netip" + "sync/atomic" + "time" + + "github.com/sagernet/sing-openconnect" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + N "github.com/sagernet/sing/common/network" +) + +const ( + DefaultMTU = 1500 + PacketHeadroom = openconnect.PacketHeadroom +) + +type PacketWriter func(packetBuffers []*buf.Buffer) error + +type Device interface { + N.Dialer + Start() error + UpdateConfiguration(configuration Configuration) error + WriteInboundBuffers(packetBuffers []*buf.Buffer) error + SetPacketWriter(writer PacketWriter) + PortAddresses() (netip.Addr, netip.Addr) + PortMTU() uint32 + AttachReturn(returnPath tun.Return) error + DetachReturn(returnPath tun.Return) error + ReturnPath() (tun.Return, int) + Close() error +} + +type DeviceOptions struct { + Context context.Context + Logger logger.ContextLogger + System bool + Handler tun.Handler + UDPTimeout time.Duration + ICMPTimeout time.Duration + Name string + MTU uint32 + Configuration Configuration +} + +type Configuration struct { + MTU uint32 + Addresses []netip.Prefix + Routes []Route + ExcludedRoutes []Route + DNS []netip.Addr + NBNS []netip.Addr + SearchDomains []string + SplitDNS []string + SplitDNSRules []SplitDNSRule + ProxyAutoConfigURL string + Banner string + TunnelAllDNS bool + ClientBypassProtocol bool + IdleTimeout time.Duration + AuthenticationExpiration time.Time +} + +type Route struct { + Prefix netip.Prefix + Gateway netip.Addr + Metric int +} + +type SplitDNSRule struct { + Domains []string + Servers []netip.Addr +} + +func NewDevice(options DeviceOptions) (Device, error) { + if !options.System { + return newStackDevice(options) + } + if !tun.WithGVisor { + return newSystemDevice(options) + } + return newSystemStackDevice(options) +} + +type baseDevice struct { + packetWriter PacketWriter + returnState atomic.Pointer[returnPathState] +} + +func (d *baseDevice) SetPacketWriter(writer PacketWriter) { + d.packetWriter = writer +} + +func (d *baseDevice) writeOutbound(packetBuffers []*buf.Buffer) error { + if d.packetWriter == nil { + buf.ReleaseMulti(packetBuffers) + return E.New("missing OpenConnect packet writer") + } + return d.packetWriter(packetBuffers) +} + +func (d *baseDevice) processInboundBuffers(packetBuffers []*buf.Buffer, writeBuffers func(packetBuffers []*buf.Buffer) error) error { + if len(packetBuffers) == 0 { + return nil + } + state := d.returnState.Load() + if state == nil { + return writeBuffers(packetBuffers) + } + packets := make([][]byte, len(packetBuffers)) + for i, packetBuffer := range packetBuffers { + packetBuffer.ExtendHeader(state.headroom) + packets[i] = packetBuffer.Bytes() + } + unconsumed := state.returnPath.ReturnPackets(packets) + if len(unconsumed) == 0 { + return nil + } + unconsumedBuffers := make([]*buf.Buffer, len(unconsumed)) + for i, packet := range unconsumed { + packetBuffer := buf.As(packet) + packetBuffer.Advance(state.headroom) + unconsumedBuffers[i] = packetBuffer + } + return writeBuffers(unconsumedBuffers) +} + +func (d *baseDevice) AttachReturn(returnPath tun.Return) error { + headroom := returnPath.ReturnHeadroom() + if headroom > PacketHeadroom { + return E.New("return path headroom ", headroom, " exceeds available ", PacketHeadroom) + } + newState := &returnPathState{ + returnPath: returnPath, + headroom: headroom, + } + for { + currentState := d.returnState.Load() + if currentState != nil { + if currentState.returnPath == returnPath { + return nil + } + return E.New("return path already attached") + } + if d.returnState.CompareAndSwap(nil, newState) { + return nil + } + } +} + +func (d *baseDevice) DetachReturn(returnPath tun.Return) error { + currentState := d.returnState.Load() + if currentState != nil && currentState.returnPath == returnPath { + d.returnState.CompareAndSwap(currentState, nil) + } + return nil +} + +func (d *baseDevice) ReturnPath() (tun.Return, int) { + state := d.returnState.Load() + if state == nil { + return nil, 0 + } + return state.returnPath, state.headroom +} + +type returnPathState struct { + returnPath tun.Return + headroom int +} + +func firstAddresses(addresses []netip.Prefix) (netip.Addr, netip.Addr) { + var inet4Address netip.Addr + var inet6Address netip.Addr + for _, prefix := range addresses { + if prefix.Addr().Is4() && !inet4Address.IsValid() { + inet4Address = prefix.Addr() + } else if prefix.Addr().Is6() && !inet6Address.IsValid() { + inet6Address = prefix.Addr() + } + } + return inet4Address, inet6Address +} + +func splitPrefixes(prefixes []netip.Prefix) ([]netip.Prefix, []netip.Prefix) { + var inet4Prefixes []netip.Prefix + var inet6Prefixes []netip.Prefix + for _, prefix := range prefixes { + if prefix.Addr().Is4() { + inet4Prefixes = append(inet4Prefixes, prefix) + } else { + inet6Prefixes = append(inet6Prefixes, prefix) + } + } + return inet4Prefixes, inet6Prefixes +} diff --git a/transport/openconnect/device_stack.go b/transport/openconnect/device_stack.go new file mode 100644 index 000000000..2887c27bf --- /dev/null +++ b/transport/openconnect/device_stack.go @@ -0,0 +1,341 @@ +//go:build with_gvisor + +package openconnect + +import ( + "context" + "net" + "net/netip" + "slices" + "sync" + "sync/atomic" + + "github.com/sagernet/gvisor/pkg/buffer" + "github.com/sagernet/gvisor/pkg/tcpip" + "github.com/sagernet/gvisor/pkg/tcpip/adapters/gonet" + "github.com/sagernet/gvisor/pkg/tcpip/header" + "github.com/sagernet/gvisor/pkg/tcpip/network/ipv4" + "github.com/sagernet/gvisor/pkg/tcpip/network/ipv6" + "github.com/sagernet/gvisor/pkg/tcpip/stack" + "github.com/sagernet/gvisor/pkg/tcpip/transport/icmp" + "github.com/sagernet/gvisor/pkg/tcpip/transport/tcp" + "github.com/sagernet/gvisor/pkg/tcpip/transport/udp" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +var _ Device = (*stackDevice)(nil) + +type stackDevice struct { + baseDevice + stateAccess sync.RWMutex + options DeviceOptions + stack *stack.Stack + endpoint *stackEndpoint + inet4Address netip.Addr + inet6Address netip.Addr + icmpForwarder *tun.ICMPForwarder + closeOnce sync.Once +} + +func newStackDevice(options DeviceOptions) (*stackDevice, error) { + if options.MTU == 0 { + options.MTU = DefaultMTU + } + device := &stackDevice{ + options: options, + } + endpoint := &stackEndpoint{ + device: device, + done: make(chan struct{}), + } + endpoint.mtu.Store(options.MTU) + ipStack, err := tun.NewGVisorStackWithOptions(endpoint, stack.NICOptions{}, true) + if err != nil { + return nil, err + } + device.stack = ipStack + device.endpoint = endpoint + err = device.updateAddresses(nil, options.Configuration.Addresses) + if err != nil { + ipStack.Close() + return nil, err + } + if options.Handler != nil { + ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(options.Context, ipStack, options.Handler).HandlePacket) + ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(options.Context, ipStack, options.Handler, tun.UDPNatOptions{ + Timeout: options.UDPTimeout, + }).HandlePacket) + icmpForwarder := tun.NewICMPForwarder(ipStack, options.Handler, options.Logger) + ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket) + ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket) + device.icmpForwarder = icmpForwarder + } + return device, nil +} + +func (d *stackDevice) Start() error { + return nil +} + +func (d *stackDevice) UpdateConfiguration(configuration Configuration) error { + d.stateAccess.Lock() + defer d.stateAccess.Unlock() + if configuration.MTU != 0 { + d.options.MTU = configuration.MTU + d.endpoint.mtu.Store(configuration.MTU) + } + previousAddresses := d.options.Configuration.Addresses + d.options.Configuration = configuration + return d.updateAddresses(previousAddresses, configuration.Addresses) +} + +func (d *stackDevice) updateAddresses(previousAddresses []netip.Prefix, addresses []netip.Prefix) error { + for _, prefix := range previousAddresses { + if slices.Contains(addresses, prefix) { + continue + } + gErr := d.stack.RemoveAddress(tun.DefaultNIC, tun.AddressFromAddr(prefix.Addr())) + if gErr != nil { + return E.New("remove local address ", prefix, ": ", gErr.String()) + } + } + for _, prefix := range addresses { + if slices.Contains(previousAddresses, prefix) { + continue + } + protocolAddress := tcpip.ProtocolAddress{ + AddressWithPrefix: tcpip.AddressWithPrefix{ + Address: tun.AddressFromAddr(prefix.Addr()), + PrefixLen: prefix.Bits(), + }, + } + if prefix.Addr().Is4() { + protocolAddress.Protocol = ipv4.ProtocolNumber + } else { + protocolAddress.Protocol = ipv6.ProtocolNumber + } + gErr := d.stack.AddProtocolAddress(tun.DefaultNIC, protocolAddress, stack.AddressProperties{}) + if gErr != nil { + return E.New("add local address ", prefix, ": ", gErr.String()) + } + } + d.inet4Address, d.inet6Address = firstAddresses(addresses) + return nil +} + +func (d *stackDevice) WriteInboundBuffers(packetBuffers []*buf.Buffer) error { + return d.processInboundBuffers(packetBuffers, d.writeBuffers) +} + +func (d *stackDevice) writeBuffers(packetBuffers []*buf.Buffer) error { + networkProtocols := make([]tcpip.NetworkProtocolNumber, 0, len(packetBuffers)) + stackPacketBuffers := make([]*stack.PacketBuffer, 0, len(packetBuffers)) + var packetErr error + for _, packetBuffer := range packetBuffers { + packet := packetBuffer.Bytes() + var networkProtocol tcpip.NetworkProtocolNumber + switch header.IPVersion(packet) { + case header.IPv4Version: + networkProtocol = header.IPv4ProtocolNumber + case header.IPv6Version: + networkProtocol = header.IPv6ProtocolNumber + default: + if packetErr == nil { + packetErr = E.New("invalid IP packet") + } + continue + } + networkProtocols = append(networkProtocols, networkProtocol) + stackPacketBuffers = append(stackPacketBuffers, stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: buffer.MakeWithData(packet), + })) + } + d.endpoint.deliverNetworkPackets(networkProtocols, stackPacketBuffers) + for _, packetBuffer := range stackPacketBuffers { + packetBuffer.DecRef() + } + return packetErr +} + +func (d *stackDevice) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + inet4Address, inet6Address := d.PortAddresses() + address := tcpip.FullAddress{ + NIC: tun.DefaultNIC, + Port: destination.Port, + Addr: tun.AddressFromAddr(destination.Addr), + } + bind := tcpip.FullAddress{ + NIC: tun.DefaultNIC, + } + var networkProtocol tcpip.NetworkProtocolNumber + if destination.IsIPv4() { + if !inet4Address.IsValid() { + return nil, E.New("missing IPv4 local address") + } + networkProtocol = header.IPv4ProtocolNumber + bind.Addr = tun.AddressFromAddr(inet4Address) + } else { + if !inet6Address.IsValid() { + return nil, E.New("missing IPv6 local address") + } + networkProtocol = header.IPv6ProtocolNumber + bind.Addr = tun.AddressFromAddr(inet6Address) + } + switch N.NetworkName(network) { + case N.NetworkTCP: + return gonet.DialTCPWithBind(ctx, d.stack, bind, address, networkProtocol) + case N.NetworkUDP: + return gonet.DialUDP(d.stack, &bind, &address, networkProtocol) + default: + return nil, E.Extend(N.ErrUnknownNetwork, network) + } +} + +func (d *stackDevice) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + inet4Address, inet6Address := d.PortAddresses() + bind := tcpip.FullAddress{ + NIC: tun.DefaultNIC, + } + var networkProtocol tcpip.NetworkProtocolNumber + if destination.IsIPv4() { + if !inet4Address.IsValid() { + return nil, E.New("missing IPv4 local address") + } + networkProtocol = header.IPv4ProtocolNumber + bind.Addr = tun.AddressFromAddr(inet4Address) + } else { + if !inet6Address.IsValid() { + return nil, E.New("missing IPv6 local address") + } + networkProtocol = header.IPv6ProtocolNumber + bind.Addr = tun.AddressFromAddr(inet6Address) + } + return gonet.DialUDP(d.stack, &bind, nil, networkProtocol) +} + +func (d *stackDevice) PortAddresses() (netip.Addr, netip.Addr) { + d.stateAccess.RLock() + defer d.stateAccess.RUnlock() + return d.inet4Address, d.inet6Address +} + +func (d *stackDevice) PortMTU() uint32 { + d.stateAccess.RLock() + defer d.stateAccess.RUnlock() + return d.options.MTU +} + +func (d *stackDevice) Close() error { + d.closeOnce.Do(func() { + close(d.endpoint.done) + if d.icmpForwarder != nil { + d.icmpForwarder.Close() + } + d.stack.Close() + for _, endpoint := range d.stack.CleanupEndpoints() { + endpoint.Abort() + } + d.stack.Wait() + }) + return nil +} + +type stackEndpoint struct { + device *stackDevice + mtu atomic.Uint32 + done chan struct{} + dispatcherAccess sync.RWMutex + dispatcher stack.NetworkDispatcher +} + +func (e *stackEndpoint) MTU() uint32 { + return e.mtu.Load() +} + +func (e *stackEndpoint) SetMTU(mtu uint32) { + e.mtu.Store(mtu) +} + +func (e *stackEndpoint) MaxHeaderLength() uint16 { + return 0 +} + +func (e *stackEndpoint) LinkAddress() tcpip.LinkAddress { + return "" +} + +func (e *stackEndpoint) SetLinkAddress(addr tcpip.LinkAddress) { +} + +func (e *stackEndpoint) Capabilities() stack.LinkEndpointCapabilities { + return stack.CapabilityRXChecksumOffload +} + +func (e *stackEndpoint) Attach(dispatcher stack.NetworkDispatcher) { + e.dispatcherAccess.Lock() + defer e.dispatcherAccess.Unlock() + e.dispatcher = dispatcher +} + +func (e *stackEndpoint) IsAttached() bool { + e.dispatcherAccess.RLock() + defer e.dispatcherAccess.RUnlock() + return e.dispatcher != nil +} + +func (e *stackEndpoint) deliverNetworkPackets(networkProtocols []tcpip.NetworkProtocolNumber, packetBuffers []*stack.PacketBuffer) { + e.dispatcherAccess.RLock() + defer e.dispatcherAccess.RUnlock() + if e.dispatcher == nil { + return + } + for i, packetBuffer := range packetBuffers { + e.dispatcher.DeliverNetworkPacket(networkProtocols[i], packetBuffer) + } +} + +func (e *stackEndpoint) Wait() { +} + +func (e *stackEndpoint) ARPHardwareType() header.ARPHardwareType { + return header.ARPHardwareNone +} + +func (e *stackEndpoint) AddHeader(packetBuffer *stack.PacketBuffer) { +} + +func (e *stackEndpoint) ParseHeader(packetBuffer *stack.PacketBuffer) bool { + return true +} + +func (e *stackEndpoint) WritePackets(list stack.PacketBufferList) (int, tcpip.Error) { + packetBuffers := make([]*buf.Buffer, 0, list.Len()) + for _, packetBuffer := range list.AsSlice() { + packetSlices := packetBuffer.AsSlices() + packetLength := 0 + for _, packetSlice := range packetSlices { + packetLength += len(packetSlice) + } + outboundBuffer := buf.NewSize(PacketHeadroom + packetLength + systemDevicePacketRearSpace) + outboundBuffer.Resize(PacketHeadroom, 0) + for _, packetSlice := range packetSlices { + _, _ = outboundBuffer.Write(packetSlice) + } + packetBuffers = append(packetBuffers, outboundBuffer) + } + err := e.device.writeOutbound(packetBuffers) + if err != nil { + return 0, &tcpip.ErrClosedForSend{} + } + return list.Len(), nil +} + +func (e *stackEndpoint) Close() { +} + +func (e *stackEndpoint) SetOnCloseAction(action func()) { +} diff --git a/transport/openconnect/device_stack_stub.go b/transport/openconnect/device_stack_stub.go new file mode 100644 index 000000000..8df454dd7 --- /dev/null +++ b/transport/openconnect/device_stack_stub.go @@ -0,0 +1,13 @@ +//go:build !with_gvisor + +package openconnect + +import E "github.com/sagernet/sing/common/exceptions" + +func newStackDevice(options DeviceOptions) (Device, error) { + return nil, E.New("OpenConnect system:false requires the with_gvisor build tag") +} + +func newSystemStackDevice(options DeviceOptions) (Device, error) { + return nil, E.New("OpenConnect system stack requires the with_gvisor build tag") +} diff --git a/transport/openconnect/device_system.go b/transport/openconnect/device_system.go new file mode 100644 index 000000000..ab6d01b7c --- /dev/null +++ b/transport/openconnect/device_system.go @@ -0,0 +1,366 @@ +package openconnect + +import ( + "context" + "net" + "net/netip" + "runtime" + "slices" + "sync" + "syscall" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun/gtcpip/header" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" +) + +var _ Device = (*systemDevice)(nil) + +const ( + systemDeviceReadBufferSize = 65535 + tun.PacketOffset + systemDevicePacketRearSpace = 64 +) + +type systemDevice struct { + baseDevice + stateAccess sync.RWMutex + options DeviceOptions + dialer N.Dialer + device tun.Tun + inet4Address netip.Addr + inet6Address netip.Addr + closed bool +} + +func newSystemDevice(options DeviceOptions) (*systemDevice, error) { + if options.Name == "" { + options.Name = tun.CalculateInterfaceName("oc") + } + if options.MTU == 0 { + options.MTU = DefaultMTU + } + interfaceDialer, err := dialer.NewDefault(options.Context, option.DialerOptions{ + BindInterface: options.Name, + }) + if err != nil { + return nil, err + } + inet4Address, inet6Address := firstAddresses(options.Configuration.Addresses) + return &systemDevice{ + options: options, + dialer: interfaceDialer, + inet4Address: inet4Address, + inet6Address: inet6Address, + }, nil +} + +func (d *systemDevice) Start() error { + d.stateAccess.Lock() + defer d.stateAccess.Unlock() + return d.startLocked() +} + +func (d *systemDevice) startLocked() error { + if d.closed { + return net.ErrClosed + } + if d.device != nil { + return nil + } + tunOptions := d.buildTunOptions() + tunInterface, err := tun.New(tunOptions) + if err != nil { + return err + } + err = tunInterface.Start() + if err != nil { + tunInterface.Close() + return err + } + d.device = tunInterface + d.options.Logger.Info("started at ", d.options.Name) + go d.readLoop(tunInterface, int(d.options.MTU)) + return nil +} + +func (d *systemDevice) buildTunOptions() tun.Options { + inet4Address, inet6Address := firstAddresses(d.options.Configuration.Addresses) + d.inet4Address = inet4Address + d.inet6Address = inet6Address + inet4Addresses, inet6Addresses := splitPrefixes(d.options.Configuration.Addresses) + inet4Routes, inet6Routes := splitPrefixes(common.Map(d.options.Configuration.Routes, func(route Route) netip.Prefix { return route.Prefix })) + inet4ExcludedRoutes, inet6ExcludedRoutes := splitPrefixes(common.Map(d.options.Configuration.ExcludedRoutes, func(route Route) netip.Prefix { return route.Prefix })) + networkManager := service.FromContext[adapter.NetworkManager](d.options.Context) + tunOptions := tun.Options{ + Name: d.options.Name, + Inet4Address: inet4Addresses, + Inet6Address: inet6Addresses, + MTU: d.options.MTU, + GSO: true, + InterfaceScope: true, + DNSAddress: d.options.Configuration.DNS, + Inet4RouteAddress: inet4Routes, + Inet6RouteAddress: inet6Routes, + Inet4RouteExcludeAddress: inet4ExcludedRoutes, + Inet6RouteExcludeAddress: inet6ExcludedRoutes, + InterfaceMonitor: nil, + InterfaceFinder: nil, + Logger: d.options.Logger, + IPRoute2TableIndex: tun.DefaultIPRoute2TableIndex, + IPRoute2RuleIndex: tun.DefaultIPRoute2RuleIndex, + EXP_DisableDNSHijack: true, + } + if runtime.GOOS == "darwin" { + tunOptions.AutoRoute = true + } + if networkManager != nil { + tunOptions.InterfaceMonitor = networkManager.InterfaceMonitor() + tunOptions.InterfaceFinder = networkManager.InterfaceFinder() + } + return tunOptions +} + +func (d *systemDevice) readLoop(tunInterface tun.Tun, mtu int) { + linuxTUN, isLinuxTUN := tunInterface.(tun.LinuxTUN) + if isLinuxTUN && linuxTUN.BatchSize() > 1 { + d.readLoopLinux(linuxTUN, linuxTUN.BatchSize(), mtu) + return + } + darwinTUN, isDarwinTUN := tunInterface.(tun.DarwinTUN) + if isDarwinTUN { + d.readLoopDarwin(darwinTUN) + return + } + packetBuffer := buf.NewSize(PacketHeadroom + systemDeviceReadBufferSize + systemDevicePacketRearSpace) + defer packetBuffer.Release() + for { + packetBuffer.Reset() + packetBuffer.Resize(PacketHeadroom, 0) + readN, err := tunInterface.Read(packetBuffer.FreeBytes()[:systemDeviceReadBufferSize]) + if err != nil { + if E.IsClosed(err) { + return + } + d.options.Logger.Error(E.Cause(err, "read packet")) + continue + } + if readN <= tun.PacketOffset { + continue + } + packetBuffer.Truncate(readN) + packetBuffer.Advance(tun.PacketOffset) + packetBuffer.IncRef() + err = d.writeOutbound([]*buf.Buffer{packetBuffer}) + packetBuffer.DecRef() + if err != nil { + d.options.Logger.Error(E.Cause(err, "write packet")) + } + } +} + +func (d *systemDevice) readLoopLinux(tunInterface tun.LinuxTUN, batchSize int, mtu int) { + packetBuffers := make([]*buf.Buffer, batchSize) + readBuffers := make([][]byte, batchSize) + packetSizes := make([]int, batchSize) + for i := range packetBuffers { + packetBuffers[i] = buf.NewSize(PacketHeadroom + mtu + systemDevicePacketRearSpace) + } + defer buf.ReleaseMulti(packetBuffers) + for { + for i, packetBuffer := range packetBuffers { + packetBuffer.Reset() + packetBuffer.Resize(PacketHeadroom, 0) + readBuffers[i] = packetBuffer.FreeBytes()[:mtu] + } + packetCount, readErr := tunInterface.BatchRead(readBuffers, 0, packetSizes) + for i := range packetCount { + packetBuffers[i].Truncate(packetSizes[i]) + packetBuffers[i].IncRef() + } + if packetCount > 0 { + writeErr := d.writeOutbound(packetBuffers[:packetCount]) + for i := range packetCount { + packetBuffers[i].DecRef() + } + if writeErr != nil { + d.options.Logger.Error(E.Cause(writeErr, "write packet batch")) + } + } + if readErr != nil { + if E.IsClosed(readErr) { + return + } + d.options.Logger.Error(E.Cause(readErr, "batch read packet")) + } + } +} + +func (d *systemDevice) readLoopDarwin(tunInterface tun.DarwinTUN) { + for { + packetBuffers, readErr := tunInterface.BatchRead() + outboundBuffers := packetBuffers[:0] + for _, packetBuffer := range packetBuffers { + if packetBuffer.IsEmpty() { + packetBuffer.Release() + continue + } + outboundBuffers = append(outboundBuffers, packetBuffer) + } + if len(outboundBuffers) > 0 { + writeErr := d.writeOutbound(outboundBuffers) + if writeErr != nil { + d.options.Logger.Error(E.Cause(writeErr, "write packet batch")) + } + } + if readErr != nil { + if E.IsClosed(readErr) || E.IsMulti(readErr, syscall.EBADF) { + return + } + d.options.Logger.Error(E.Cause(readErr, "batch read packet")) + } + } +} + +func (d *systemDevice) UpdateConfiguration(configuration Configuration) error { + d.stateAccess.Lock() + defer d.stateAccess.Unlock() + previousConfiguration := d.options.Configuration + previousMTU := d.options.MTU + updatedMTU := d.options.MTU + if configuration.MTU != 0 { + updatedMTU = configuration.MTU + } + d.options.MTU = updatedMTU + d.options.Configuration = configuration + if d.device == nil { + inet4Address, inet6Address := firstAddresses(configuration.Addresses) + d.inet4Address = inet4Address + d.inet6Address = inet6Address + return nil + } + if !slices.Equal(previousConfiguration.Addresses, configuration.Addresses) || + previousMTU != updatedMTU || + !slices.Equal(previousConfiguration.DNS, configuration.DNS) { + d.device.Close() + d.device = nil + return d.startLocked() + } + return d.device.UpdateRouteOptions(d.buildTunOptions()) +} + +func (d *systemDevice) WriteInboundBuffers(packetBuffers []*buf.Buffer) error { + return d.processInboundBuffers(packetBuffers, d.writeBuffers) +} + +func (d *systemDevice) writeBuffers(packetBuffers []*buf.Buffer) error { + d.stateAccess.RLock() + tunInterface := d.device + d.stateAccess.RUnlock() + if tunInterface == nil { + return E.New("OpenConnect system device is not ready") + } + linuxTUN, isLinuxTUN := tunInterface.(tun.LinuxTUN) + if isLinuxTUN { + headroom := linuxTUN.FrontHeadroom() + packets := make([][]byte, len(packetBuffers)) + var temporaryBuffers []*buf.Buffer + for i, packetBuffer := range packetBuffers { + if packetBuffer.Start() >= headroom { + packetBuffer.ExtendHeader(headroom) + packets[i] = packetBuffer.Bytes() + packetBuffer.Advance(headroom) + continue + } + temporaryBuffer := buf.NewSize(headroom + packetBuffer.Len()) + temporaryBuffer.Resize(headroom, 0) + _, _ = temporaryBuffer.Write(packetBuffer.Bytes()) + temporaryBuffer.ExtendHeader(headroom) + packets[i] = temporaryBuffer.Bytes() + temporaryBuffers = append(temporaryBuffers, temporaryBuffer) + } + _, err := linuxTUN.BatchWrite(packets, headroom) + buf.ReleaseMulti(temporaryBuffers) + return err + } + darwinTUN, isDarwinTUN := tunInterface.(tun.DarwinTUN) + if isDarwinTUN { + return darwinTUN.BatchWrite(packetBuffers) + } + for _, packetBuffer := range packetBuffers { + err := d.writePacket(packetBuffer.Bytes()) + if err != nil { + return err + } + } + return nil +} + +func (d *systemDevice) writePacket(packet []byte) error { + d.stateAccess.RLock() + tunInterface := d.device + d.stateAccess.RUnlock() + if tunInterface == nil { + return E.New("OpenConnect system device is not ready") + } + if tun.PacketOffset == 0 { + _, err := tunInterface.Write(packet) + return err + } + writeBuffer := make([]byte, tun.PacketOffset+len(packet)) + tun.PacketFillHeader(writeBuffer[:tun.PacketOffset], header.IPVersion(packet)) + copy(writeBuffer[tun.PacketOffset:], packet) + _, err := tunInterface.Write(writeBuffer) + return err +} + +func (d *systemDevice) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + if !destination.Addr.IsValid() { + return nil, E.New("invalid non-IP destination") + } + return d.dialer.DialContext(ctx, network, destination) +} + +func (d *systemDevice) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + if !destination.Addr.IsValid() { + return nil, E.New("invalid non-IP destination") + } + return d.dialer.ListenPacket(ctx, destination) +} + +func (d *systemDevice) PortAddresses() (netip.Addr, netip.Addr) { + d.stateAccess.RLock() + defer d.stateAccess.RUnlock() + return d.inet4Address, d.inet6Address +} + +func (d *systemDevice) PortMTU() uint32 { + d.stateAccess.RLock() + defer d.stateAccess.RUnlock() + return d.options.MTU +} + +func (d *systemDevice) Close() error { + d.stateAccess.Lock() + defer d.stateAccess.Unlock() + d.closed = true + if d.device == nil { + return nil + } + err := d.device.Close() + d.device = nil + return err +} + +func (d *systemDevice) configurationAddresses() []netip.Prefix { + d.stateAccess.RLock() + defer d.stateAccess.RUnlock() + return slices.Clone(d.options.Configuration.Addresses) +} diff --git a/transport/openconnect/device_system_stack.go b/transport/openconnect/device_system_stack.go new file mode 100644 index 000000000..faa9826a9 --- /dev/null +++ b/transport/openconnect/device_system_stack.go @@ -0,0 +1,105 @@ +//go:build with_gvisor + +package openconnect + +import ( + "net/netip" + + "github.com/sagernet/sing-tun/gtcpip/header" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" +) + +var _ Device = (*systemStackDevice)(nil) + +type systemStackDevice struct { + *systemDevice + stackDevice *stackDevice +} + +func newSystemStackDevice(options DeviceOptions) (*systemStackDevice, error) { + system, err := newSystemDevice(options) + if err != nil { + return nil, err + } + stackOptions := options + stackOptions.System = false + stackDevice, err := newStackDevice(stackOptions) + if err != nil { + system.Close() + return nil, err + } + return &systemStackDevice{ + systemDevice: system, + stackDevice: stackDevice, + }, nil +} + +func (d *systemStackDevice) SetPacketWriter(writer PacketWriter) { + d.systemDevice.SetPacketWriter(writer) + d.stackDevice.SetPacketWriter(writer) +} + +func (d *systemStackDevice) UpdateConfiguration(configuration Configuration) error { + err := d.systemDevice.UpdateConfiguration(configuration) + if err != nil { + return err + } + return d.stackDevice.UpdateConfiguration(configuration) +} + +func (d *systemStackDevice) WriteInboundBuffers(packetBuffers []*buf.Buffer) error { + return d.systemDevice.processInboundBuffers(packetBuffers, d.writeBuffers) +} + +func (d *systemStackDevice) writeBuffers(packetBuffers []*buf.Buffer) error { + addresses := d.systemDevice.configurationAddresses() + runStart := 0 + runUsesSystemDevice := false + var writeErr error + for i, packetBuffer := range packetBuffers { + destination := packetDestination(packetBuffer.Bytes()) + useSystemDevice := false + for _, prefix := range addresses { + if prefix.Contains(destination) { + useSystemDevice = true + break + } + } + if i > runStart && useSystemDevice != runUsesSystemDevice { + var err error + if runUsesSystemDevice { + err = d.systemDevice.writeBuffers(packetBuffers[runStart:i]) + } else { + err = d.stackDevice.writeBuffers(packetBuffers[runStart:i]) + } + writeErr = E.Errors(writeErr, err) + runStart = i + } + if i == runStart { + runUsesSystemDevice = useSystemDevice + } + } + if runStart == len(packetBuffers) { + return writeErr + } + if runUsesSystemDevice { + return E.Errors(writeErr, d.systemDevice.writeBuffers(packetBuffers[runStart:])) + } + return E.Errors(writeErr, d.stackDevice.writeBuffers(packetBuffers[runStart:])) +} + +func packetDestination(packet []byte) netip.Addr { + switch header.IPVersion(packet) { + case header.IPv4Version: + return header.IPv4(packet).DestinationAddr() + case header.IPv6Version: + return header.IPv6(packet).DestinationAddr() + default: + return netip.Addr{} + } +} + +func (d *systemStackDevice) Close() error { + return E.Errors(d.stackDevice.Close(), d.systemDevice.Close()) +} diff --git a/transport/openvpn/device.go b/transport/openvpn/device.go new file mode 100644 index 000000000..0fe16b853 --- /dev/null +++ b/transport/openvpn/device.go @@ -0,0 +1,224 @@ +package openvpn + +import ( + "context" + "net/netip" + "slices" + "sync/atomic" + "time" + + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + N "github.com/sagernet/sing/common/network" +) + +const ( + DefaultMTU = 1500 + PacketHeadroom = 4096 +) + +type PacketWriter func(packetBuffers []*buf.Buffer) error + +type Device interface { + N.Dialer + Start() error + UpdateConfiguration(configuration Configuration) error + WriteInboundBuffers(packetBuffers []*buf.Buffer) error + SetPacketWriter(writer PacketWriter) + PortAddresses() (netip.Addr, netip.Addr) + PortMTU() uint32 + AttachReturn(returnPath tun.Return) error + DetachReturn(returnPath tun.Return) error + ReturnPath() (tun.Return, int) + Close() error +} + +type DeviceOptions struct { + Context context.Context + Logger logger.ContextLogger + System bool + Handler tun.Handler + UDPTimeout time.Duration + ICMPTimeout time.Duration + Name string + MTU uint32 + Configuration Configuration +} + +type Configuration struct { + MTU uint32 + Address []netip.Prefix + Routes []Route + DNS []netip.Addr + Topology string + Interface string + BlockIPv6 bool +} + +type Route struct { + Prefix netip.Prefix + Gateway netip.Addr + Metric int +} + +func NewDevice(options DeviceOptions) (Device, error) { + if !options.System { + return newStackDevice(options) + } + if !tun.WithGVisor { + return newSystemDevice(options) + } + return newSystemStackDevice(options) +} + +type baseDevice struct { + packetWriter PacketWriter + returnState atomic.Pointer[returnPathState] +} + +func (d *baseDevice) SetPacketWriter(writer PacketWriter) { + d.packetWriter = writer +} + +func (d *baseDevice) writeOutbound(packetBuffers []*buf.Buffer) error { + if d.packetWriter == nil { + buf.ReleaseMulti(packetBuffers) + return E.New("missing OpenVPN packet writer") + } + return d.packetWriter(packetBuffers) +} + +func (d *baseDevice) processInboundBuffers(packetBuffers []*buf.Buffer, writeBuffers func(packetBuffers []*buf.Buffer) error) error { + if len(packetBuffers) == 0 { + return nil + } + state := d.returnState.Load() + if state == nil { + return writeBuffers(packetBuffers) + } + packets := make([][]byte, len(packetBuffers)) + for i, packetBuffer := range packetBuffers { + packetBuffer.ExtendHeader(state.headroom) + packets[i] = packetBuffer.Bytes() + } + unconsumed := state.returnPath.ReturnPackets(packets) + if len(unconsumed) == 0 { + return nil + } + unconsumedBuffers := make([]*buf.Buffer, len(unconsumed)) + for i, packet := range unconsumed { + packetBuffer := buf.As(packet) + packetBuffer.Advance(state.headroom) + unconsumedBuffers[i] = packetBuffer + } + return writeBuffers(unconsumedBuffers) +} + +func (d *baseDevice) AttachReturn(returnPath tun.Return) error { + headroom := returnPath.ReturnHeadroom() + if headroom > PacketHeadroom { + return E.New("return path headroom ", headroom, " exceeds available ", PacketHeadroom) + } + newState := &returnPathState{ + returnPath: returnPath, + headroom: headroom, + } + for { + currentState := d.returnState.Load() + if currentState != nil { + if currentState.returnPath == returnPath { + return nil + } + return E.New("return path already attached") + } + if d.returnState.CompareAndSwap(nil, newState) { + return nil + } + } +} + +func (d *baseDevice) DetachReturn(returnPath tun.Return) error { + currentState := d.returnState.Load() + if currentState != nil && currentState.returnPath == returnPath { + d.returnState.CompareAndSwap(currentState, nil) + } + return nil +} + +func (d *baseDevice) ReturnPath() (tun.Return, int) { + state := d.returnState.Load() + if state == nil { + return nil, 0 + } + return state.returnPath, state.headroom +} + +type returnPathState struct { + returnPath tun.Return + headroom int +} + +func firstAddresses(addresses []netip.Prefix) (netip.Addr, netip.Addr) { + var inet4Address netip.Addr + var inet6Address netip.Addr + for _, prefix := range addresses { + if prefix.Addr().Is4() && !inet4Address.IsValid() { + inet4Address = prefix.Addr() + } else if prefix.Addr().Is6() && !inet6Address.IsValid() { + inet6Address = prefix.Addr() + } + } + return inet4Address, inet6Address +} + +func splitPrefixes(prefixes []netip.Prefix) ([]netip.Prefix, []netip.Prefix) { + var inet4Prefixes []netip.Prefix + var inet6Prefixes []netip.Prefix + for _, prefix := range prefixes { + if prefix.Addr().Is4() { + inet4Prefixes = append(inet4Prefixes, prefix) + } else { + inet6Prefixes = append(inet6Prefixes, prefix) + } + } + return inet4Prefixes, inet6Prefixes +} + +func splitRoutes(routes []Route) ([]netip.Prefix, []netip.Prefix) { + var inet4Prefixes []netip.Prefix + var inet6Prefixes []netip.Prefix + for _, route := range routes { + if route.Prefix.Addr().Is4() { + inet4Prefixes = append(inet4Prefixes, route.Prefix) + } else { + inet6Prefixes = append(inet6Prefixes, route.Prefix) + } + } + return inet4Prefixes, inet6Prefixes +} + +func routesWithBlockIPv6(configuration Configuration) []Route { + routes := configuration.Routes + if !configuration.BlockIPv6 { + return routes + } + inet6DefaultRoute := netip.PrefixFrom(netip.IPv6Unspecified(), 0) + for _, route := range routes { + if route.Prefix == inet6DefaultRoute { + return routes + } + } + routes = append(slices.Clone(routes), Route{Prefix: inet6DefaultRoute}) + return routes +} + +func hasRouteOptions(routes []Route) bool { + for _, route := range routes { + if route.Gateway.IsValid() || route.Metric != 0 { + return true + } + } + return false +} diff --git a/transport/openvpn/device_stack.go b/transport/openvpn/device_stack.go new file mode 100644 index 000000000..764ebfc26 --- /dev/null +++ b/transport/openvpn/device_stack.go @@ -0,0 +1,358 @@ +//go:build with_gvisor + +package openvpn + +import ( + "context" + "net" + "net/netip" + "slices" + "sync" + "sync/atomic" + + "github.com/sagernet/gvisor/pkg/buffer" + "github.com/sagernet/gvisor/pkg/tcpip" + "github.com/sagernet/gvisor/pkg/tcpip/adapters/gonet" + "github.com/sagernet/gvisor/pkg/tcpip/header" + "github.com/sagernet/gvisor/pkg/tcpip/network/ipv4" + "github.com/sagernet/gvisor/pkg/tcpip/network/ipv6" + "github.com/sagernet/gvisor/pkg/tcpip/stack" + "github.com/sagernet/gvisor/pkg/tcpip/transport/icmp" + "github.com/sagernet/gvisor/pkg/tcpip/transport/tcp" + "github.com/sagernet/gvisor/pkg/tcpip/transport/udp" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +var _ Device = (*stackDevice)(nil) + +type stackDevice struct { + baseDevice + stateAccess sync.RWMutex + options DeviceOptions + stack *stack.Stack + endpoint *stackEndpoint + inet4Address netip.Addr + inet6Address netip.Addr + icmpForwarder *tun.ICMPForwarder + logRouteOptions bool + closeOnce sync.Once +} + +func newStackDevice(options DeviceOptions) (*stackDevice, error) { + if options.MTU == 0 { + options.MTU = DefaultMTU + } + device := &stackDevice{ + options: options, + logRouteOptions: true, + } + endpoint := &stackEndpoint{ + device: device, + done: make(chan struct{}), + } + endpoint.mtu.Store(options.MTU) + ipStack, err := tun.NewGVisorStackWithOptions(endpoint, stack.NICOptions{}, true) + if err != nil { + return nil, err + } + device.stack = ipStack + device.endpoint = endpoint + err = device.updateAddresses(nil, options.Configuration.Address) + if err != nil { + ipStack.Close() + return nil, err + } + if options.Handler != nil { + ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(options.Context, ipStack, options.Handler).HandlePacket) + ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(options.Context, ipStack, options.Handler, tun.UDPNatOptions{ + Timeout: options.UDPTimeout, + }).HandlePacket) + icmpForwarder := tun.NewICMPForwarder(ipStack, options.Handler, options.Logger) + ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket) + ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket) + device.icmpForwarder = icmpForwarder + } + return device, nil +} + +func (d *stackDevice) Start() error { + return nil +} + +func (d *stackDevice) UpdateConfiguration(configuration Configuration) error { + d.stateAccess.Lock() + defer d.stateAccess.Unlock() + if d.logRouteOptions && hasRouteOptions(configuration.Routes) { + d.options.Logger.Debug("OpenVPN route gateway and metric options are not representable by the gVisor stack device; routes are installed by prefix") + } + if configuration.MTU != 0 { + d.options.MTU = configuration.MTU + d.endpoint.mtu.Store(configuration.MTU) + } + previousAddresses := d.options.Configuration.Address + d.options.Configuration = configuration + return d.updateAddresses(previousAddresses, configuration.Address) +} + +func (d *stackDevice) updateAddresses(previousAddresses []netip.Prefix, addresses []netip.Prefix) error { + for _, prefix := range previousAddresses { + if slices.Contains(addresses, prefix) { + continue + } + gErr := d.stack.RemoveAddress(tun.DefaultNIC, tun.AddressFromAddr(prefix.Addr())) + if gErr != nil { + return E.New("remove local address ", prefix, ": ", gErr.String()) + } + } + for _, prefix := range addresses { + if slices.Contains(previousAddresses, prefix) { + continue + } + protocolAddress := tcpip.ProtocolAddress{ + AddressWithPrefix: tcpip.AddressWithPrefix{ + Address: tun.AddressFromAddr(prefix.Addr()), + PrefixLen: prefix.Bits(), + }, + } + if prefix.Addr().Is4() { + protocolAddress.Protocol = ipv4.ProtocolNumber + } else { + protocolAddress.Protocol = ipv6.ProtocolNumber + } + gErr := d.stack.AddProtocolAddress(tun.DefaultNIC, protocolAddress, stack.AddressProperties{}) + if gErr != nil { + return E.New("add local address ", prefix, ": ", gErr.String()) + } + } + d.inet4Address, d.inet6Address = firstAddresses(addresses) + return nil +} + +func (d *stackDevice) WriteInboundBuffers(packetBuffers []*buf.Buffer) error { + return d.processInboundBuffers(packetBuffers, d.writeBuffers) +} + +func (d *stackDevice) writeBuffers(packetBuffers []*buf.Buffer) error { + networkProtocols := make([]tcpip.NetworkProtocolNumber, 0, len(packetBuffers)) + stackPacketBuffers := make([]*stack.PacketBuffer, 0, len(packetBuffers)) + var packetErr error + for _, packetBuffer := range packetBuffers { + packet := packetBuffer.Bytes() + var networkProtocol tcpip.NetworkProtocolNumber + switch header.IPVersion(packet) { + case header.IPv4Version: + networkProtocol = header.IPv4ProtocolNumber + case header.IPv6Version: + networkProtocol = header.IPv6ProtocolNumber + default: + if packetErr == nil { + packetErr = E.New("invalid IP packet") + } + continue + } + networkProtocols = append(networkProtocols, networkProtocol) + stackPacketBuffers = append(stackPacketBuffers, stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: buffer.MakeWithData(packet), + })) + } + d.endpoint.deliverNetworkPackets(networkProtocols, stackPacketBuffers) + for _, packetBuffer := range stackPacketBuffers { + packetBuffer.DecRef() + } + return packetErr +} + +func (d *stackDevice) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + if destination.IsIPv6() && d.blockIPv6Enabled() { + return nil, E.New("IPv6 blocked by pushed OpenVPN block-ipv6") + } + inet4Address, inet6Address := d.PortAddresses() + address := tcpip.FullAddress{ + NIC: tun.DefaultNIC, + Port: destination.Port, + Addr: tun.AddressFromAddr(destination.Addr), + } + bind := tcpip.FullAddress{ + NIC: tun.DefaultNIC, + } + var networkProtocol tcpip.NetworkProtocolNumber + if destination.IsIPv4() { + if !inet4Address.IsValid() { + return nil, E.New("missing IPv4 local address") + } + networkProtocol = header.IPv4ProtocolNumber + bind.Addr = tun.AddressFromAddr(inet4Address) + } else { + if !inet6Address.IsValid() { + return nil, E.New("missing IPv6 local address") + } + networkProtocol = header.IPv6ProtocolNumber + bind.Addr = tun.AddressFromAddr(inet6Address) + } + switch N.NetworkName(network) { + case N.NetworkTCP: + return gonet.DialTCPWithBind(ctx, d.stack, bind, address, networkProtocol) + case N.NetworkUDP: + return gonet.DialUDP(d.stack, &bind, &address, networkProtocol) + default: + return nil, E.Extend(N.ErrUnknownNetwork, network) + } +} + +func (d *stackDevice) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + if destination.IsIPv6() && d.blockIPv6Enabled() { + return nil, E.New("IPv6 blocked by pushed OpenVPN block-ipv6") + } + inet4Address, inet6Address := d.PortAddresses() + bind := tcpip.FullAddress{ + NIC: tun.DefaultNIC, + } + var networkProtocol tcpip.NetworkProtocolNumber + if destination.IsIPv4() { + if !inet4Address.IsValid() { + return nil, E.New("missing IPv4 local address") + } + networkProtocol = header.IPv4ProtocolNumber + bind.Addr = tun.AddressFromAddr(inet4Address) + } else { + if !inet6Address.IsValid() { + return nil, E.New("missing IPv6 local address") + } + networkProtocol = header.IPv6ProtocolNumber + bind.Addr = tun.AddressFromAddr(inet6Address) + } + return gonet.DialUDP(d.stack, &bind, nil, networkProtocol) +} + +func (d *stackDevice) blockIPv6Enabled() bool { + d.stateAccess.RLock() + defer d.stateAccess.RUnlock() + return d.options.Configuration.BlockIPv6 +} + +func (d *stackDevice) PortAddresses() (netip.Addr, netip.Addr) { + d.stateAccess.RLock() + defer d.stateAccess.RUnlock() + return d.inet4Address, d.inet6Address +} + +func (d *stackDevice) PortMTU() uint32 { + d.stateAccess.RLock() + defer d.stateAccess.RUnlock() + return d.options.MTU +} + +func (d *stackDevice) Close() error { + d.closeOnce.Do(func() { + close(d.endpoint.done) + if d.icmpForwarder != nil { + d.icmpForwarder.Close() + } + d.stack.Close() + for _, endpoint := range d.stack.CleanupEndpoints() { + endpoint.Abort() + } + d.stack.Wait() + }) + return nil +} + +type stackEndpoint struct { + device *stackDevice + mtu atomic.Uint32 + done chan struct{} + dispatcherAccess sync.RWMutex + dispatcher stack.NetworkDispatcher +} + +func (e *stackEndpoint) MTU() uint32 { + return e.mtu.Load() +} + +func (e *stackEndpoint) SetMTU(mtu uint32) { + e.mtu.Store(mtu) +} + +func (e *stackEndpoint) MaxHeaderLength() uint16 { + return 0 +} + +func (e *stackEndpoint) LinkAddress() tcpip.LinkAddress { + return "" +} + +func (e *stackEndpoint) SetLinkAddress(addr tcpip.LinkAddress) { +} + +func (e *stackEndpoint) Capabilities() stack.LinkEndpointCapabilities { + return stack.CapabilityRXChecksumOffload +} + +func (e *stackEndpoint) Attach(dispatcher stack.NetworkDispatcher) { + e.dispatcherAccess.Lock() + defer e.dispatcherAccess.Unlock() + e.dispatcher = dispatcher +} + +func (e *stackEndpoint) IsAttached() bool { + e.dispatcherAccess.RLock() + defer e.dispatcherAccess.RUnlock() + return e.dispatcher != nil +} + +func (e *stackEndpoint) deliverNetworkPackets(networkProtocols []tcpip.NetworkProtocolNumber, packetBuffers []*stack.PacketBuffer) { + e.dispatcherAccess.RLock() + defer e.dispatcherAccess.RUnlock() + if e.dispatcher == nil { + return + } + for i, packetBuffer := range packetBuffers { + e.dispatcher.DeliverNetworkPacket(networkProtocols[i], packetBuffer) + } +} + +func (e *stackEndpoint) Wait() { +} + +func (e *stackEndpoint) ARPHardwareType() header.ARPHardwareType { + return header.ARPHardwareNone +} + +func (e *stackEndpoint) AddHeader(packetBuffer *stack.PacketBuffer) { +} + +func (e *stackEndpoint) ParseHeader(packetBuffer *stack.PacketBuffer) bool { + return true +} + +func (e *stackEndpoint) WritePackets(list stack.PacketBufferList) (int, tcpip.Error) { + packetBuffers := make([]*buf.Buffer, 0, list.Len()) + for _, packetBuffer := range list.AsSlice() { + packetSlices := packetBuffer.AsSlices() + packetLength := 0 + for _, packetSlice := range packetSlices { + packetLength += len(packetSlice) + } + outboundBuffer := buf.NewSize(PacketHeadroom + packetLength + systemDevicePacketRearSpace) + outboundBuffer.Resize(PacketHeadroom, 0) + for _, packetSlice := range packetSlices { + _, _ = outboundBuffer.Write(packetSlice) + } + packetBuffers = append(packetBuffers, outboundBuffer) + } + err := e.device.writeOutbound(packetBuffers) + if err != nil { + return 0, &tcpip.ErrClosedForSend{} + } + return list.Len(), nil +} + +func (e *stackEndpoint) Close() { +} + +func (e *stackEndpoint) SetOnCloseAction(action func()) { +} diff --git a/transport/openvpn/device_stack_stub.go b/transport/openvpn/device_stack_stub.go new file mode 100644 index 000000000..fc438a7ca --- /dev/null +++ b/transport/openvpn/device_stack_stub.go @@ -0,0 +1,13 @@ +//go:build !with_gvisor + +package openvpn + +import E "github.com/sagernet/sing/common/exceptions" + +func newStackDevice(options DeviceOptions) (Device, error) { + return nil, E.New("OpenVPN system:false requires the with_gvisor build tag") +} + +func newSystemStackDevice(options DeviceOptions) (Device, error) { + return nil, E.New("OpenVPN system stack requires the with_gvisor build tag") +} diff --git a/transport/openvpn/device_system.go b/transport/openvpn/device_system.go new file mode 100644 index 000000000..bea50ab2c --- /dev/null +++ b/transport/openvpn/device_system.go @@ -0,0 +1,435 @@ +package openvpn + +import ( + "context" + "net" + "net/netip" + "runtime" + "slices" + "sync" + "syscall" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun/gtcpip/header" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" +) + +var _ Device = (*systemDevice)(nil) + +const ( + systemDeviceReadBufferSize = 65535 + tun.PacketOffset + systemDevicePacketRearSpace = 64 +) + +type systemDevice struct { + baseDevice + stateAccess sync.RWMutex + options DeviceOptions + dialer N.Dialer + device tun.Tun + inet4Address netip.Addr + inet6Address netip.Addr + closed bool +} + +func newSystemDevice(options DeviceOptions) (*systemDevice, error) { + if options.Name == "" { + options.Name = tun.CalculateInterfaceName("ovpn") + } + if options.MTU == 0 { + options.MTU = DefaultMTU + } + interfaceDialer, err := dialer.NewDefault(options.Context, option.DialerOptions{ + BindInterface: options.Name, + }) + if err != nil { + return nil, err + } + inet4Address, inet6Address := firstAddresses(options.Configuration.Address) + return &systemDevice{ + options: options, + dialer: interfaceDialer, + inet4Address: inet4Address, + inet6Address: inet6Address, + }, nil +} + +func (d *systemDevice) Start() error { + d.stateAccess.Lock() + defer d.stateAccess.Unlock() + return d.startLocked() +} + +func (d *systemDevice) startLocked() error { + if d.closed { + return net.ErrClosed + } + if d.device != nil { + return nil + } + tunOptions := d.buildTunOptions() + tunInterface, err := tun.New(tunOptions) + if err != nil { + return err + } + err = tunInterface.Start() + if err != nil { + tunInterface.Close() + return err + } + d.device = tunInterface + d.options.Logger.Info("started at ", d.options.Name) + go d.readLoop(tunInterface, int(d.options.MTU)) + return nil +} + +func (d *systemDevice) buildTunOptions() tun.Options { + inet4Address, inet6Address := firstAddresses(d.options.Configuration.Address) + d.inet4Address = inet4Address + d.inet6Address = inet6Address + inet4Addresses, inet6Addresses := splitPrefixes(d.options.Configuration.Address) + if d.options.Configuration.BlockIPv6 && len(inet6Addresses) == 0 { + inet6Addresses = append(inet6Addresses, netip.MustParsePrefix("fddd:1194:1194:1194::2/64")) + } + routes := routesWithBlockIPv6(d.options.Configuration) + inet4Routes, inet6Routes := splitRoutes(routes) + inet4Gateway, _ := systemRouteGateway(routes, true) + inet6Gateway, _ := systemRouteGateway(routes, false) + networkManager := service.FromContext[adapter.NetworkManager](d.options.Context) + tunOptions := tun.Options{ + Name: d.options.Name, + Inet4Address: inet4Addresses, + Inet6Address: inet6Addresses, + MTU: d.options.MTU, + GSO: true, + InterfaceScope: true, + DNSAddress: d.options.Configuration.DNS, + Inet4Gateway: inet4Gateway, + Inet6Gateway: inet6Gateway, + Inet4RouteAddress: inet4Routes, + Inet6RouteAddress: inet6Routes, + InterfaceMonitor: nil, + InterfaceFinder: nil, + Logger: d.options.Logger, + IPRoute2TableIndex: tun.DefaultIPRoute2TableIndex, + IPRoute2RuleIndex: tun.DefaultIPRoute2RuleIndex, + EXP_DisableDNSHijack: true, + } + if runtime.GOOS == "darwin" { + tunOptions.AutoRoute = true + } + if networkManager != nil { + tunOptions.InterfaceMonitor = networkManager.InterfaceMonitor() + tunOptions.InterfaceFinder = networkManager.InterfaceFinder() + } + return tunOptions +} + +func systemRouteGateway(routes []Route, ipv4 bool) (netip.Addr, bool) { + var gateway netip.Addr + var hasGateway bool + var hasMissingGateway bool + var gatewayUnrepresentable bool + var metricUnrepresentable bool + for _, route := range routes { + if route.Prefix.Addr().Is4() != ipv4 { + continue + } + if route.Metric != 0 { + metricUnrepresentable = true + } + if !route.Gateway.IsValid() { + hasMissingGateway = true + continue + } + if route.Gateway.Is4() != ipv4 { + gatewayUnrepresentable = true + continue + } + if !hasGateway { + gateway = route.Gateway + hasGateway = true + } else if gateway != route.Gateway { + gatewayUnrepresentable = true + } + } + if hasGateway && hasMissingGateway { + gatewayUnrepresentable = true + } + if gatewayUnrepresentable { + gateway = netip.Addr{} + } + return gateway, gatewayUnrepresentable || metricUnrepresentable +} + +func (d *systemDevice) readLoop(tunInterface tun.Tun, mtu int) { + linuxTUN, isLinuxTUN := tunInterface.(tun.LinuxTUN) + if isLinuxTUN && linuxTUN.BatchSize() > 1 { + d.readLoopLinux(linuxTUN, linuxTUN.BatchSize(), mtu) + return + } + darwinTUN, isDarwinTUN := tunInterface.(tun.DarwinTUN) + if isDarwinTUN { + d.readLoopDarwin(darwinTUN) + return + } + packetBuffer := buf.NewSize(PacketHeadroom + systemDeviceReadBufferSize + systemDevicePacketRearSpace) + defer packetBuffer.Release() + for { + packetBuffer.Reset() + packetBuffer.Resize(PacketHeadroom, 0) + readN, err := tunInterface.Read(packetBuffer.FreeBytes()[:systemDeviceReadBufferSize]) + if err != nil { + if E.IsClosed(err) { + return + } + d.options.Logger.Error(E.Cause(err, "read packet")) + continue + } + if readN <= tun.PacketOffset { + continue + } + packetBuffer.Truncate(readN) + packetBuffer.Advance(tun.PacketOffset) + if d.blockIPv6Enabled() && header.IPVersion(packetBuffer.Bytes()) == header.IPv6Version { + continue + } + packetBuffer.IncRef() + err = d.writeOutbound([]*buf.Buffer{packetBuffer}) + packetBuffer.DecRef() + if err != nil { + d.options.Logger.Error(E.Cause(err, "write packet")) + } + } +} + +func (d *systemDevice) readLoopLinux(tunInterface tun.LinuxTUN, batchSize int, mtu int) { + packetBuffers := make([]*buf.Buffer, batchSize) + readBuffers := make([][]byte, batchSize) + packetSizes := make([]int, batchSize) + outboundBuffers := make([]*buf.Buffer, 0, batchSize) + for i := range packetBuffers { + packetBuffers[i] = buf.NewSize(PacketHeadroom + mtu + systemDevicePacketRearSpace) + } + defer buf.ReleaseMulti(packetBuffers) + for { + for i, packetBuffer := range packetBuffers { + packetBuffer.Reset() + packetBuffer.Resize(PacketHeadroom, 0) + readBuffers[i] = packetBuffer.FreeBytes()[:mtu] + } + packetCount, readErr := tunInterface.BatchRead(readBuffers, 0, packetSizes) + outboundBuffers = outboundBuffers[:0] + blockIPv6 := d.blockIPv6Enabled() + for i := range packetCount { + packetBuffers[i].Truncate(packetSizes[i]) + if blockIPv6 && header.IPVersion(packetBuffers[i].Bytes()) == header.IPv6Version { + continue + } + packetBuffers[i].IncRef() + outboundBuffers = append(outboundBuffers, packetBuffers[i]) + } + if len(outboundBuffers) > 0 { + writeErr := d.writeOutbound(outboundBuffers) + for _, packetBuffer := range outboundBuffers { + packetBuffer.DecRef() + } + if writeErr != nil { + d.options.Logger.Error(E.Cause(writeErr, "write packet batch")) + } + } + if readErr != nil { + if E.IsClosed(readErr) { + return + } + d.options.Logger.Error(E.Cause(readErr, "batch read packet")) + } + } +} + +func (d *systemDevice) readLoopDarwin(tunInterface tun.DarwinTUN) { + for { + packetBuffers, readErr := tunInterface.BatchRead() + outboundBuffers := packetBuffers[:0] + blockIPv6 := d.blockIPv6Enabled() + for _, packetBuffer := range packetBuffers { + if packetBuffer.IsEmpty() { + packetBuffer.Release() + continue + } + if blockIPv6 && header.IPVersion(packetBuffer.Bytes()) == header.IPv6Version { + packetBuffer.Release() + continue + } + outboundBuffers = append(outboundBuffers, packetBuffer) + } + if len(outboundBuffers) > 0 { + writeErr := d.writeOutbound(outboundBuffers) + if writeErr != nil { + d.options.Logger.Error(E.Cause(writeErr, "write packet batch")) + } + } + if readErr != nil { + if E.IsClosed(readErr) || E.IsMulti(readErr, syscall.EBADF) { + return + } + d.options.Logger.Error(E.Cause(readErr, "batch read packet")) + } + } +} + +func (d *systemDevice) UpdateConfiguration(configuration Configuration) error { + d.stateAccess.Lock() + defer d.stateAccess.Unlock() + routes := routesWithBlockIPv6(configuration) + _, hasUnrepresentableInet4RouteOptions := systemRouteGateway(routes, true) + _, hasUnrepresentableInet6RouteOptions := systemRouteGateway(routes, false) + if hasUnrepresentableInet4RouteOptions || hasUnrepresentableInet6RouteOptions { + d.options.Logger.Debug("some OpenVPN route gateway or metric options are not representable by the system device; routes are installed by prefix") + } + previousConfiguration := d.options.Configuration + previousMTU := d.options.MTU + updatedMTU := d.options.MTU + if configuration.MTU != 0 { + updatedMTU = configuration.MTU + } + d.options.MTU = updatedMTU + d.options.Configuration = configuration + if d.device == nil { + inet4Address, inet6Address := firstAddresses(configuration.Address) + d.inet4Address = inet4Address + d.inet6Address = inet6Address + return nil + } + if !slices.Equal(previousConfiguration.Address, configuration.Address) || + previousMTU != updatedMTU || + !slices.Equal(previousConfiguration.DNS, configuration.DNS) || + previousConfiguration.BlockIPv6 != configuration.BlockIPv6 { + d.device.Close() + d.device = nil + return d.startLocked() + } + return d.device.UpdateRouteOptions(d.buildTunOptions()) +} + +func (d *systemDevice) blockIPv6Enabled() bool { + d.stateAccess.RLock() + defer d.stateAccess.RUnlock() + return d.options.Configuration.BlockIPv6 +} + +func (d *systemDevice) WriteInboundBuffers(packetBuffers []*buf.Buffer) error { + return d.processInboundBuffers(packetBuffers, d.writeBuffers) +} + +func (d *systemDevice) writeBuffers(packetBuffers []*buf.Buffer) error { + d.stateAccess.RLock() + tunInterface := d.device + d.stateAccess.RUnlock() + if tunInterface == nil { + return E.New("OpenVPN system device is not ready") + } + linuxTUN, isLinuxTUN := tunInterface.(tun.LinuxTUN) + if isLinuxTUN { + headroom := linuxTUN.FrontHeadroom() + packets := make([][]byte, len(packetBuffers)) + var temporaryBuffers []*buf.Buffer + for i, packetBuffer := range packetBuffers { + if packetBuffer.Start() >= headroom { + packetBuffer.ExtendHeader(headroom) + packets[i] = packetBuffer.Bytes() + packetBuffer.Advance(headroom) + continue + } + temporaryBuffer := buf.NewSize(headroom + packetBuffer.Len()) + temporaryBuffer.Resize(headroom, 0) + _, _ = temporaryBuffer.Write(packetBuffer.Bytes()) + temporaryBuffer.ExtendHeader(headroom) + packets[i] = temporaryBuffer.Bytes() + temporaryBuffers = append(temporaryBuffers, temporaryBuffer) + } + _, err := linuxTUN.BatchWrite(packets, headroom) + buf.ReleaseMulti(temporaryBuffers) + return err + } + darwinTUN, isDarwinTUN := tunInterface.(tun.DarwinTUN) + if isDarwinTUN { + return darwinTUN.BatchWrite(packetBuffers) + } + for _, packetBuffer := range packetBuffers { + err := d.writePacket(packetBuffer.Bytes()) + if err != nil { + return err + } + } + return nil +} + +func (d *systemDevice) writePacket(packet []byte) error { + d.stateAccess.RLock() + tunInterface := d.device + d.stateAccess.RUnlock() + if tunInterface == nil { + return E.New("OpenVPN system device is not ready") + } + if tun.PacketOffset == 0 { + _, err := tunInterface.Write(packet) + return err + } + writeBuffer := make([]byte, tun.PacketOffset+len(packet)) + tun.PacketFillHeader(writeBuffer[:tun.PacketOffset], header.IPVersion(packet)) + copy(writeBuffer[tun.PacketOffset:], packet) + _, err := tunInterface.Write(writeBuffer) + return err +} + +func (d *systemDevice) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + if !destination.Addr.IsValid() { + return nil, E.New("invalid non-IP destination") + } + return d.dialer.DialContext(ctx, network, destination) +} + +func (d *systemDevice) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + if !destination.Addr.IsValid() { + return nil, E.New("invalid non-IP destination") + } + return d.dialer.ListenPacket(ctx, destination) +} + +func (d *systemDevice) PortAddresses() (netip.Addr, netip.Addr) { + d.stateAccess.RLock() + defer d.stateAccess.RUnlock() + return d.inet4Address, d.inet6Address +} + +func (d *systemDevice) PortMTU() uint32 { + d.stateAccess.RLock() + defer d.stateAccess.RUnlock() + return d.options.MTU +} + +func (d *systemDevice) Close() error { + d.stateAccess.Lock() + defer d.stateAccess.Unlock() + d.closed = true + if d.device == nil { + return nil + } + err := d.device.Close() + d.device = nil + return err +} + +func (d *systemDevice) configurationAddresses() []netip.Prefix { + d.stateAccess.RLock() + defer d.stateAccess.RUnlock() + return slices.Clone(d.options.Configuration.Address) +} diff --git a/transport/openvpn/device_system_stack.go b/transport/openvpn/device_system_stack.go new file mode 100644 index 000000000..3762c2db4 --- /dev/null +++ b/transport/openvpn/device_system_stack.go @@ -0,0 +1,106 @@ +//go:build with_gvisor + +package openvpn + +import ( + "net/netip" + + "github.com/sagernet/sing-tun/gtcpip/header" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" +) + +var _ Device = (*systemStackDevice)(nil) + +type systemStackDevice struct { + *systemDevice + stackDevice *stackDevice +} + +func newSystemStackDevice(options DeviceOptions) (*systemStackDevice, error) { + system, err := newSystemDevice(options) + if err != nil { + return nil, err + } + stackOptions := options + stackOptions.System = false + stackDevice, err := newStackDevice(stackOptions) + if err != nil { + system.Close() + return nil, err + } + stackDevice.logRouteOptions = false + return &systemStackDevice{ + systemDevice: system, + stackDevice: stackDevice, + }, nil +} + +func (d *systemStackDevice) SetPacketWriter(writer PacketWriter) { + d.systemDevice.SetPacketWriter(writer) + d.stackDevice.SetPacketWriter(writer) +} + +func (d *systemStackDevice) UpdateConfiguration(configuration Configuration) error { + err := d.systemDevice.UpdateConfiguration(configuration) + if err != nil { + return err + } + return d.stackDevice.UpdateConfiguration(configuration) +} + +func (d *systemStackDevice) WriteInboundBuffers(packetBuffers []*buf.Buffer) error { + return d.systemDevice.processInboundBuffers(packetBuffers, d.writeBuffers) +} + +func (d *systemStackDevice) writeBuffers(packetBuffers []*buf.Buffer) error { + addresses := d.systemDevice.configurationAddresses() + runStart := 0 + runUsesSystemDevice := false + var writeErr error + for i, packetBuffer := range packetBuffers { + destination := packetDestination(packetBuffer.Bytes()) + useSystemDevice := false + for _, prefix := range addresses { + if prefix.Contains(destination) { + useSystemDevice = true + break + } + } + if i > runStart && useSystemDevice != runUsesSystemDevice { + var err error + if runUsesSystemDevice { + err = d.systemDevice.writeBuffers(packetBuffers[runStart:i]) + } else { + err = d.stackDevice.writeBuffers(packetBuffers[runStart:i]) + } + writeErr = E.Errors(writeErr, err) + runStart = i + } + if i == runStart { + runUsesSystemDevice = useSystemDevice + } + } + if runStart == len(packetBuffers) { + return writeErr + } + if runUsesSystemDevice { + return E.Errors(writeErr, d.systemDevice.writeBuffers(packetBuffers[runStart:])) + } + return E.Errors(writeErr, d.stackDevice.writeBuffers(packetBuffers[runStart:])) +} + +func packetDestination(packet []byte) netip.Addr { + switch header.IPVersion(packet) { + case header.IPv4Version: + return header.IPv4(packet).DestinationAddr() + case header.IPv6Version: + return header.IPv6(packet).DestinationAddr() + default: + return netip.Addr{} + } +} + +func (d *systemStackDevice) Close() error { + return E.Errors(d.stackDevice.Close(), d.systemDevice.Close()) +} diff --git a/transport/wireguard/endpoint.go b/transport/wireguard/endpoint.go index 97e4dae9a..c21db3235 100644 --- a/transport/wireguard/endpoint.go +++ b/transport/wireguard/endpoint.go @@ -152,10 +152,10 @@ func (e *Endpoint) Start(resolve bool) error { return nil } var bind conn.Bind - wireGuardListener, isWireGuardListener := common.Cast[dialer.WireGuardListener](e.options.Dialer) - if isWireGuardListener { - wireGuardControl, _ := wireGuardListener.WireGuardControl() - standardBind := conn.NewStdNetBind(wireGuardControl).(*conn.StdNetBind) + udpListener, isUDPListener := common.Cast[dialer.UDPListener](e.options.Dialer) + if isUDPListener { + listenerControl, _ := udpListener.UDPListenerControl() + standardBind := conn.NewStdNetBind(listenerControl).(*conn.StdNetBind) if e.options.ListenPort == 0 && len(e.peers) == 1 && e.peers[0].endpoint.IsValid() { standardBind.SetSinglePeerMode() } @@ -176,7 +176,7 @@ func (e *Endpoint) Start(resolve bool) error { } bind = NewClientBind(e.options.Context, e.options.Logger, e.options.Dialer, isConnect, connectAddr, reserved) } - if isWireGuardListener || len(e.peers) > 1 { + if isUDPListener || len(e.peers) > 1 { for _, peer := range e.peers { if peer.reserved != [3]uint8{} { bind.SetReservedForEndpoint(peer.endpoint, peer.reserved)