mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-07-17 21:31:04 +03:00
Add MTProxy, MASQUE, VPN, Link parser. Update AmneziaWG. Remove Tunneling
This commit is contained in:
82
transport/masque/adapter.go
Normal file
82
transport/masque/adapter.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package masque
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/wireguard-go/tun"
|
||||
"github.com/songgao/water"
|
||||
)
|
||||
|
||||
type NetstackAdapter struct {
|
||||
dev tun.Device
|
||||
tunnelBufPool sync.Pool
|
||||
tunnelSizesPool sync.Pool
|
||||
}
|
||||
|
||||
func (n *NetstackAdapter) ReadPacket(buf []byte) (int, error) {
|
||||
packetBufsPtr := n.tunnelBufPool.Get().(*[][]byte)
|
||||
sizesPtr := n.tunnelSizesPool.Get().(*[]int)
|
||||
|
||||
defer func() {
|
||||
(*packetBufsPtr)[0] = nil
|
||||
n.tunnelBufPool.Put(packetBufsPtr)
|
||||
n.tunnelSizesPool.Put(sizesPtr)
|
||||
}()
|
||||
|
||||
(*packetBufsPtr)[0] = buf
|
||||
(*sizesPtr)[0] = 0
|
||||
|
||||
_, err := n.dev.Read(*packetBufsPtr, *sizesPtr, 0)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return (*sizesPtr)[0], nil
|
||||
}
|
||||
|
||||
func (n *NetstackAdapter) WritePacket(pkt []byte) error {
|
||||
// Write expects a slice of packet buffers.
|
||||
_, err := n.dev.Write([][]byte{pkt}, 0)
|
||||
return err
|
||||
}
|
||||
|
||||
// NewNetstackAdapter creates a new NetstackAdapter.
|
||||
func NewNetstackAdapter(dev tun.Device) TunnelDevice {
|
||||
return &NetstackAdapter{
|
||||
dev: dev,
|
||||
tunnelBufPool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
buf := make([][]byte, 1)
|
||||
return &buf
|
||||
},
|
||||
},
|
||||
tunnelSizesPool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
sizes := make([]int, 1)
|
||||
return &sizes
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type WaterAdapter struct {
|
||||
iface *water.Interface
|
||||
}
|
||||
|
||||
func (w *WaterAdapter) ReadPacket(buf []byte) (int, error) {
|
||||
n, err := w.iface.Read(buf)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (w *WaterAdapter) WritePacket(pkt []byte) error {
|
||||
_, err := w.iface.Write(pkt)
|
||||
return err
|
||||
}
|
||||
|
||||
func NewWaterAdapter(iface *water.Interface) TunnelDevice {
|
||||
return &WaterAdapter{iface: iface}
|
||||
}
|
||||
34
transport/masque/buffer.go
Normal file
34
transport/masque/buffer.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package masque
|
||||
|
||||
import "sync"
|
||||
|
||||
type NetBuffer struct {
|
||||
capacity uint32
|
||||
buf sync.Pool
|
||||
}
|
||||
|
||||
func (n *NetBuffer) Get() []byte {
|
||||
return *(n.buf.Get().(*[]byte))
|
||||
}
|
||||
|
||||
func (n *NetBuffer) Put(buf []byte) {
|
||||
if cap(buf) != int(n.capacity) {
|
||||
return
|
||||
}
|
||||
n.buf.Put(&buf)
|
||||
}
|
||||
|
||||
func NewNetBuffer(capacity uint32) *NetBuffer {
|
||||
if capacity == 0 {
|
||||
panic("capacity must be greater than 0")
|
||||
}
|
||||
return &NetBuffer{
|
||||
capacity: capacity,
|
||||
buf: sync.Pool{
|
||||
New: func() interface{} {
|
||||
b := make([]byte, capacity)
|
||||
return &b
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
33
transport/masque/device.go
Normal file
33
transport/masque/device.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package masque
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
wgTun "github.com/sagernet/wireguard-go/tun"
|
||||
)
|
||||
|
||||
type Device interface {
|
||||
wgTun.Device
|
||||
N.Dialer
|
||||
Start() error
|
||||
}
|
||||
|
||||
type DeviceOptions struct {
|
||||
Context context.Context
|
||||
Logger logger.ContextLogger
|
||||
Handler tun.Handler
|
||||
UDPTimeout time.Duration
|
||||
CreateDialer func(interfaceName string) N.Dialer
|
||||
Name string
|
||||
MTU uint32
|
||||
Address []netip.Prefix
|
||||
}
|
||||
|
||||
func NewDevice(options DeviceOptions) (Device, error) {
|
||||
return newStackDevice(options)
|
||||
}
|
||||
307
transport/masque/device_stack.go
Normal file
307
transport/masque/device_stack.go
Normal file
@@ -0,0 +1,307 @@
|
||||
package masque
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
|
||||
"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-box/log"
|
||||
"github.com/sagernet/sing-box/transport/wireguard"
|
||||
"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"
|
||||
wgTun "github.com/sagernet/wireguard-go/tun"
|
||||
)
|
||||
|
||||
type stackDevice struct {
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
stack *stack.Stack
|
||||
mtu uint32
|
||||
events chan wgTun.Event
|
||||
wgTun.Device
|
||||
outbound chan *stack.PacketBuffer
|
||||
packetOutbound chan *buf.Buffer
|
||||
done chan struct{}
|
||||
dispatcher stack.NetworkDispatcher
|
||||
inet4Address netip.Addr
|
||||
inet6Address netip.Addr
|
||||
}
|
||||
|
||||
func newStackDevice(options DeviceOptions) (*stackDevice, error) {
|
||||
tunDevice := &stackDevice{
|
||||
ctx: options.Context,
|
||||
logger: options.Logger,
|
||||
mtu: options.MTU,
|
||||
events: make(chan wgTun.Event, 1),
|
||||
outbound: make(chan *stack.PacketBuffer, 256),
|
||||
packetOutbound: make(chan *buf.Buffer, 256),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
ipStack, err := tun.NewGVisorStackWithOptions((*wireEndpoint)(tunDevice), stack.NICOptions{}, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var (
|
||||
inet4Address netip.Addr
|
||||
inet6Address netip.Addr
|
||||
)
|
||||
for _, prefix := range options.Address {
|
||||
addr := tun.AddressFromAddr(prefix.Addr())
|
||||
protoAddr := tcpip.ProtocolAddress{
|
||||
AddressWithPrefix: tcpip.AddressWithPrefix{
|
||||
Address: addr,
|
||||
PrefixLen: prefix.Bits(),
|
||||
},
|
||||
}
|
||||
if prefix.Addr().Is4() {
|
||||
inet4Address = prefix.Addr()
|
||||
tunDevice.inet4Address = inet4Address
|
||||
protoAddr.Protocol = ipv4.ProtocolNumber
|
||||
} else {
|
||||
inet6Address = prefix.Addr()
|
||||
tunDevice.inet6Address = inet6Address
|
||||
protoAddr.Protocol = ipv6.ProtocolNumber
|
||||
}
|
||||
gErr := ipStack.AddProtocolAddress(tun.DefaultNIC, protoAddr, stack.AddressProperties{})
|
||||
if gErr != nil {
|
||||
return nil, E.New("parse local address ", protoAddr.AddressWithPrefix, ": ", gErr.String())
|
||||
}
|
||||
}
|
||||
tunDevice.stack = ipStack
|
||||
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, options.UDPTimeout).HandlePacket)
|
||||
icmpForwarder := tun.NewICMPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout)
|
||||
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket)
|
||||
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket)
|
||||
}
|
||||
return tunDevice, nil
|
||||
}
|
||||
|
||||
func (w *stackDevice) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
addr := 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 !w.inet4Address.IsValid() {
|
||||
return nil, E.New("missing IPv4 local address")
|
||||
}
|
||||
networkProtocol = header.IPv4ProtocolNumber
|
||||
bind.Addr = tun.AddressFromAddr(w.inet4Address)
|
||||
} else {
|
||||
if !w.inet6Address.IsValid() {
|
||||
return nil, E.New("missing IPv6 local address")
|
||||
}
|
||||
networkProtocol = header.IPv6ProtocolNumber
|
||||
bind.Addr = tun.AddressFromAddr(w.inet6Address)
|
||||
}
|
||||
switch N.NetworkName(network) {
|
||||
case N.NetworkTCP:
|
||||
tcpConn, err := wireguard.DialTCPWithBind(ctx, w.stack, bind, addr, networkProtocol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tcpConn, nil
|
||||
case N.NetworkUDP:
|
||||
udpConn, err := gonet.DialUDP(w.stack, &bind, &addr, networkProtocol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return udpConn, nil
|
||||
default:
|
||||
return nil, E.Extend(N.ErrUnknownNetwork, network)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *stackDevice) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
bind := tcpip.FullAddress{
|
||||
NIC: tun.DefaultNIC,
|
||||
}
|
||||
var networkProtocol tcpip.NetworkProtocolNumber
|
||||
if destination.IsIPv4() {
|
||||
networkProtocol = header.IPv4ProtocolNumber
|
||||
bind.Addr = tun.AddressFromAddr(w.inet4Address)
|
||||
} else {
|
||||
networkProtocol = header.IPv6ProtocolNumber
|
||||
bind.Addr = tun.AddressFromAddr(w.inet4Address)
|
||||
}
|
||||
udpConn, err := gonet.DialUDP(w.stack, &bind, nil, networkProtocol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return udpConn, nil
|
||||
}
|
||||
|
||||
func (w *stackDevice) Start() error {
|
||||
w.events <- wgTun.EventUp
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *stackDevice) File() *os.File {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *stackDevice) Read(bufs [][]byte, sizes []int, offset int) (count int, err error) {
|
||||
select {
|
||||
case packet, ok := <-w.outbound:
|
||||
if !ok {
|
||||
return 0, os.ErrClosed
|
||||
}
|
||||
defer packet.DecRef()
|
||||
var copyN int
|
||||
/*rangeIterate(packet.Data().AsRange(), func(view *buffer.View) {
|
||||
copyN += copy(bufs[0][offset+copyN:], view.AsSlice())
|
||||
})*/
|
||||
for _, view := range packet.AsSlices() {
|
||||
copyN += copy(bufs[0][offset+copyN:], view)
|
||||
}
|
||||
sizes[0] = copyN
|
||||
return 1, nil
|
||||
case packet := <-w.packetOutbound:
|
||||
defer packet.Release()
|
||||
sizes[0] = copy(bufs[0][offset:], packet.Bytes())
|
||||
return 1, nil
|
||||
case <-w.done:
|
||||
return 0, os.ErrClosed
|
||||
}
|
||||
}
|
||||
|
||||
func (w *stackDevice) Write(bufs [][]byte, offset int) (count int, err error) {
|
||||
for _, b := range bufs {
|
||||
b = b[offset:]
|
||||
if len(b) == 0 {
|
||||
continue
|
||||
}
|
||||
var networkProtocol tcpip.NetworkProtocolNumber
|
||||
switch header.IPVersion(b) {
|
||||
case header.IPv4Version:
|
||||
networkProtocol = header.IPv4ProtocolNumber
|
||||
case header.IPv6Version:
|
||||
networkProtocol = header.IPv6ProtocolNumber
|
||||
}
|
||||
packetBuffer := stack.NewPacketBuffer(stack.PacketBufferOptions{
|
||||
Payload: buffer.MakeWithData(b),
|
||||
})
|
||||
w.dispatcher.DeliverNetworkPacket(networkProtocol, packetBuffer)
|
||||
packetBuffer.DecRef()
|
||||
count++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (w *stackDevice) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *stackDevice) MTU() (int, error) {
|
||||
return int(w.mtu), nil
|
||||
}
|
||||
|
||||
func (w *stackDevice) Name() (string, error) {
|
||||
return "sing-box", nil
|
||||
}
|
||||
|
||||
func (w *stackDevice) Events() <-chan wgTun.Event {
|
||||
return w.events
|
||||
}
|
||||
|
||||
func (w *stackDevice) Close() error {
|
||||
close(w.done)
|
||||
close(w.events)
|
||||
w.stack.Close()
|
||||
for _, endpoint := range w.stack.CleanupEndpoints() {
|
||||
endpoint.Abort()
|
||||
}
|
||||
w.stack.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *stackDevice) BatchSize() int {
|
||||
return 1
|
||||
}
|
||||
|
||||
var _ stack.LinkEndpoint = (*wireEndpoint)(nil)
|
||||
|
||||
type wireEndpoint stackDevice
|
||||
|
||||
func (ep *wireEndpoint) MTU() uint32 {
|
||||
return ep.mtu
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) SetMTU(mtu uint32) {
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) MaxHeaderLength() uint16 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) LinkAddress() tcpip.LinkAddress {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) SetLinkAddress(addr tcpip.LinkAddress) {
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) Capabilities() stack.LinkEndpointCapabilities {
|
||||
return stack.CapabilityRXChecksumOffload
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) Attach(dispatcher stack.NetworkDispatcher) {
|
||||
ep.dispatcher = dispatcher
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) IsAttached() bool {
|
||||
return ep.dispatcher != nil
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) Wait() {
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) ARPHardwareType() header.ARPHardwareType {
|
||||
return header.ARPHardwareNone
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) AddHeader(buffer *stack.PacketBuffer) {
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) ParseHeader(ptr *stack.PacketBuffer) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) WritePackets(list stack.PacketBufferList) (int, tcpip.Error) {
|
||||
for _, packetBuffer := range list.AsSlice() {
|
||||
packetBuffer.IncRef()
|
||||
select {
|
||||
case <-ep.done:
|
||||
return 0, &tcpip.ErrClosedForSend{}
|
||||
case ep.outbound <- packetBuffer:
|
||||
}
|
||||
}
|
||||
return list.Len(), nil
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) Close() {
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) SetOnCloseAction(f func()) {
|
||||
}
|
||||
166
transport/masque/masque.go
Normal file
166
transport/masque/masque.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package masque
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
connectip "github.com/Diniboy1123/connect-ip-go"
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
qtls "github.com/sagernet/sing-quic"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
"github.com/yosida95/uritemplate/v3"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
type (
|
||||
DialContext func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
ListenPacket func(network string, address string) (net.PacketConn, error)
|
||||
)
|
||||
|
||||
func ConnectTunnel(ctx context.Context, dialer N.Dialer, tlsConfig aTLS.Config, quicConfig *quic.Config, connectUri string, endpoint net.Addr, useHTTP2 bool) (net.PacketConn, *http3.Transport, *connectip.Conn, *http.Response, error) {
|
||||
template := uritemplate.MustNew(connectUri)
|
||||
additionalHeaders := http.Header{
|
||||
"User-Agent": []string{""},
|
||||
}
|
||||
if useHTTP2 {
|
||||
h2Endpoint, ok := endpoint.(*net.TCPAddr)
|
||||
if !ok || h2Endpoint == nil {
|
||||
return nil, nil, nil, nil, errors.New("missing HTTP/2 TCP endpoint")
|
||||
}
|
||||
h2Headers := additionalHeaders.Clone()
|
||||
h2Headers.Set("cf-connect-proto", "cf-connect-ip")
|
||||
h2Headers.Set("pq-enabled", "false")
|
||||
h2Client, err := newHTTP2Client(dialer, tlsConfig, h2Endpoint, connectUri)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("failed to create HTTP/2 client: %w", err)
|
||||
}
|
||||
ipConn, rsp, err := connectip.DialH2(ctx, h2Client, template, h2Headers)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "tls: access denied") {
|
||||
return nil, nil, nil, nil, errors.New("login failed! Please double-check if your tls key and cert is enrolled in the Cloudflare Access service")
|
||||
}
|
||||
return nil, nil, nil, nil, fmt.Errorf("failed to dial connect-ip over HTTP/2: %w", err)
|
||||
}
|
||||
return nil, nil, ipConn, rsp, nil
|
||||
}
|
||||
quicEndpoint, ok := endpoint.(*net.UDPAddr)
|
||||
if !ok || quicEndpoint == nil {
|
||||
return nil, nil, nil, nil, errors.New("missing HTTP/3 UDP endpoint")
|
||||
}
|
||||
udpConn, err := dialer.ListenPacket(ctx, M.SocksaddrFromNetIP(quicEndpoint.AddrPort()))
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
conn, err := qtls.Dial(
|
||||
ctx,
|
||||
udpConn,
|
||||
quicEndpoint,
|
||||
tlsConfig,
|
||||
quicConfig,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
tr := &http3.Transport{
|
||||
EnableDatagrams: true,
|
||||
AdditionalSettings: map[uint64]uint64{
|
||||
// official client still sends this out as well, even though
|
||||
// it's deprecated, see https://datatracker.ietf.org/doc/draft-ietf-masque-h3-datagram/00/
|
||||
// SETTINGS_H3_DATAGRAM_00 = 0x0000000000000276
|
||||
// https://github.com/cloudflare/quiche/blob/7c66757dbc55b8d0c3653d4b345c6785a181f0b7/quiche/src/h3/frame.rs#L46
|
||||
0x276: 1,
|
||||
},
|
||||
DisableCompression: true,
|
||||
}
|
||||
hconn := tr.NewClientConn(conn)
|
||||
ipConn, rsp, err := connectip.Dial(ctx, hconn, template, "cf-connect-ip", additionalHeaders, true)
|
||||
if err != nil {
|
||||
if err.Error() == "CRYPTO_ERROR 0x131 (remote): tls: access denied" {
|
||||
return udpConn, nil, nil, nil, errors.New("login failed! Please double-check if your tls key and cert is enrolled in the Cloudflare Access service")
|
||||
}
|
||||
return udpConn, nil, nil, nil, fmt.Errorf("failed to dial connect-ip: %w", err)
|
||||
}
|
||||
err = ipConn.AdvertiseRoute(ctx, []connectip.IPRoute{
|
||||
{
|
||||
IPProtocol: 0,
|
||||
StartIP: netip.AddrFrom4([4]byte{}),
|
||||
EndIP: netip.AddrFrom4([4]byte{255, 255, 255, 255}),
|
||||
},
|
||||
{
|
||||
IPProtocol: 0,
|
||||
StartIP: netip.AddrFrom16([16]byte{}),
|
||||
EndIP: netip.AddrFrom16([16]byte{
|
||||
255, 255, 255, 255,
|
||||
255, 255, 255, 255,
|
||||
255, 255, 255, 255,
|
||||
255, 255, 255, 255,
|
||||
}),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return udpConn, nil, nil, nil, err
|
||||
}
|
||||
return udpConn, tr, ipConn, rsp, nil
|
||||
}
|
||||
|
||||
func newHTTP2Client(dialer N.Dialer, baseTLSConfig aTLS.Config, endpoint *net.TCPAddr, connectURI string) (*http.Client, error) {
|
||||
if endpoint == nil {
|
||||
return nil, errors.New("missing HTTP/2 endpoint")
|
||||
}
|
||||
tlsConfig := baseTLSConfig.Clone()
|
||||
tlsConfig.SetNextProtos([]string{"h2"})
|
||||
return &http.Client{
|
||||
Transport: &http2.Transport{
|
||||
DialTLSContext: func(ctx context.Context, network, _ string, _ *tls.Config) (net.Conn, error) {
|
||||
conn, err := dialer.DialContext(ctx, network, M.SocksaddrFromNetIP(endpoint.AddrPort()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConn, err := tlsConfig.Client(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return tlsConn, nil
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func authorityWithDefaultPort(u *url.URL, defaultPort string) string {
|
||||
if u == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
host := u.Hostname()
|
||||
if host == "" {
|
||||
return u.Host
|
||||
}
|
||||
|
||||
port := u.Port()
|
||||
if port == "" {
|
||||
port = defaultPort
|
||||
}
|
||||
|
||||
return net.JoinHostPort(host, port)
|
||||
}
|
||||
|
||||
func proxyDefaultPort(u *url.URL) string {
|
||||
if u != nil && u.Scheme == "https" {
|
||||
return "443"
|
||||
}
|
||||
return "80"
|
||||
}
|
||||
24
transport/masque/options.go
Normal file
24
transport/masque/options.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package masque
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
tun "github.com/sagernet/sing-tun"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/tls"
|
||||
)
|
||||
|
||||
type TunnelOptions struct {
|
||||
Handler tun.Handler
|
||||
Dialer N.Dialer
|
||||
Address []netip.Prefix
|
||||
Endpoint net.Addr
|
||||
TLSConfig tls.Config
|
||||
UseHTTP2 bool
|
||||
UDPTimeout time.Duration
|
||||
UDPKeepalivePeriod time.Duration
|
||||
UDPInitialPacketSize uint16
|
||||
ReconnectDelay time.Duration
|
||||
}
|
||||
200
transport/masque/tunnel.go
Normal file
200
transport/masque/tunnel.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package masque
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
connectip "github.com/Diniboy1123/connect-ip-go"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
)
|
||||
|
||||
type TunnelDevice interface {
|
||||
ReadPacket(buf []byte) (int, error)
|
||||
WritePacket(pkt []byte) error
|
||||
}
|
||||
|
||||
type Tunnel struct {
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
options TunnelOptions
|
||||
tunDevice Device
|
||||
tunnelDevice TunnelDevice
|
||||
}
|
||||
|
||||
func NewTunnel(ctx context.Context, logger logger.ContextLogger, options TunnelOptions) (*Tunnel, error) {
|
||||
deviceOptions := DeviceOptions{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
Handler: options.Handler,
|
||||
UDPTimeout: options.UDPTimeout,
|
||||
MTU: 1280,
|
||||
Address: options.Address,
|
||||
}
|
||||
tunDevice, err := NewDevice(deviceOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create MASQUE device")
|
||||
}
|
||||
return &Tunnel{
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
options: options,
|
||||
tunDevice: tunDevice,
|
||||
tunnelDevice: NewNetstackAdapter(tunDevice),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *Tunnel) Start(resolve bool) error {
|
||||
if resolve {
|
||||
err := e.tunDevice.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go e.MaintainTunnel()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Tunnel) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
if !destination.Addr.IsValid() {
|
||||
return nil, E.Cause(os.ErrInvalid, "invalid non-IP destination")
|
||||
}
|
||||
return e.tunDevice.DialContext(ctx, network, destination)
|
||||
}
|
||||
|
||||
func (e *Tunnel) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
if !destination.Addr.IsValid() {
|
||||
return nil, E.Cause(os.ErrInvalid, "invalid non-IP destination")
|
||||
}
|
||||
return e.tunDevice.ListenPacket(ctx, destination)
|
||||
}
|
||||
|
||||
func (e *Tunnel) Close() error {
|
||||
return e.tunDevice.Close()
|
||||
}
|
||||
|
||||
func (e *Tunnel) MaintainTunnel() {
|
||||
packetBufferPool := NewNetBuffer(1280)
|
||||
timer := time.NewTimer(0)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-e.ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
e.logger.InfoContext(e.ctx, fmt.Errorf("Establishing MASQUE connection to %s", e.options.Endpoint))
|
||||
udpConn, tr, ipConn, rsp, err := ConnectTunnel(
|
||||
e.ctx,
|
||||
e.options.Dialer,
|
||||
e.options.TLSConfig,
|
||||
DefaultQuicConfig(e.options.UDPKeepalivePeriod, e.options.UDPInitialPacketSize),
|
||||
"https://cloudflareaccess.com",
|
||||
e.options.Endpoint,
|
||||
e.options.UseHTTP2,
|
||||
)
|
||||
if err != nil {
|
||||
e.logger.InfoContext(e.ctx, fmt.Errorf("Failed to connect tunnel: %v", err))
|
||||
timer.Reset(e.options.ReconnectDelay)
|
||||
select {
|
||||
case <-e.ctx.Done():
|
||||
return
|
||||
case <-timer.C:
|
||||
}
|
||||
continue
|
||||
}
|
||||
if rsp.StatusCode != 200 {
|
||||
e.logger.InfoContext(e.ctx, fmt.Errorf("Tunnel connection failed: %s", rsp.Status))
|
||||
ipConn.Close()
|
||||
if udpConn != nil {
|
||||
udpConn.Close()
|
||||
}
|
||||
if tr != nil {
|
||||
tr.Close()
|
||||
}
|
||||
timer.Reset(e.options.ReconnectDelay)
|
||||
select {
|
||||
case <-e.ctx.Done():
|
||||
return
|
||||
case <-timer.C:
|
||||
}
|
||||
continue
|
||||
}
|
||||
e.logger.InfoContext(e.ctx, "Connected to MASQUE server")
|
||||
errChan := make(chan error, 2)
|
||||
go func() {
|
||||
for {
|
||||
buf := packetBufferPool.Get()
|
||||
n, err := e.tunnelDevice.ReadPacket(buf)
|
||||
if err != nil {
|
||||
packetBufferPool.Put(buf)
|
||||
errChan <- fmt.Errorf("failed to read from TUN device: %w", err)
|
||||
return
|
||||
}
|
||||
icmp, err := ipConn.WritePacket(buf[:n])
|
||||
if err != nil {
|
||||
packetBufferPool.Put(buf)
|
||||
if errors.As(err, new(*connectip.CloseError)) {
|
||||
errChan <- fmt.Errorf("connection closed while writing to IP connection: %w", err)
|
||||
return
|
||||
}
|
||||
e.logger.InfoContext(e.ctx, fmt.Errorf("Error writing to IP connection: %v, continuing...", err))
|
||||
continue
|
||||
}
|
||||
packetBufferPool.Put(buf)
|
||||
if len(icmp) > 0 {
|
||||
if err := e.tunnelDevice.WritePacket(icmp); err != nil {
|
||||
if errors.As(err, new(*connectip.CloseError)) {
|
||||
errChan <- fmt.Errorf("connection closed while writing ICMP to TUN device: %w", err)
|
||||
return
|
||||
}
|
||||
e.logger.InfoContext(e.ctx, fmt.Errorf("Error writing ICMP to TUN device: %v, continuing...", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
buf := packetBufferPool.Get()
|
||||
defer packetBufferPool.Put(buf)
|
||||
for {
|
||||
n, err := ipConn.ReadPacket(buf, true)
|
||||
if err != nil {
|
||||
if e.options.UseHTTP2 {
|
||||
errChan <- fmt.Errorf("connection closed while reading from IP connection: %w", err)
|
||||
return
|
||||
}
|
||||
if errors.As(err, new(*connectip.CloseError)) {
|
||||
errChan <- fmt.Errorf("connection closed while reading from IP connection: %w", err)
|
||||
return
|
||||
}
|
||||
e.logger.InfoContext(e.ctx, fmt.Errorf("Error reading from IP connection: %v, continuing...", err))
|
||||
continue
|
||||
}
|
||||
if err := e.tunnelDevice.WritePacket(buf[:n]); err != nil {
|
||||
errChan <- fmt.Errorf("failed to write to TUN device: %w", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
err = <-errChan
|
||||
e.logger.InfoContext(e.ctx, fmt.Errorf("Tunnel connection lost: %v. Reconnecting...", err))
|
||||
ipConn.Close()
|
||||
if udpConn != nil {
|
||||
udpConn.Close()
|
||||
}
|
||||
if tr != nil {
|
||||
tr.Close()
|
||||
}
|
||||
timer.Reset(e.options.ReconnectDelay)
|
||||
select {
|
||||
case <-e.ctx.Done():
|
||||
return
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
326
transport/masque/utils.go
Normal file
326
transport/masque/utils.go
Normal file
@@ -0,0 +1,326 @@
|
||||
package masque
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"log"
|
||||
"math/big"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
)
|
||||
|
||||
// PortMapping represents a network port forwarding rule.
|
||||
type PortMapping struct {
|
||||
BindAddress string // The address to bind the local port.
|
||||
LocalPort int // The local port number.
|
||||
RemoteIP string // The remote destination IP address.
|
||||
RemotePort int // The remote destination port number.
|
||||
}
|
||||
|
||||
// GenerateRandomAndroidSerial generates a random 8-byte Android-like device identifier
|
||||
// and returns it as a hexadecimal string.
|
||||
//
|
||||
// Returns:
|
||||
// - string: A randomly generated 16-character hexadecimal serial number.
|
||||
// - error: An error if random data generation fails.
|
||||
func GenerateRandomAndroidSerial() (string, error) {
|
||||
serial := make([]byte, 8)
|
||||
if _, err := rand.Read(serial); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(serial), nil
|
||||
}
|
||||
|
||||
// GenerateRandomWgPubkey generates a random 32-byte WireGuard like public key
|
||||
// and returns it as a base64-encoded string.
|
||||
//
|
||||
// Returns:
|
||||
// - string: A randomly generated WireGuard like public key in base64 format.
|
||||
// - error: An error if random data generation fails.
|
||||
func GenerateRandomWgPubkey() (string, error) {
|
||||
publicKey := make([]byte, 32)
|
||||
if _, err := rand.Read(publicKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(publicKey), nil
|
||||
}
|
||||
|
||||
// TimeAsCfString formats a given time.Time into a Cloudflare-compatible string format.
|
||||
//
|
||||
// The format follows the standard: "YYYY-MM-DDTHH:MM:SS.sss-07:00".
|
||||
//
|
||||
// Parameters:
|
||||
// - t: time.Time to format.
|
||||
//
|
||||
// Returns:
|
||||
// - string: The formatted time string.
|
||||
func TimeAsCfString(t time.Time) string {
|
||||
return t.Format("2006-01-02T15:04:05.000-07:00")
|
||||
}
|
||||
|
||||
// GenerateEcKeyPair generates a new ECDSA key pair using the P-256 curve.
|
||||
//
|
||||
// Returns:
|
||||
// - []byte: The marshalled private key in ASN.1 DER format.
|
||||
// - []byte: The marshalled public key in PKIX format.
|
||||
// - error: An error if key generation or marshalling fails.
|
||||
func GenerateEcKeyPair() ([]byte, []byte, error) {
|
||||
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
marshalledPrivKey, err := x509.MarshalECPrivateKey(privKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
marshalledPubKey, err := x509.MarshalPKIXPublicKey(&privKey.PublicKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return marshalledPrivKey, marshalledPubKey, nil
|
||||
}
|
||||
|
||||
// GenerateCert creates a self-signed certificate using the provided ECDSA private and public keys.
|
||||
//
|
||||
// The certificate is valid for 24 hours.
|
||||
//
|
||||
// Parameters:
|
||||
// - privKey: *ecdsa.PrivateKey - The private key to sign the certificate.
|
||||
// - pubKey: *ecdsa.PublicKey - The public key to include in the certificate.
|
||||
//
|
||||
// Returns:
|
||||
// - [][]byte: A slice containing the certificate in DER format.
|
||||
// - error: An error if certificate generation fails.
|
||||
func GenerateCert(privKey *ecdsa.PrivateKey, pubKey *ecdsa.PublicKey) ([][]byte, error) {
|
||||
cert, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{
|
||||
SerialNumber: big.NewInt(0),
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(1 * 24 * time.Hour),
|
||||
}, &x509.Certificate{}, &privKey.PublicKey, privKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return [][]byte{cert}, nil
|
||||
}
|
||||
|
||||
// DefaultQuicConfig returns a MASQUE-compatible default QUIC configuration.
|
||||
//
|
||||
// When initialPacketSize is 0, Path MTU Discovery remains enabled.
|
||||
//
|
||||
// Parameters:
|
||||
// - keepalivePeriod: time.Duration - The duration for sending QUIC keep-alive packets.
|
||||
// - initialPacketSize: uint16 - The custom initial size of QUIC packets (0 = auto with PMTU discovery).
|
||||
//
|
||||
// Returns:
|
||||
// - *quic.Config: A pointer to a configured QUIC configuration object.
|
||||
func DefaultQuicConfig(keepalivePeriod time.Duration, initialPacketSize uint16) *quic.Config {
|
||||
cfg := &quic.Config{
|
||||
EnableDatagrams: true,
|
||||
KeepAlivePeriod: keepalivePeriod,
|
||||
}
|
||||
|
||||
if initialPacketSize > 0 {
|
||||
cfg.InitialPacketSize = initialPacketSize
|
||||
cfg.DisablePathMTUDiscovery = true
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
// parsePortMapping is an internal helper function that parses a port mapping string into its components.
|
||||
//
|
||||
// It handles IPv6 addresses enclosed in brackets and various format edge cases.
|
||||
//
|
||||
// Parameters:
|
||||
// - port: string - The port mapping string.
|
||||
//
|
||||
// Returns:
|
||||
// - string: The bind address.
|
||||
// - int: The local port.
|
||||
// - string: The remote hostname/IP.
|
||||
// - int: The remote port.
|
||||
// - error: An error if parsing fails.
|
||||
func parsePortMapping(port string) (bindAddress string, localPort int, remoteHost string, remotePort int, err error) {
|
||||
parts := strings.Split(port, ":")
|
||||
|
||||
// Handle IPv6 addresses (which are enclosed in brackets)
|
||||
if len(parts) >= 4 && strings.HasPrefix(parts[0], "[") && strings.Contains(parts[0], "]") {
|
||||
bindAddress = parts[0]
|
||||
parts = parts[1:] // Shift parts forward
|
||||
} else if len(parts) == 3 {
|
||||
bindAddress = "localhost" // Default to localhost
|
||||
} else if len(parts) == 4 {
|
||||
bindAddress = parts[0]
|
||||
parts = parts[1:] // Shift forward
|
||||
} else {
|
||||
return "", 0, "", 0, errors.New("invalid port mapping format (expected format: [bind_address:]local_port:remote_host:remote_port)")
|
||||
}
|
||||
|
||||
// Parse local port
|
||||
localPort, err = strconv.Atoi(parts[0])
|
||||
if err != nil || localPort <= 0 || localPort > 65535 {
|
||||
return "", 0, "", 0, errors.New("invalid local port")
|
||||
}
|
||||
|
||||
// Validate remote host (allow both hostnames and IPs)
|
||||
remoteHost = parts[1]
|
||||
if net.ParseIP(remoteHost) == nil && !isValidHostname(remoteHost) {
|
||||
return "", 0, "", 0, errors.New("invalid remote hostname/IP")
|
||||
}
|
||||
|
||||
// Parse remote port
|
||||
remotePort, err = strconv.Atoi(parts[2])
|
||||
if err != nil || remotePort <= 0 || remotePort > 65535 {
|
||||
return "", 0, "", 0, errors.New("invalid remote port")
|
||||
}
|
||||
|
||||
// If bindAddress is an IPv6 address, remove brackets for proper binding
|
||||
if strings.HasPrefix(bindAddress, "[") && strings.HasSuffix(bindAddress, "]") {
|
||||
bindAddress = strings.Trim(bindAddress, "[]")
|
||||
}
|
||||
|
||||
// Convert "localhost" or hostnames to actual addresses
|
||||
if bindAddress == "*" {
|
||||
bindAddress = "0.0.0.0" // Allow all interfaces
|
||||
}
|
||||
|
||||
// Validate bind address (support both IPs and hostnames)
|
||||
bindAddress, err = resolveBindAddress(bindAddress)
|
||||
if err != nil {
|
||||
return "", 0, "", 0, errors.New("invalid local address: " + err.Error())
|
||||
}
|
||||
|
||||
remoteHost, err = resolveBindAddress(remoteHost)
|
||||
if err != nil {
|
||||
return "", 0, "", 0, errors.New("invalid remote address: " + err.Error())
|
||||
}
|
||||
|
||||
return bindAddress, localPort, remoteHost, remotePort, nil
|
||||
}
|
||||
|
||||
// ParsePortMapping parses a port mapping string into a structured PortMapping.
|
||||
//
|
||||
// The expected format is: `[bind_address:]local_port:remote_host:remote_port`.
|
||||
//
|
||||
// Parameters:
|
||||
// - port: string - The port mapping string.
|
||||
//
|
||||
// Returns:
|
||||
// - PortMapping: A structured representation of the parsed port mapping.
|
||||
// - error: An error if the parsing fails.
|
||||
func ParsePortMapping(port string) (PortMapping, error) {
|
||||
bindAddress, localPort, remoteHost, remotePort, err := parsePortMapping(port)
|
||||
if err != nil {
|
||||
return PortMapping{}, err
|
||||
}
|
||||
|
||||
return PortMapping{
|
||||
BindAddress: bindAddress,
|
||||
LocalPort: localPort,
|
||||
RemoteIP: remoteHost,
|
||||
RemotePort: remotePort,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// resolveBindAddress resolves a hostname or IP to its string representation.
|
||||
//
|
||||
// Parameters:
|
||||
// - addr: string - The hostname or IP.
|
||||
//
|
||||
// Returns:
|
||||
// - string: The resolved IP address.
|
||||
// - error: An error if resolution fails.
|
||||
func resolveBindAddress(addr string) (string, error) {
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", addr+":0") // Resolve the address
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return tcpAddr.IP.String(), nil // Return resolved IP
|
||||
}
|
||||
|
||||
// isValidHostname checks if a given hostname is valid.
|
||||
// Pretty ugly for now, needs to be refactored.
|
||||
//
|
||||
// Parameters:
|
||||
// - hostname: string - The hostname to validate.
|
||||
//
|
||||
// Returns:
|
||||
// - bool: True if valid, false otherwise.
|
||||
func isValidHostname(hostname string) bool {
|
||||
// Must contain at least one dot (.) unless it's "localhost"
|
||||
if hostname == "localhost" {
|
||||
return true
|
||||
}
|
||||
return strings.Contains(hostname, ".")
|
||||
}
|
||||
|
||||
// LoginToBase64 encodes a username and password into a base64-encoded string in "username:password" format.
|
||||
// This is commonly used for HTTP Basic Authentication.
|
||||
//
|
||||
// Parameters:
|
||||
// - username: string - The username to encode.
|
||||
// - password: string - The password to encode.
|
||||
//
|
||||
// Returns:
|
||||
// - string: The base64-encoded "username:password" string.
|
||||
func LoginToBase64(username, password string) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
|
||||
}
|
||||
|
||||
// CheckIfname validates a network interface name according to the following rules:
|
||||
// - Must not be empty.
|
||||
// - Should not exceed 15 characters (warning if it does).
|
||||
// - Should not contain non-ASCII characters (warning if it does).
|
||||
// - Should not contain invalid characters: '/', whitespace, or control characters.
|
||||
//
|
||||
// Parameters:
|
||||
// - name: string - The interface name to validate.
|
||||
//
|
||||
// Returns:
|
||||
// - error: An error if the name is invalid, or nil if valid.
|
||||
func CheckIfname(name string) error {
|
||||
if name == "" {
|
||||
return errors.New("interface name cannot be empty")
|
||||
}
|
||||
|
||||
if len(name) >= 16 {
|
||||
log.Printf("Warning: interface name '%s' is longer than %d characters", name, 16-1)
|
||||
}
|
||||
|
||||
var invalidChar bool
|
||||
var hasWhitespace bool
|
||||
|
||||
for _, r := range name {
|
||||
if r > 127 {
|
||||
invalidChar = true
|
||||
break
|
||||
}
|
||||
if r == '/' || r == ' ' || strings.ContainsRune("\t\n\v\f\r", r) {
|
||||
hasWhitespace = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if invalidChar {
|
||||
log.Printf("Warning: interface name contains non-ASCII character")
|
||||
}
|
||||
|
||||
if hasWhitespace {
|
||||
return errors.New("interface name contains invalid character: '/' or whitespace")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user