Add openvpn and openconnect

This commit is contained in:
世界
2026-08-30 17:41:43 +08:00
parent 23ca3b415f
commit 2df7a9bac7
65 changed files with 12495 additions and 386 deletions
+62
View File
@@ -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
}
+57
View File
@@ -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
}
+1 -1
View File
@@ -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")
+2 -2
View File
@@ -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 == ""
}
+9
View File
@@ -0,0 +1,9 @@
package dialer
import (
"github.com/sagernet/sing/common/control"
)
type UDPListener interface {
UDPListenerControl() (control.Func, bool)
}
-9
View File
@@ -1,9 +0,0 @@
package dialer
import (
"github.com/sagernet/sing/common/control"
)
type WireGuardListener interface {
WireGuardControl() (control.Func, bool)
}
+4 -1
View File
@@ -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))
}
+9
View File
@@ -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:
+291 -1
View File
@@ -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() {
}
+1272 -139
View File
File diff suppressed because it is too large Load Diff
+112
View File
@@ -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<string, string> 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;
}
+268 -28
View File
@@ -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",
}
+7 -4
View File
@@ -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
+7 -4
View File
@@ -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
+387
View File
@@ -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.
@@ -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。
@@ -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.
@@ -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。
@@ -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.
@@ -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`
+5 -1
View File
@@ -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.
+5 -1
View File
@@ -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。
+112 -66
View File
@@ -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{
@@ -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
}
@@ -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
}
+8
View File
@@ -47,6 +47,8 @@ require (
github.com/sagernet/sing v0.9.0-beta.3
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.20260803041931-6c84c468bea2
github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1
@@ -84,6 +86,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
@@ -123,6 +126,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
@@ -157,6 +163,7 @@ require (
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260807161529-8d42107dcdfc // indirect
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260807161529-8d42107dcdfc // indirect
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260807161529-8d42107dcdfc // 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
@@ -170,6 +177,7 @@ 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
+70
View File
@@ -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=
@@ -95,6 +97,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.20260803041931-6c84c468bea2 h1:XhJro6+Ou+WOPjfs22m14lY7Sh6sWPm/f0QiyFUgM60=
github.com/sagernet/sing-quic v0.6.4-0.20260803041931-6c84c468bea2/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.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
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.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
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.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
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.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
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.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
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.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
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.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
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=
+12
View File
@@ -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)
}
+23
View File
@@ -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`)
})
}
+12
View File
@@ -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)
}
+29
View File
@@ -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`)
})
}
+2
View File
@@ -111,6 +111,8 @@ func EndpointRegistry() *endpoint.Registry {
registry := endpoint.NewRegistry()
registerWireGuardEndpoint(registry)
registerOpenConnectEndpoint(registry)
registerOpenVPNEndpoints(registry)
registerTailscaleEndpoint(registry)
return registry
+3
View File
@@ -152,6 +152,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
+77
View File
@@ -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"`
}
+123
View File
@@ -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"`
}
+513
View File
@@ -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)
}
+158
View File
@@ -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()
}
+135
View File
@@ -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:
}
}
}
+607
View File
@@ -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)
}
+277
View File
@@ -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
}
}
+571
View File
@@ -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
}
+21
View File
@@ -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)
}
+28
View File
@@ -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)
})
}
+114
View File
@@ -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)
}
}
+3 -3
View File
@@ -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,
+1 -1
View File
@@ -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
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
+1 -1
View File
@@ -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
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
+1 -1
View File
@@ -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
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
+55 -40
View File
@@ -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
+162 -78
View File
@@ -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=
+575
View File
@@ -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)
}
+1797
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -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
+22
View File
@@ -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()
+200
View File
@@ -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
}
+341
View File
@@ -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()) {
}
@@ -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")
}
+366
View File
@@ -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)
}
@@ -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())
}
+224
View File
@@ -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
}
+358
View File
@@ -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()) {
}
+13
View File
@@ -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")
}
+435
View File
@@ -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)
}
+106
View File
@@ -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())
}
+5 -5
View File
@@ -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)