mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-09-15 21:00:27 +00:00
Add openvpn and openconnect
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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 == ""
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing/common/control"
|
||||
)
|
||||
|
||||
type UDPListener interface {
|
||||
UDPListenerControl() (control.Func, bool)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing/common/control"
|
||||
)
|
||||
|
||||
type WireGuardListener interface {
|
||||
WireGuardControl() (control.Func, bool)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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`。
|
||||
@@ -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.
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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`)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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`)
|
||||
})
|
||||
}
|
||||
@@ -111,6 +111,8 @@ func EndpointRegistry() *endpoint.Registry {
|
||||
registry := endpoint.NewRegistry()
|
||||
|
||||
registerWireGuardEndpoint(registry)
|
||||
registerOpenConnectEndpoint(registry)
|
||||
registerOpenVPNEndpoints(registry)
|
||||
registerTailscaleEndpoint(registry)
|
||||
|
||||
return registry
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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:
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
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 @@
|
||||
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 @@
|
||||
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
@@ -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
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+11
@@ -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
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()) {
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user