mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-10-09 23:55:42 +00:00
Add openvpn and openconnect
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user