mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-01 21:45:44 +00:00
Merge commit 'cd4ce973e9f6ef3a7acf9a7030927b4143f9ea47' into finalrules
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
//go:build openbsd
|
||||
// +build openbsd
|
||||
|
||||
package dokodemo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func FakeUDP(addr *net.UDPAddr, mark int) (net.PacketConn, error) {
|
||||
domain := unix.AF_INET6
|
||||
var sockaddr unix.Sockaddr
|
||||
if ip4 := addr.IP.To4(); ip4 != nil {
|
||||
domain = unix.AF_INET
|
||||
sa := &unix.SockaddrInet4{Port: addr.Port}
|
||||
copy(sa.Addr[:], ip4)
|
||||
sockaddr = sa
|
||||
} else if ip6 := addr.IP.To16(); ip6 != nil {
|
||||
sa := &unix.SockaddrInet6{Port: addr.Port}
|
||||
copy(sa.Addr[:], ip6)
|
||||
if addr.Zone != "" {
|
||||
iface, err := net.InterfaceByName(addr.Zone)
|
||||
if err != nil {
|
||||
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("resolve zone %s: %w", addr.Zone, err)}
|
||||
}
|
||||
sa.ZoneId = uint32(iface.Index)
|
||||
}
|
||||
sockaddr = sa
|
||||
} else {
|
||||
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("unsupported address %v", addr.IP)}
|
||||
}
|
||||
|
||||
fd, err := unix.Socket(domain, unix.SOCK_DGRAM, 0)
|
||||
if err != nil {
|
||||
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("socket open: %w", err)}
|
||||
}
|
||||
|
||||
closeFD := true
|
||||
defer func() {
|
||||
if closeFD {
|
||||
unix.Close(fd)
|
||||
}
|
||||
}()
|
||||
|
||||
if err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_BINDANY, 1); err != nil {
|
||||
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("set socket option SO_BINDANY: %w", err)}
|
||||
}
|
||||
if err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEADDR, 1); err != nil {
|
||||
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("set socket option SO_REUSEADDR: %w", err)}
|
||||
}
|
||||
// Several client sessions can be answered from the same original
|
||||
// destination at the same time, so the address has to be shareable.
|
||||
if err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
|
||||
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("set socket option SO_REUSEPORT: %w", err)}
|
||||
}
|
||||
|
||||
if err = unix.Bind(fd, sockaddr); err != nil {
|
||||
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("bind %s: %w", addr.String(), err)}
|
||||
}
|
||||
|
||||
fdFile := os.NewFile(uintptr(fd), fmt.Sprintf("net-udp-bindany-%s", addr.String()))
|
||||
if fdFile == nil {
|
||||
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("convert descriptor to file")}
|
||||
}
|
||||
defer fdFile.Close()
|
||||
|
||||
packetConn, err := net.FilePacketConn(fdFile)
|
||||
if err != nil {
|
||||
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("convert descriptor to packet connection: %w", err)}
|
||||
}
|
||||
|
||||
closeFD = false
|
||||
return packetConn, nil
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
//go:build !linux && !openbsd
|
||||
// +build !linux,!openbsd
|
||||
|
||||
package dokodemo
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/pires/go-proxyproto"
|
||||
@@ -31,12 +32,24 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
useSplice bool
|
||||
useSplice atomic.Bool
|
||||
allNetworks [8]bool
|
||||
defaultBlockPrivateRule *FinalRule
|
||||
defaultBlockAllRule *FinalRule
|
||||
)
|
||||
|
||||
func reloadEnvSettings() error {
|
||||
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
||||
value := platform.NewEnvFlag(platform.UseFreedomSplice).GetValue(func() string { return defaultFlagValue })
|
||||
enabled := false
|
||||
switch value {
|
||||
case defaultFlagValue, "auto", "enable":
|
||||
enabled = true
|
||||
}
|
||||
useSplice.Store(enabled)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
|
||||
h := new(Handler)
|
||||
@@ -52,12 +65,7 @@ func init() {
|
||||
return h, nil
|
||||
}))
|
||||
|
||||
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
||||
value := platform.NewEnvFlag(platform.UseFreedomSplice).GetValue(func() string { return defaultFlagValue })
|
||||
switch value {
|
||||
case defaultFlagValue, "auto", "enable":
|
||||
useSplice = true
|
||||
}
|
||||
platform.RegisterEnvReload(reloadEnvSettings)
|
||||
|
||||
for i := range allNetworks {
|
||||
allNetworks[i] = true
|
||||
@@ -412,7 +420,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
|
||||
responseDone := func() error {
|
||||
defer timer.SetTimeout(plcy.Timeouts.UplinkOnly)
|
||||
if destination.Network == net.Network_TCP && useSplice && proxy.IsRAWTransportWithoutSecurity(conn) { // it would be tls conn in special use case of MITM, we need to let link handle traffic
|
||||
if destination.Network == net.Network_TCP && useSplice.Load() && proxy.IsRAWTransportWithoutSecurity(conn) { // it would be tls conn in special use case of MITM, we need to let link handle traffic
|
||||
var writeConn net.Conn
|
||||
var inTimer *signal.ActivityTimer
|
||||
if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Conn != nil {
|
||||
|
||||
@@ -173,15 +173,18 @@ func fillRequestHeader(ctx context.Context, header []*Header) ([]*Header, error)
|
||||
outbounds := session.OutboundsFromContext(ctx)
|
||||
ob := outbounds[len(outbounds)-1]
|
||||
|
||||
if inbound == nil || ob == nil {
|
||||
return nil, errors.New("missing inbound or outbound metadata from context")
|
||||
var src net.Destination
|
||||
if inbound != nil {
|
||||
src = inbound.Source
|
||||
} else {
|
||||
src = net.TCPDestination(net.AnyIP, 0)
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Source net.Destination
|
||||
Target net.Destination
|
||||
}{
|
||||
Source: inbound.Source,
|
||||
Source: src,
|
||||
Target: ob.Target,
|
||||
}
|
||||
|
||||
|
||||
@@ -332,7 +332,7 @@ func readResponseAndHandle100Continue(r *bufio.Reader, req *http.Request, writer
|
||||
return nil, errors.New("failed to read http 1xx response").Base(err)
|
||||
}
|
||||
ResponseHeader1xx = append(ResponseHeader1xx, data...)
|
||||
if bytes.Equal(ResponseHeader1xx[len(ResponseHeader1xx)-4:], []byte{'\r', '\n', '\r', '\n'}) {
|
||||
if len(ResponseHeader1xx) >= 4 && bytes.Equal(ResponseHeader1xx[len(ResponseHeader1xx)-4:], []byte{'\r', '\n', '\r', '\n'}) {
|
||||
break
|
||||
}
|
||||
if len(ResponseHeader1xx) > 1024 {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A malformed upstream response containing a bare '\n' before the real
|
||||
// status line used to crash readResponseAndHandle100Continue: the first
|
||||
// ReadSlice('\n') returns fewer than 4 bytes, and slicing
|
||||
// ResponseHeader1xx[len(ResponseHeader1xx)-4:] panicked with a negative
|
||||
// index instead of returning an error.
|
||||
func TestReadResponseAndHandle100ContinueDoesNotPanicOnEarlyNewline(t *testing.T) {
|
||||
payload := "X\nHTTP/1.1 100 Continue\r\n\r\n" + strings.Repeat("A", 40)
|
||||
r := bufio.NewReader(bytes.NewReader([]byte(payload)))
|
||||
req, err := http.NewRequest("GET", "http://example.com/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Must not panic; a parse error for the garbage trailing bytes is fine.
|
||||
_, _ = readResponseAndHandle100Continue(r, req, io.Discard)
|
||||
}
|
||||
|
||||
func TestReadResponseAndHandle100ContinueForwardsAndParsesFinalResponse(t *testing.T) {
|
||||
payload := "HTTP/1.1 100 Continue\r\n\r\n" +
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"
|
||||
r := bufio.NewReader(bytes.NewReader([]byte(payload)))
|
||||
req, err := http.NewRequest("GET", "http://example.com/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var forwarded bytes.Buffer
|
||||
resp, err := readResponseAndHandle100Continue(r, req, &forwarded)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
if !strings.Contains(forwarded.String(), "100 Continue") {
|
||||
t.Fatalf("expected 1xx response to be forwarded, got %q", forwarded.String())
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,7 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int
|
||||
if err != nil {
|
||||
return errors.New("failed to connect to server").Base(err)
|
||||
}
|
||||
defer connection.Close()
|
||||
|
||||
if session.TimeoutOnlyFromContext(ctx) {
|
||||
ctx, _ = context.WithCancel(context.Background())
|
||||
|
||||
+6
-2
@@ -31,13 +31,17 @@ Here is simple Xray config snippet to enable the inbound:
|
||||
"port": 0,
|
||||
"protocol": "tun",
|
||||
"settings": {
|
||||
"name": "xray0",
|
||||
"MTU": 1492
|
||||
"name": "utun10",
|
||||
"desc": "Wintun",
|
||||
"mtu": 1500
|
||||
}
|
||||
}
|
||||
],
|
||||
```
|
||||
|
||||
`desc` sets the Windows Wintun adapter tunnel type and defaults to `Wintun`.
|
||||
It is ignored on other platforms.
|
||||
|
||||
## SUPPORTED FEATURES
|
||||
|
||||
- IPv4 and IPv6
|
||||
|
||||
+14
-4
@@ -7,11 +7,12 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -30,6 +31,7 @@ type Config struct {
|
||||
UserLevel uint32 `protobuf:"varint,5,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
|
||||
AutoSystemRoutingTable []string `protobuf:"bytes,6,rep,name=auto_system_routing_table,json=autoSystemRoutingTable,proto3" json:"auto_system_routing_table,omitempty"`
|
||||
AutoOutboundsInterface string `protobuf:"bytes,7,opt,name=auto_outbounds_interface,json=autoOutboundsInterface,proto3" json:"auto_outbounds_interface,omitempty"`
|
||||
Desc string `protobuf:"bytes,8,opt,name=desc,proto3" json:"desc,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -113,11 +115,18 @@ func (x *Config) GetAutoOutboundsInterface() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetDesc() string {
|
||||
if x != nil {
|
||||
return x.Desc
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_proxy_tun_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_proxy_tun_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\xee\x01\n" +
|
||||
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\x82\x02\n" +
|
||||
"\x06Config\x12\x12\n" +
|
||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" +
|
||||
"\x03MTU\x18\x02 \x01(\rR\x03MTU\x12\x18\n" +
|
||||
@@ -126,7 +135,8 @@ const file_proxy_tun_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"user_level\x18\x05 \x01(\rR\tuserLevel\x129\n" +
|
||||
"\x19auto_system_routing_table\x18\x06 \x03(\tR\x16autoSystemRoutingTable\x128\n" +
|
||||
"\x18auto_outbounds_interface\x18\a \x01(\tR\x16autoOutboundsInterfaceBL\n" +
|
||||
"\x18auto_outbounds_interface\x18\a \x01(\tR\x16autoOutboundsInterface\x12\x12\n" +
|
||||
"\x04desc\x18\b \x01(\tR\x04descBL\n" +
|
||||
"\x12com.xray.proxy.tunP\x01Z#github.com/xtls/xray-core/proxy/tun\xaa\x02\x0eXray.Proxy.Tunb\x06proto3"
|
||||
|
||||
var (
|
||||
|
||||
@@ -14,4 +14,5 @@ message Config {
|
||||
uint32 user_level = 5;
|
||||
repeated string auto_system_routing_table = 6;
|
||||
string auto_outbounds_interface = 7;
|
||||
string desc = 8;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routin
|
||||
if len(t.tag) > 0 && pm.ForSystem().Stats.InboundUplink {
|
||||
statsManager := core.MustFromContext(ctx).GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "inbound>>>" + t.tag + ">>>traffic>>>uplink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
t.uplinkCounter = c
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routin
|
||||
if len(t.tag) > 0 && pm.ForSystem().Stats.InboundDownlink {
|
||||
statsManager := core.MustFromContext(ctx).GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "inbound>>>" + t.tag + ">>>traffic>>>downlink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
t.downlinkCounter = c
|
||||
}
|
||||
|
||||
+136
-5
@@ -11,6 +11,8 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
@@ -38,21 +40,111 @@ const (
|
||||
ND6_INFINITE_LIFETIME = 0xFFFFFFFF // netinet6/nd6.h
|
||||
)
|
||||
|
||||
//go:linkname procyield runtime.procyield
|
||||
func procyield(cycles uint32)
|
||||
|
||||
type DarwinTun struct {
|
||||
tunFile *os.File
|
||||
options *Config
|
||||
tunFd int
|
||||
ownsFd bool // true for macOS (we created the fd), false for iOS (fd from system)
|
||||
|
||||
// Genuinely blocks Wait() until tunFd is readable, instead of the
|
||||
// previous procyield-only busy-spin (dispatchLoop in
|
||||
// stack_gvisor_endpoint.go calls ReadPacket() then Wait() in a tight
|
||||
// loop with no other throttling whenever the queue is empty -- with
|
||||
// only procyield(1), that pins a full CPU core for as long as the
|
||||
// tunnel is up, observed causing severe device heating/thermal
|
||||
// shutdown). nil if kqueue setup failed, in which case Wait() falls
|
||||
// back to a bounded time.Sleep instead. See waitKqueue's own doc
|
||||
// comment for why this is a dedicated type rather than a bare fd.
|
||||
waitKq *waitKqueue
|
||||
|
||||
routeMonitor *os.File
|
||||
routeMonitorOnce sync.Once
|
||||
systemRoutes []netip.Prefix
|
||||
gateway netip.Prefix
|
||||
}
|
||||
|
||||
// waitKqueue owns a kqueue fd used by DarwinTun.Wait() to block on
|
||||
// read-readiness. Closing and waiting can race from different goroutines
|
||||
// (Close() from the caller that tears down the tunnel, Wait() from
|
||||
// dispatchLoop's own goroutine) -- reviewer feedback on XTLS/Xray-core#6580
|
||||
// found that a bare `int` fd field let Close() race Wait()'s use of the
|
||||
// same fd number, and on Darwin a closed fd number can be reused by an
|
||||
// unrelated concurrent open() before Wait() gets to call Kevent on it,
|
||||
// so Wait() could end up polling (or Close() could end up closing) a
|
||||
// completely unrelated file descriptor. This type makes closing
|
||||
// idempotent (sync.Once) and gates every Kevent call behind an atomic
|
||||
// "closed" flag checked immediately before the syscall, so Wait() never
|
||||
// issues a kevent syscall against a fd number that Close() has already
|
||||
// (or is concurrently) invalidated -- there's still a narrow window where
|
||||
// Wait() checks-then-uses the fd, but Close() only actually closes it
|
||||
// after Wait() cannot start a new syscall on it (the flag is set first,
|
||||
// synchronized with acquire/release semantics), which is sufficient since
|
||||
// Wait()'s Kevent call itself is what's being raced, not a fd read/write.
|
||||
type waitKqueue struct {
|
||||
fd int
|
||||
closed atomic.Bool
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// newWaitKqueue creates a kqueue registered for read-readiness on fd, for
|
||||
// Wait() to block on. Returns nil if anything fails, so callers can fall
|
||||
// back to a bounded sleep rather than error out of NewTun over what is
|
||||
// purely a CPU-efficiency concern.
|
||||
func newWaitKqueue(fd int) *waitKqueue {
|
||||
kq, err := unix.Kqueue()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
_, err = unix.Kevent(kq, []unix.Kevent_t{{
|
||||
Ident: uint64(fd),
|
||||
Filter: unix.EVFILT_READ,
|
||||
Flags: unix.EV_ADD | unix.EV_ENABLE,
|
||||
}}, nil, nil)
|
||||
if err != nil {
|
||||
_ = unix.Close(kq)
|
||||
return nil
|
||||
}
|
||||
return &waitKqueue{fd: kq}
|
||||
}
|
||||
|
||||
// wait blocks until the registered fd is readable, timeout elapses, or a
|
||||
// benign interrupt occurs -- all three are "this kqueue is still healthy,
|
||||
// the caller should just try again" and return true; the caller
|
||||
// (DarwinTun.Wait) doesn't need to distinguish them since it always calls
|
||||
// ReadPacket() right after anyway, and that already handles "nothing was
|
||||
// actually there" via ErrQueueEmpty. Returns false only when the kqueue
|
||||
// itself is no longer usable -- already closed, or the kevent syscall
|
||||
// failed for a reason other than EINTR -- see its own call site in
|
||||
// DarwinTun.Wait for why a persistent failure must not be silently
|
||||
// retried forever (reviewer feedback, XTLS/Xray-core#6580 P2).
|
||||
func (w *waitKqueue) wait(timeout time.Duration) (ok bool) {
|
||||
if w.closed.Load() {
|
||||
return false
|
||||
}
|
||||
events := make([]unix.Kevent_t, 1)
|
||||
ts := unix.NsecToTimespec(timeout.Nanoseconds())
|
||||
_, err := unix.Kevent(w.fd, nil, events, &ts)
|
||||
if err != nil {
|
||||
return errors.Is(err, unix.EINTR)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// close marks the kqueue as unusable (so any Wait() call that hasn't yet
|
||||
// entered the kevent syscall bails out instead) and closes the underlying
|
||||
// fd exactly once, regardless of how many times close is called or
|
||||
// whether it races a Wait() already inside its kevent syscall (that call
|
||||
// either completes against the still-open fd or returns an error safely
|
||||
// -- either way, no other goroutine can be handed this fd number in
|
||||
// between the atomic flag flip and the actual close, since nothing else
|
||||
// in this type ever creates a new kqueue with the same field).
|
||||
func (w *waitKqueue) close() {
|
||||
w.once.Do(func() {
|
||||
w.closed.Store(true)
|
||||
_ = unix.Close(w.fd)
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
_ Tun = (*DarwinTun)(nil)
|
||||
_ GVisorDevice = (*DarwinTun)(nil)
|
||||
@@ -77,6 +169,7 @@ func NewTun(options *Config) (Tun, error) {
|
||||
options: options,
|
||||
tunFd: fd,
|
||||
ownsFd: false,
|
||||
waitKq: newWaitKqueue(fd),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -103,6 +196,7 @@ func NewTun(options *Config) (Tun, error) {
|
||||
options: options,
|
||||
tunFd: int(tunFile.Fd()),
|
||||
ownsFd: true,
|
||||
waitKq: newWaitKqueue(int(tunFile.Fd())),
|
||||
gateway: gateway,
|
||||
}, nil
|
||||
}
|
||||
@@ -134,6 +228,9 @@ func (t *DarwinTun) Close() error {
|
||||
_ = t.routeMonitor.Close()
|
||||
}
|
||||
})
|
||||
if t.waitKq != nil {
|
||||
t.waitKq.close()
|
||||
}
|
||||
routeErr := t.unsetSystemRoutes()
|
||||
if t.ownsFd {
|
||||
return xerrors.Combine(routeErr, t.tunFile.Close())
|
||||
@@ -242,9 +339,43 @@ func (t *DarwinTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
|
||||
}), nil
|
||||
}
|
||||
|
||||
// Wait some cpu cycles
|
||||
// Wait blocks until tunFd is readable (or a short timeout elapses), rather
|
||||
// than spinning the CPU -- see the waitKq field's own doc comment. A bounded
|
||||
// timeout (not an indefinite wait) keeps this responsive to a Close() that
|
||||
// happens to race a call already parked here.
|
||||
//
|
||||
// Reviewer feedback (XTLS/Xray-core#6580, P2): the original version
|
||||
// discarded every error from the underlying kevent syscall. dispatchLoop
|
||||
// (stack_gvisor_endpoint.go) calls ReadPacket() then Wait() in an
|
||||
// unconditional tight loop -- if kevent started failing at runtime for a
|
||||
// persistent reason (not just a benign EINTR), Wait() returning
|
||||
// immediately every time reintroduces exactly the busy-spin this whole
|
||||
// change exists to remove, just routed through a failing syscall instead
|
||||
// of procyield. waitKq.wait's own bool return distinguishes "genuinely
|
||||
// interrupted, try again" from "this kqueue is unusable now" -- Wait()
|
||||
// permanently falls back to the sleep path once that happens, rather than
|
||||
// retrying the same broken kqueue forever.
|
||||
func (t *DarwinTun) Wait() {
|
||||
procyield(1)
|
||||
if t.waitKq != nil && t.waitKq.wait(time.Second) {
|
||||
return
|
||||
}
|
||||
if t.waitKq != nil {
|
||||
// Persistent kevent failure (not a benign EINTR, and not just
|
||||
// "the 1s timeout elapsed with nothing to read" -- wait() already
|
||||
// returned true for both of those cases above). Stop trusting
|
||||
// this kqueue for the rest of this DarwinTun's lifetime instead of
|
||||
// re-attempting a syscall that's already shown it won't succeed.
|
||||
t.waitKq.close()
|
||||
t.waitKq = nil
|
||||
}
|
||||
// Reviewer feedback (XTLS/Xray-core#6580): procyield here is the same
|
||||
// busy-spin this whole change exists to remove, just gated behind an
|
||||
// edge case (kqueue setup failing, which practically never happens on
|
||||
// real Darwin systems, or having just failed permanently above)
|
||||
// instead of always -- a genuine bounded sleep actually yields the CPU
|
||||
// instead of being a near-instant scheduler hint that lets the tight
|
||||
// dispatchLoop caller spin just as hot as before.
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
|
||||
func (t *DarwinTun) newEndpoint() (stack.LinkEndpoint, error) {
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestSelectDarwinGatewayDefault(t *testing.T) {
|
||||
@@ -47,3 +51,183 @@ func TestSelectDarwinGatewayRequiresUsableLocalAddress(t *testing.T) {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
// newTestSocketpair returns a connected AF_UNIX/SOCK_DGRAM pair -- a real
|
||||
// fd DarwinTun.Wait's kqueue can register EVFILT_READ against, without
|
||||
// needing an actual utun interface (which requires root/network
|
||||
// entitlements this test environment doesn't have). Datagram sockets
|
||||
// (unlike pipes) support both "write makes readable" and "close makes
|
||||
// readable" the same way a tun fd's read-readiness behaves.
|
||||
func newTestSocketpair(t *testing.T) (a, b int) {
|
||||
t.Helper()
|
||||
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_DGRAM, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("socketpair: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = unix.Close(fds[0])
|
||||
_ = unix.Close(fds[1])
|
||||
})
|
||||
return fds[0], fds[1]
|
||||
}
|
||||
|
||||
// Reviewer feedback, XTLS/Xray-core#6580: "blocking with no data" case --
|
||||
// wait() must not return before the timeout when nothing is written.
|
||||
func TestWaitKqueueBlocksWithNoData(t *testing.T) {
|
||||
a, _ := newTestSocketpair(t)
|
||||
kq := newWaitKqueue(a)
|
||||
if kq == nil {
|
||||
t.Fatal("newWaitKqueue returned nil")
|
||||
}
|
||||
defer kq.close()
|
||||
|
||||
start := time.Now()
|
||||
ok := kq.wait(150 * time.Millisecond)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if !ok {
|
||||
t.Fatal("wait() returned false on a healthy kqueue with a plain timeout")
|
||||
}
|
||||
if elapsed < 100*time.Millisecond {
|
||||
t.Fatalf("wait() returned after only %v, expected it to block close to the 150ms timeout", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
// Reviewer feedback: "wake up with a readable fd" case.
|
||||
func TestWaitKqueueWakesOnReadable(t *testing.T) {
|
||||
a, b := newTestSocketpair(t)
|
||||
kq := newWaitKqueue(a)
|
||||
if kq == nil {
|
||||
t.Fatal("newWaitKqueue returned nil")
|
||||
}
|
||||
defer kq.close()
|
||||
|
||||
done := make(chan bool, 1)
|
||||
go func() {
|
||||
done <- kq.wait(5 * time.Second)
|
||||
}()
|
||||
|
||||
time.Sleep(20 * time.Millisecond) // let wait() actually enter the syscall first
|
||||
if _, err := unix.Write(b, []byte{0x1}); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case ok := <-done:
|
||||
if !ok {
|
||||
t.Fatal("wait() returned false after the fd became readable")
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("wait() did not wake up within 2s of the fd becoming readable")
|
||||
}
|
||||
}
|
||||
|
||||
// Reviewer feedback: "timeout" case, explicitly (distinct from the
|
||||
// no-data test above, which also checks blocking duration -- this one
|
||||
// only checks the return value).
|
||||
func TestWaitKqueueTimesOut(t *testing.T) {
|
||||
a, _ := newTestSocketpair(t)
|
||||
kq := newWaitKqueue(a)
|
||||
if kq == nil {
|
||||
t.Fatal("newWaitKqueue returned nil")
|
||||
}
|
||||
defer kq.close()
|
||||
|
||||
if !kq.wait(50 * time.Millisecond) {
|
||||
t.Fatal("wait() returned false on a plain timeout with no error condition")
|
||||
}
|
||||
}
|
||||
|
||||
// Reviewer feedback: "Close() wakes a blocked wait" case, and the
|
||||
// no-double-close/no-fd-reuse concern (P1) -- close() while wait() is
|
||||
// parked in its syscall must not panic, must not leave wait() hung, and a
|
||||
// second close() call (from a caller that, say, calls Close() twice on
|
||||
// the same DarwinTun) must be safe.
|
||||
func TestWaitKqueueCloseDuringWaitIsSafe(t *testing.T) {
|
||||
a, _ := newTestSocketpair(t)
|
||||
kq := newWaitKqueue(a)
|
||||
if kq == nil {
|
||||
t.Fatal("newWaitKqueue returned nil")
|
||||
}
|
||||
|
||||
started := make(chan struct{})
|
||||
done := make(chan bool, 1)
|
||||
go func() {
|
||||
close(started)
|
||||
done <- kq.wait(5 * time.Second)
|
||||
}()
|
||||
|
||||
<-started
|
||||
time.Sleep(20 * time.Millisecond) // let wait() actually enter the syscall first
|
||||
kq.close()
|
||||
kq.close() // double-close must be idempotent, not panic or double-free the fd
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Either true (the close-of-the-underlying-fd unblocked kevent, a
|
||||
// real kqueue behavior) or false (wait() observed the closed flag
|
||||
// first) is acceptable -- what matters is that it returned at all,
|
||||
// promptly, without hanging or crashing.
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("wait() did not return within 2s of close() being called")
|
||||
}
|
||||
|
||||
// A wait() call *after* close() must return false immediately (the
|
||||
// closed-flag fast path), not attempt a syscall against the
|
||||
// already-closed (and potentially since-reused, on a real system) fd
|
||||
// number.
|
||||
if kq.wait(time.Second) {
|
||||
t.Fatal("wait() returned true after close() -- should short-circuit via the closed flag")
|
||||
}
|
||||
}
|
||||
|
||||
// Reviewer feedback: "multiple/concurrent close guard" case -- many
|
||||
// goroutines calling close() concurrently must close the underlying fd
|
||||
// exactly once.
|
||||
func TestWaitKqueueConcurrentCloseIsSafe(t *testing.T) {
|
||||
a, _ := newTestSocketpair(t)
|
||||
kq := newWaitKqueue(a)
|
||||
if kq == nil {
|
||||
t.Fatal("newWaitKqueue returned nil")
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for range 20 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
kq.close()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if !kq.closed.Load() {
|
||||
t.Fatal("closed flag not set after concurrent close() calls")
|
||||
}
|
||||
}
|
||||
|
||||
// Reviewer feedback: "kevent runtime failure without spinning" case (P2).
|
||||
// Simulates a kqueue that has gone bad (closed out from under it, as if a
|
||||
// concurrent/erroneous close happened) and confirms wait() reports it as
|
||||
// unusable (false) rather than silently returning true forever, which is
|
||||
// what DarwinTun.Wait relies on to permanently fall back to the sleep
|
||||
// path instead of re-entering a failing syscall on every dispatchLoop
|
||||
// iteration.
|
||||
func TestWaitKqueueReportsPersistentFailure(t *testing.T) {
|
||||
a, _ := newTestSocketpair(t)
|
||||
kq := newWaitKqueue(a)
|
||||
if kq == nil {
|
||||
t.Fatal("newWaitKqueue returned nil")
|
||||
}
|
||||
// Close the underlying kqueue fd directly (bypassing kq.close(), which
|
||||
// would also set the closed flag) to simulate the fd going bad for a
|
||||
// reason other than this type's own close() -- e.g. some other code
|
||||
// path in the process closing it, or the kernel invalidating it.
|
||||
_ = unix.Close(kq.fd)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
if kq.wait(50 * time.Millisecond) {
|
||||
t.Fatalf("wait() call %d returned true against a closed underlying fd -- should report failure, not spin", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+729
-25
@@ -3,28 +3,60 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
_ "unsafe"
|
||||
"net/netip"
|
||||
"os"
|
||||
"slices"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
"gvisor.dev/gvisor/pkg/buffer"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||
|
||||
"golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
xerrors "github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
const tunHeaderSize = 4
|
||||
const (
|
||||
tunHeaderSize = 4
|
||||
defaultFreeBSDGateway = "169.254.10.1/30"
|
||||
|
||||
// escapeFib is the routing table outbound sockets are switched to so
|
||||
// their traffic bypasses the TUN routes installed in the default FIB
|
||||
// (FreeBSD's substitute for the per-socket interface binding other
|
||||
// platforms use). Requires the boot tunable net.fibs >= 2.
|
||||
escapeFib = 1
|
||||
)
|
||||
|
||||
//go:linkname procyield runtime.procyield
|
||||
func procyield(cycles uint32)
|
||||
|
||||
type FreeBSDTun struct {
|
||||
device tun.Device
|
||||
mtu uint32
|
||||
device tun.Device
|
||||
options *Config
|
||||
tunIndex int
|
||||
autoInterface bool
|
||||
|
||||
systemRoutes []netip.Prefix
|
||||
escapeMu sync.Mutex
|
||||
escapeRoutes []escapeRoute
|
||||
routeMonitor *os.File
|
||||
routeMonitorOnce sync.Once
|
||||
}
|
||||
|
||||
// escapeRoute remembers one route written into the escape FIB, in the exact
|
||||
// shape needed to delete it again. A zero gateway means an interface route.
|
||||
type escapeRoute struct {
|
||||
prefix netip.Prefix
|
||||
ifIndex int
|
||||
gateway netip.Addr
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -34,20 +66,172 @@ var (
|
||||
|
||||
// NewTun builds new tun interface handler
|
||||
func NewTun(options *Config) (Tun, error) {
|
||||
gateway, local, err := selectFreeBSDGateway(options.Gateway)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// net.fibs is a boot-time constant, so validate the escape routing table
|
||||
// before the shared handler registers a dialer controller that would
|
||||
// otherwise steer every outbound socket into a table that was never set up.
|
||||
if options.AutoOutboundsInterface != "" {
|
||||
if err := checkEscapeFib(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tunDev, err := tun.CreateTUN(options.Name, int(options.MTU))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &FreeBSDTun{device: tunDev, mtu: options.MTU}, nil
|
||||
name, err := tunDev.Name()
|
||||
if err != nil {
|
||||
_ = tunDev.Close()
|
||||
return nil, err
|
||||
}
|
||||
// From here the interface exists in the kernel; the wireguard library does
|
||||
// not remove it on Close, so every failure path must destroy it too or the
|
||||
// next start fails with "interface already exists".
|
||||
iface, err := net.InterfaceByName(name)
|
||||
if err != nil {
|
||||
_ = tunDev.Close()
|
||||
destroyInterface(name)
|
||||
return nil, err
|
||||
}
|
||||
if err := setIPAddress(name, gateway, local, iface.Index); err != nil {
|
||||
_ = tunDev.Close()
|
||||
destroyInterface(name)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &FreeBSDTun{
|
||||
device: tunDev,
|
||||
options: options,
|
||||
tunIndex: iface.Index,
|
||||
autoInterface: options.AutoOutboundsInterface != "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// selectFreeBSDGateway picks the first IPv4 prefix from the configured gateway
|
||||
// list and the local address derived from it (the darwin semantics: the
|
||||
// gateway is the remote side of the point-to-point pair, the local address is
|
||||
// the next one after it), falling back to the same link-local default.
|
||||
func selectFreeBSDGateway(configured []string) (netip.Prefix, netip.Addr, error) {
|
||||
gateway := netip.MustParsePrefix(defaultFreeBSDGateway)
|
||||
if len(configured) > 0 {
|
||||
found := false
|
||||
for _, value := range configured {
|
||||
prefix, err := netip.ParsePrefix(value)
|
||||
if err != nil {
|
||||
return netip.Prefix{}, netip.Addr{}, xerrors.New("invalid FreeBSD gateway ", value).Base(err)
|
||||
}
|
||||
if prefix.Addr().Is4() {
|
||||
gateway, found = prefix, true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return netip.Prefix{}, netip.Addr{}, xerrors.New("FreeBSD gateway requires at least one IPv4 prefix")
|
||||
}
|
||||
}
|
||||
|
||||
local, ok := nextLocalIPv4(gateway)
|
||||
if !ok || !gateway.Contains(local) {
|
||||
return netip.Prefix{}, netip.Addr{}, xerrors.New("FreeBSD gateway ", gateway.String(), " must contain at least one usable local IPv4 address after the gateway address")
|
||||
}
|
||||
return gateway, local, nil
|
||||
}
|
||||
|
||||
func nextLocalIPv4(gateway netip.Prefix) (netip.Addr, bool) {
|
||||
local4 := gateway.Addr().As4()
|
||||
for i := len(local4) - 1; i >= 0; i-- {
|
||||
local4[i]++
|
||||
if local4[i] != 0 {
|
||||
return netip.AddrFrom4(local4), true
|
||||
}
|
||||
}
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
func (t *FreeBSDTun) Start() error {
|
||||
if err := t.setSystemRoutes(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Gate on this instance's own option, not the package-global updater,
|
||||
// which a previously-removed inbound may have left set. checkEscapeFib
|
||||
// already ran in NewTun, before the dialer controller was registered.
|
||||
if t.autoInterface {
|
||||
if err := t.syncEscapeFib(); err != nil {
|
||||
_ = t.unsetSystemRoutes()
|
||||
return err
|
||||
}
|
||||
fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
|
||||
if err != nil {
|
||||
t.unsetEscapeFib()
|
||||
_ = t.unsetSystemRoutes()
|
||||
return err
|
||||
}
|
||||
t.routeMonitor = os.NewFile(uintptr(fd), "xray-route-monitor")
|
||||
go t.monitorRouteChanges()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// monitorRouteChanges refreshes the outbound interface and the escape FIB
|
||||
// mirror whenever the system routing table changes.
|
||||
func (t *FreeBSDTun) monitorRouteChanges() {
|
||||
buffer := make([]byte, 64*1024)
|
||||
for {
|
||||
if _, err := t.routeMonitor.Read(buffer); err != nil {
|
||||
if !errors.Is(err, os.ErrClosed) {
|
||||
xerrors.LogInfoInner(context.Background(), err, "[tun] failed to monitor route changes")
|
||||
}
|
||||
return
|
||||
}
|
||||
if updater != nil {
|
||||
updater.Update()
|
||||
}
|
||||
if err := t.syncEscapeFib(); err != nil {
|
||||
xerrors.LogInfoInner(context.Background(), err, "[tun] failed to refresh escape routes")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *FreeBSDTun) Close() error {
|
||||
return t.device.Close()
|
||||
t.routeMonitorOnce.Do(func() {
|
||||
if t.routeMonitor != nil {
|
||||
_ = t.routeMonitor.Close()
|
||||
}
|
||||
})
|
||||
t.unsetEscapeFib()
|
||||
routeErr := t.unsetSystemRoutes()
|
||||
name, nameErr := t.Name()
|
||||
closeErr := t.device.Close()
|
||||
// The wireguard tun device does not tear the interface down on FreeBSD,
|
||||
// so an unclean shutdown would leave utun<n> behind and the next start
|
||||
// would fail with "interface already exists"; destroy it explicitly.
|
||||
if nameErr == nil {
|
||||
destroyInterface(name)
|
||||
}
|
||||
return xerrors.Combine(routeErr, closeErr)
|
||||
}
|
||||
|
||||
func destroyInterface(name string) {
|
||||
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer unix.Close(fd)
|
||||
// struct ifreq: 16-byte name + a 16-byte union (SIOCIFDESTROY's encoded
|
||||
// length is 32 bytes on amd64, and the kernel copies in all of it).
|
||||
var req struct {
|
||||
Name [unix.IFNAMSIZ]byte
|
||||
_ [16]byte
|
||||
}
|
||||
copy(req.Name[:], name)
|
||||
_ = ioctlPtr(fd, unix.SIOCIFDESTROY, unsafe.Pointer(&req))
|
||||
}
|
||||
|
||||
func (t *FreeBSDTun) Name() (string, error) {
|
||||
@@ -55,21 +239,13 @@ func (t *FreeBSDTun) Name() (string, error) {
|
||||
}
|
||||
|
||||
func (t *FreeBSDTun) Index() (int, error) {
|
||||
name, err := t.Name()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
iface, err := net.InterfaceByName(name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return iface.Index, nil
|
||||
return t.tunIndex, nil
|
||||
}
|
||||
|
||||
// WritePacket implements GVisorDevice method to write one packet to the tun device
|
||||
func (t *FreeBSDTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
|
||||
// request memory to write from reusable buffer pool
|
||||
b := buf.NewWithSize(int32(t.mtu) + tunHeaderSize)
|
||||
b := buf.NewWithSize(int32(t.options.MTU) + tunHeaderSize)
|
||||
defer b.Release()
|
||||
|
||||
// prepare Unix specific packet header
|
||||
@@ -104,7 +280,7 @@ func (t *FreeBSDTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
|
||||
// which will make the stack call Wait which should implement desired push-back
|
||||
func (t *FreeBSDTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
|
||||
// request memory to write from reusable buffer pool
|
||||
b := buf.NewWithSize(int32(t.mtu) + tunHeaderSize)
|
||||
b := buf.NewWithSize(int32(t.options.MTU) + tunHeaderSize)
|
||||
|
||||
// read the bytes to the interface file
|
||||
n, err := b.ReadFrom(t.device.File())
|
||||
@@ -141,23 +317,551 @@ func (t *FreeBSDTun) Wait() {
|
||||
}
|
||||
|
||||
func (t *FreeBSDTun) newEndpoint() (stack.LinkEndpoint, error) {
|
||||
return &LinkEndpoint{deviceMTU: t.mtu, device: t}, nil
|
||||
return &LinkEndpoint{deviceMTU: t.options.MTU, device: t}, nil
|
||||
}
|
||||
|
||||
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
|
||||
const (
|
||||
IN6_IFF_NODAD = 0x0020 // netinet6/in6_var.h
|
||||
ND6_INFINITE_LIFETIME = 0xFFFFFFFF // netinet6/nd6.h
|
||||
)
|
||||
|
||||
// ifAliasReq4 is struct in_aliasreq from netinet/in_var.h in the 64-byte
|
||||
// layout unix.SIOCAIFADDR encodes (name + addr/dstaddr/mask sockaddrs).
|
||||
type ifAliasReq4 struct {
|
||||
Name [unix.IFNAMSIZ]byte
|
||||
Addr unix.RawSockaddrInet4
|
||||
Dstaddr unix.RawSockaddrInet4
|
||||
Mask unix.RawSockaddrInet4
|
||||
}
|
||||
|
||||
// ifAliasReq6 is struct in6_aliasreq from netinet6/in6_var.h. The trailing
|
||||
// Vhid field matters: unix.SIOCAIFADDR_IN6 is not in x/sys/unix, so
|
||||
// siocaifaddrIn6 is derived from this struct's size, and the kernel only
|
||||
// accepts the ioctl whose encoded length matches the real struct.
|
||||
type ifAliasReq6 struct {
|
||||
Name [unix.IFNAMSIZ]byte
|
||||
Addr unix.RawSockaddrInet6
|
||||
Dstaddr unix.RawSockaddrInet6
|
||||
Prefixmask unix.RawSockaddrInet6
|
||||
Flags int32
|
||||
Lifetime addrLifetime6
|
||||
Vhid int32
|
||||
}
|
||||
|
||||
// addrLifetime6 is struct in6_addrlifetime (time_t is int64 on freebsd/amd64).
|
||||
type addrLifetime6 struct {
|
||||
Expire int64
|
||||
Preferred int64
|
||||
Vltime uint32
|
||||
Pltime uint32
|
||||
}
|
||||
|
||||
// SIOCAIFADDR_IN6 = _IOW('i', 27, struct in6_aliasreq); x/sys/unix does not
|
||||
// carry the netinet6 ioctls, so encode it from the struct size like the
|
||||
// header macro does.
|
||||
const siocaifaddrIn6 = 0x80000000 | (uintptr(unsafe.Sizeof(ifAliasReq6{})) << 16) | ('i' << 8) | 27
|
||||
|
||||
// setIPAddress assigns the local/remote point-to-point IPv4 pair and a
|
||||
// link-local IPv6 address to the interface, required for the routing to work
|
||||
// (same scheme as the darwin implementation: local address is the one right
|
||||
// after the gateway address).
|
||||
func setIPAddress(name string, gateway netip.Prefix, local netip.Addr, ifIndex int) error {
|
||||
socket4, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unix.Close(socket4)
|
||||
|
||||
local4 := local.As4()
|
||||
|
||||
ifReq4 := ifAliasReq4{
|
||||
Addr: unix.RawSockaddrInet4{
|
||||
Len: unix.SizeofSockaddrInet4,
|
||||
Family: unix.AF_INET,
|
||||
Addr: local4,
|
||||
},
|
||||
Dstaddr: unix.RawSockaddrInet4{
|
||||
Len: unix.SizeofSockaddrInet4,
|
||||
Family: unix.AF_INET,
|
||||
Addr: gateway.Addr().As4(),
|
||||
},
|
||||
Mask: unix.RawSockaddrInet4{
|
||||
Len: unix.SizeofSockaddrInet4,
|
||||
Family: unix.AF_INET,
|
||||
Addr: prefixMask4(gateway.Bits()),
|
||||
},
|
||||
}
|
||||
copy(ifReq4.Name[:], name)
|
||||
if err = ioctlPtr(socket4, unix.SIOCAIFADDR, unsafe.Pointer(&ifReq4)); err != nil {
|
||||
return os.NewSyscallError("SIOCAIFADDR", err)
|
||||
}
|
||||
|
||||
socket6, err := unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unix.Close(socket6)
|
||||
|
||||
// link-local ipv6 address with suffix from ipv4, enough for v6 interface
|
||||
// routes to be attachable (darwin parity); a link-local address needs its
|
||||
// scope, which for FreeBSD ioctls is the interface index
|
||||
local6 := netip.AddrFrom16([16]byte{0: 0xfe, 1: 0x80, 12: local4[0], 13: local4[1], 14: local4[2], 15: local4[3]})
|
||||
|
||||
ifReq6 := ifAliasReq6{
|
||||
Addr: unix.RawSockaddrInet6{
|
||||
Len: unix.SizeofSockaddrInet6,
|
||||
Family: unix.AF_INET6,
|
||||
Addr: local6.As16(),
|
||||
Scope_id: uint32(ifIndex),
|
||||
},
|
||||
Prefixmask: unix.RawSockaddrInet6{
|
||||
Len: unix.SizeofSockaddrInet6,
|
||||
Family: unix.AF_INET6,
|
||||
Addr: prefixMask6(64),
|
||||
},
|
||||
Flags: IN6_IFF_NODAD,
|
||||
Lifetime: addrLifetime6{
|
||||
Vltime: ND6_INFINITE_LIFETIME,
|
||||
Pltime: ND6_INFINITE_LIFETIME,
|
||||
},
|
||||
}
|
||||
copy(ifReq6.Name[:], name)
|
||||
if err = ioctlPtr(socket6, uint(siocaifaddrIn6), unsafe.Pointer(&ifReq6)); err != nil {
|
||||
// non-fatal: FreeBSD auto-configures a link-local address on UP
|
||||
// interfaces, which is all the v6 interface routes need
|
||||
xerrors.LogInfoInner(context.Background(), os.NewSyscallError("SIOCAIFADDR_IN6", err), "[tun] failed to assign the IPv6 link-local address")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
|
||||
if fixedName == "" {
|
||||
return nil, errors.New("automatic outbound interface selection is not supported on this platform")
|
||||
func ioctlPtr(fd int, req uint, arg unsafe.Pointer) error {
|
||||
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg))
|
||||
if errno != 0 {
|
||||
return errno
|
||||
}
|
||||
iface, err := net.InterfaceByName(fixedName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func prefixMask4(bits int) [4]byte {
|
||||
var mask [4]byte
|
||||
copy(mask[:], net.CIDRMask(bits, 32))
|
||||
return mask
|
||||
}
|
||||
|
||||
func prefixMask6(bits int) [16]byte {
|
||||
var mask [16]byte
|
||||
copy(mask[:], net.CIDRMask(bits, 128))
|
||||
return mask
|
||||
}
|
||||
|
||||
// setinterface is the per-socket half of autoOutboundsInterface. FreeBSD has
|
||||
// no SO_BINDTODEVICE/IP_BOUND_IF equivalent, so the socket is pointed at the
|
||||
// escape FIB instead, where Start() mirrors the physical default route; the
|
||||
// iface argument is resolved by the shared updater but unused here (the escape
|
||||
// is table-based, not a per-socket interface bind). checkEscapeFib in NewTun
|
||||
// guarantees the FIB exists before this can run.
|
||||
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
|
||||
return unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_SETFIB, escapeFib)
|
||||
}
|
||||
|
||||
func (t *FreeBSDTun) setSystemRoutes() error {
|
||||
routes, err := buildSystemRoutes(t.options.AutoSystemRoutingTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Route through the interface, not a gateway: the tun(4) device is a
|
||||
// broadcast interface here, so its point-to-point peer address doubles as
|
||||
// the subnet broadcast and the kernel refuses to route to it (EACCES).
|
||||
// Interface routes sidestep the gateway entirely (what wg-quick does on
|
||||
// FreeBSD).
|
||||
for _, destination := range routes {
|
||||
if err := execRoute(-1, unix.RTM_ADD, t.tunIndex, destination, netip.Addr{}); err != nil {
|
||||
_ = t.unsetSystemRoutes()
|
||||
return xerrors.New("failed to add system route ", destination).Base(err)
|
||||
}
|
||||
t.systemRoutes = append(t.systemRoutes, destination)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *FreeBSDTun) unsetSystemRoutes() error {
|
||||
var errs []error
|
||||
for i := len(t.systemRoutes) - 1; i >= 0; i-- {
|
||||
destination := t.systemRoutes[i]
|
||||
if err := execRoute(-1, unix.RTM_DELETE, t.tunIndex, destination, netip.Addr{}); err != nil && !errors.Is(err, unix.ESRCH) {
|
||||
errs = append(errs, xerrors.New("failed to delete system route ", destination).Base(err))
|
||||
}
|
||||
}
|
||||
t.systemRoutes = nil
|
||||
return xerrors.Combine(errs...)
|
||||
}
|
||||
|
||||
func buildSystemRoutes(configured []string) ([]netip.Prefix, error) {
|
||||
routes := make([]netip.Prefix, 0, len(configured))
|
||||
seen := make(map[netip.Prefix]struct{})
|
||||
|
||||
appendRoute := func(prefix netip.Prefix) {
|
||||
prefix = prefix.Masked()
|
||||
if _, found := seen[prefix]; found {
|
||||
return
|
||||
}
|
||||
seen[prefix] = struct{}{}
|
||||
routes = append(routes, prefix)
|
||||
}
|
||||
|
||||
for _, value := range configured {
|
||||
prefix, err := netip.ParsePrefix(value)
|
||||
if err != nil {
|
||||
return nil, xerrors.New("invalid system route ", value).Base(err)
|
||||
}
|
||||
if prefix.Bits() == 0 {
|
||||
for _, protected := range protectedDefaultRoutes(prefix.Addr().Is4()) {
|
||||
appendRoute(protected)
|
||||
}
|
||||
continue
|
||||
}
|
||||
appendRoute(prefix)
|
||||
}
|
||||
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
// protectedDefaultRoutes splits a full default route into eight more-specific
|
||||
// prefixes covering everything but the zero /8, so the system's real default
|
||||
// route stays in place for outbound interface discovery (darwin parity).
|
||||
func protectedDefaultRoutes(ipv4 bool) []netip.Prefix {
|
||||
routes := make([]netip.Prefix, 0, 8)
|
||||
for i := 0; i < 8; i++ {
|
||||
if ipv4 {
|
||||
var address [4]byte
|
||||
address[0] = 1 << i
|
||||
routes = append(routes, netip.PrefixFrom(netip.AddrFrom4(address), 8-i))
|
||||
} else {
|
||||
var address [16]byte
|
||||
address[0] = 1 << i
|
||||
routes = append(routes, netip.PrefixFrom(netip.AddrFrom16(address), 8-i))
|
||||
}
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
// execRoute writes one RTM message to a routing socket. fib >= 0 targets that
|
||||
// routing table via SO_SETFIB on the routing socket (what route(8) -fib
|
||||
// does); fib -1 leaves the process default table. An invalid gateway produces
|
||||
// an interface route pinned to interfaceIndex instead of a gateway route.
|
||||
func execRoute(fib int, messageType int, interfaceIndex int, destination netip.Prefix, gateway netip.Addr) error {
|
||||
message := route.RouteMessage{
|
||||
Type: messageType,
|
||||
Version: unix.RTM_VERSION,
|
||||
Flags: unix.RTF_STATIC | unix.RTF_GATEWAY,
|
||||
Seq: 1,
|
||||
}
|
||||
if messageType == unix.RTM_ADD {
|
||||
message.Flags |= unix.RTF_UP
|
||||
}
|
||||
|
||||
var gatewayAddr route.Addr
|
||||
switch {
|
||||
case !gateway.IsValid():
|
||||
message.Flags &^= unix.RTF_GATEWAY
|
||||
message.Index = interfaceIndex
|
||||
gatewayAddr = &route.LinkAddr{Index: interfaceIndex}
|
||||
case gateway.Is4():
|
||||
gatewayAddr = &route.Inet4Addr{IP: gateway.As4()}
|
||||
default:
|
||||
gatewayAddr = &route.Inet6Addr{IP: gateway.As16()}
|
||||
}
|
||||
|
||||
if destination.Addr().Is4() {
|
||||
message.Addrs = []route.Addr{
|
||||
unix.RTAX_DST: &route.Inet4Addr{IP: destination.Addr().As4()},
|
||||
unix.RTAX_NETMASK: &route.Inet4Addr{IP: prefixMask4(destination.Bits())},
|
||||
unix.RTAX_GATEWAY: gatewayAddr,
|
||||
}
|
||||
} else {
|
||||
message.Addrs = []route.Addr{
|
||||
unix.RTAX_DST: &route.Inet6Addr{IP: destination.Addr().As16()},
|
||||
unix.RTAX_NETMASK: &route.Inet6Addr{IP: prefixMask6(destination.Bits())},
|
||||
unix.RTAX_GATEWAY: gatewayAddr,
|
||||
}
|
||||
}
|
||||
|
||||
request, err := message.Marshal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unix.Close(fd)
|
||||
if fib >= 0 {
|
||||
if err := unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_SETFIB, fib); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = unix.Write(fd, request)
|
||||
return err
|
||||
}
|
||||
|
||||
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
|
||||
if fixedName != "" {
|
||||
iface, err := net.InterfaceByName(fixedName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if iface.Index == tunIndex {
|
||||
return nil, errors.New("outbound interface cannot be the TUN interface")
|
||||
}
|
||||
return iface, nil
|
||||
}
|
||||
|
||||
physical, err := physicalDefaultRoutes(tunIndex, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if iface.Index == tunIndex {
|
||||
return nil, errors.New("outbound interface cannot be the TUN interface")
|
||||
for _, family := range []int{unix.AF_INET, unix.AF_INET6} {
|
||||
for _, route := range physical {
|
||||
if route.family == family {
|
||||
return route.iface, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, errors.New("default route not found")
|
||||
}
|
||||
|
||||
// physicalRoute describes one physical default route: the interface it
|
||||
// leaves through, its gateway, and the connected prefix that makes the
|
||||
// gateway resolvable.
|
||||
type physicalRoute struct {
|
||||
family int
|
||||
iface *net.Interface
|
||||
gateway netip.Addr
|
||||
connected netip.Prefix
|
||||
}
|
||||
|
||||
// physicalDefaultRoutes scans the default routing table for default routes
|
||||
// that do not go through the TUN interface, at most one per address family
|
||||
// (the first usable one wins, matching the darwin implementation's
|
||||
// preference order). A non-zero onlyIndex restricts the scan to that
|
||||
// interface, for the fixed-name mode of autoOutboundsInterface.
|
||||
func physicalDefaultRoutes(tunIndex int, onlyIndex int) ([]physicalRoute, error) {
|
||||
rib, err := route.FetchRIB(unix.AF_UNSPEC, route.RIBTypeRoute, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
messages, err := route.ParseRIB(route.RIBTypeRoute, rib)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
found := make([]physicalRoute, 0, 2)
|
||||
seen := make(map[int]bool)
|
||||
for _, message := range messages {
|
||||
routeMessage, ok := message.(*route.RouteMessage)
|
||||
if !ok || routeMessage.Index == tunIndex {
|
||||
continue
|
||||
}
|
||||
if onlyIndex != 0 && routeMessage.Index != onlyIndex {
|
||||
continue
|
||||
}
|
||||
if routeMessage.Flags&unix.RTF_UP == 0 || routeMessage.Flags&unix.RTF_GATEWAY == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
family, ok := defaultRouteFamily(routeMessage)
|
||||
if !ok || seen[family] {
|
||||
continue
|
||||
}
|
||||
iface, err := usableInterface(routeMessage.Index)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
gatewayAddr, ok := routeAddrToNetip(routeMessage.Addrs[unix.RTAX_GATEWAY])
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
connected, err := connectedPrefix(iface, gatewayAddr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
seen[family] = true
|
||||
found = append(found, physicalRoute{
|
||||
family: family,
|
||||
iface: iface,
|
||||
gateway: gatewayAddr,
|
||||
connected: connected,
|
||||
})
|
||||
}
|
||||
|
||||
if len(found) == 0 {
|
||||
return nil, errors.New("default route not found")
|
||||
}
|
||||
return found, nil
|
||||
}
|
||||
|
||||
// defaultRouteFamily reports the address family of a RIB message that
|
||||
// represents a true default route (unspecified destination, zero mask).
|
||||
func defaultRouteFamily(message *route.RouteMessage) (int, bool) {
|
||||
if len(message.Addrs) <= unix.RTAX_NETMASK {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
switch destination := message.Addrs[unix.RTAX_DST].(type) {
|
||||
case *route.Inet4Addr:
|
||||
mask, ok := message.Addrs[unix.RTAX_NETMASK].(*route.Inet4Addr)
|
||||
if !ok || destination.IP != netip.IPv4Unspecified().As4() {
|
||||
return 0, false
|
||||
}
|
||||
ones, bits := net.IPMask(mask.IP[:]).Size()
|
||||
return unix.AF_INET, ones == 0 && bits == 32
|
||||
case *route.Inet6Addr:
|
||||
mask, ok := message.Addrs[unix.RTAX_NETMASK].(*route.Inet6Addr)
|
||||
if !ok || destination.IP != netip.IPv6Unspecified().As16() {
|
||||
return 0, false
|
||||
}
|
||||
ones, bits := net.IPMask(mask.IP[:]).Size()
|
||||
return unix.AF_INET6, ones == 0 && bits == 128
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func usableInterface(index int) (*net.Interface, error) {
|
||||
iface, err := net.InterfaceByIndex(index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
|
||||
return nil, errors.New("default route interface is not usable")
|
||||
}
|
||||
return iface, nil
|
||||
}
|
||||
|
||||
func routeAddrToNetip(addr route.Addr) (netip.Addr, bool) {
|
||||
switch typed := addr.(type) {
|
||||
case *route.Inet4Addr:
|
||||
return netip.AddrFrom4(typed.IP), true
|
||||
case *route.Inet6Addr:
|
||||
return netip.AddrFrom16(typed.IP), true
|
||||
default:
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
}
|
||||
|
||||
// checkEscapeFib verifies the system can host the escape routing table at
|
||||
// all: FIBs are a boot-time resource on FreeBSD.
|
||||
func checkEscapeFib() error {
|
||||
fibs, err := unix.SysctlUint32("net.fibs")
|
||||
if err != nil {
|
||||
return xerrors.New("failed to read net.fibs").Base(err)
|
||||
}
|
||||
if fibs < 2 {
|
||||
return errors.New("automatic outbound interface on FreeBSD needs a second routing table: add net.fibs=2 to /boot/loader.conf and reboot")
|
||||
}
|
||||
current, err := unix.SysctlUint32("net.my_fibnum")
|
||||
if err != nil {
|
||||
return xerrors.New("failed to read net.my_fibnum").Base(err)
|
||||
}
|
||||
if current == escapeFib {
|
||||
return errors.New("xray runs inside routing table 1, which is reserved as the escape table; start it in another FIB")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncEscapeFib mirrors the physical default routes (and the connected
|
||||
// prefixes their gateways resolve through) into the escape FIB, replacing
|
||||
// whatever mirror a previous call installed. On discovery failure the old
|
||||
// mirror is kept, since a stale escape route beats none during a transient
|
||||
// route flap.
|
||||
func (t *FreeBSDTun) syncEscapeFib() error {
|
||||
var onlyIndex int
|
||||
if t.options.AutoOutboundsInterface != "" && updater != nil {
|
||||
if iface := updater.Get(); iface != nil {
|
||||
onlyIndex = iface.Index
|
||||
}
|
||||
}
|
||||
physical, err := physicalDefaultRoutes(t.tunIndex, onlyIndex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desired := make([]escapeRoute, 0, 2*len(physical))
|
||||
for _, p := range physical {
|
||||
desired = append(desired,
|
||||
escapeRoute{prefix: p.connected, ifIndex: p.iface.Index},
|
||||
escapeRoute{prefix: defaultPrefix(p.family), ifIndex: p.iface.Index, gateway: p.gateway},
|
||||
)
|
||||
}
|
||||
|
||||
t.escapeMu.Lock()
|
||||
defer t.escapeMu.Unlock()
|
||||
|
||||
// The route monitor hears our own escape FIB writes too; rewriting an
|
||||
// unchanged mirror on every wake-up would ping-pong forever.
|
||||
if slices.Equal(t.escapeRoutes, desired) {
|
||||
return nil
|
||||
}
|
||||
|
||||
t.unsetEscapeFibLocked()
|
||||
for _, entry := range desired {
|
||||
err := execRoute(escapeFib, unix.RTM_ADD, entry.ifIndex, entry.prefix, entry.gateway)
|
||||
if err != nil && !errors.Is(err, unix.EEXIST) {
|
||||
return xerrors.New("failed to add escape route ", entry.prefix).Base(err)
|
||||
}
|
||||
t.escapeRoutes = append(t.escapeRoutes, entry)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *FreeBSDTun) unsetEscapeFib() {
|
||||
t.escapeMu.Lock()
|
||||
defer t.escapeMu.Unlock()
|
||||
t.unsetEscapeFibLocked()
|
||||
}
|
||||
|
||||
func (t *FreeBSDTun) unsetEscapeFibLocked() {
|
||||
for i := len(t.escapeRoutes) - 1; i >= 0; i-- {
|
||||
entry := t.escapeRoutes[i]
|
||||
err := execRoute(escapeFib, unix.RTM_DELETE, entry.ifIndex, entry.prefix, entry.gateway)
|
||||
if err != nil && !errors.Is(err, unix.ESRCH) {
|
||||
xerrors.LogInfoInner(context.Background(), err, "[tun] failed to delete escape route ", entry.prefix)
|
||||
}
|
||||
}
|
||||
t.escapeRoutes = nil
|
||||
}
|
||||
|
||||
func defaultPrefix(family int) netip.Prefix {
|
||||
if family == unix.AF_INET {
|
||||
return netip.PrefixFrom(netip.IPv4Unspecified(), 0)
|
||||
}
|
||||
return netip.PrefixFrom(netip.IPv6Unspecified(), 0)
|
||||
}
|
||||
|
||||
// connectedPrefix finds the interface's address prefix containing the
|
||||
// gateway, which the escape FIB needs as an interface route so the mirrored
|
||||
// default route's gateway is resolvable there.
|
||||
func connectedPrefix(iface *net.Interface, gateway netip.Addr) (netip.Prefix, error) {
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return netip.Prefix{}, err
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
ipNet, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ip, ok := netip.AddrFromSlice(ipNet.IP)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ip = ip.Unmap()
|
||||
ones, _ := ipNet.Mask.Size()
|
||||
prefix := netip.PrefixFrom(ip, ones).Masked()
|
||||
if prefix.Contains(gateway.WithZone("").Unmap()) {
|
||||
return prefix, nil
|
||||
}
|
||||
}
|
||||
return netip.Prefix{}, errors.New("no connected prefix contains the gateway")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
//go:build freebsd
|
||||
|
||||
package tun
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestSelectFreeBSDGatewayDefault(t *testing.T) {
|
||||
gateway, local, err := selectFreeBSDGateway(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gateway != netip.MustParsePrefix(defaultFreeBSDGateway) {
|
||||
t.Fatal("expected default gateway, got ", gateway)
|
||||
}
|
||||
if local != netip.MustParseAddr("169.254.10.2") {
|
||||
t.Fatal("wrong local address: ", local)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectFreeBSDGatewayPicksFirstIPv4(t *testing.T) {
|
||||
gateway, local, err := selectFreeBSDGateway([]string{"fd00::1/64", "10.0.0.1/30", "10.9.9.9/24"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gateway != netip.MustParsePrefix("10.0.0.1/30") {
|
||||
t.Fatal("wrong gateway: ", gateway)
|
||||
}
|
||||
if local != netip.MustParseAddr("10.0.0.2") {
|
||||
t.Fatal("wrong local address: ", local)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectFreeBSDGatewayRequiresIPv4(t *testing.T) {
|
||||
if _, _, err := selectFreeBSDGateway([]string{"fd00::1/64"}); err == nil {
|
||||
t.Fatal("expected error when no IPv4 gateway is configured")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectFreeBSDGatewayRejectsGarbage(t *testing.T) {
|
||||
if _, _, err := selectFreeBSDGateway([]string{"not-a-prefix"}); err == nil {
|
||||
t.Fatal("expected error for invalid gateway")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectFreeBSDGatewayRejectsFullPrefix(t *testing.T) {
|
||||
// 10.0.0.255/30: the "next" local address falls outside the prefix
|
||||
if _, _, err := selectFreeBSDGateway([]string{"10.0.0.255/30"}); err == nil {
|
||||
t.Fatal("expected error when no usable local address follows the gateway")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextLocalIPv4(t *testing.T) {
|
||||
local, ok := nextLocalIPv4(netip.MustParsePrefix("169.254.10.1/30"))
|
||||
if !ok || local != netip.MustParseAddr("169.254.10.2") {
|
||||
t.Fatal("wrong local address: ", local)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSystemRoutesSplitsDefault(t *testing.T) {
|
||||
routes, err := buildSystemRoutes([]string{"0.0.0.0/0"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected := []string{
|
||||
"1.0.0.0/8", "2.0.0.0/7", "4.0.0.0/6", "8.0.0.0/5",
|
||||
"16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/1",
|
||||
}
|
||||
if len(routes) != len(expected) {
|
||||
t.Fatal("expected ", len(expected), " routes, got ", routes)
|
||||
}
|
||||
for i, want := range expected {
|
||||
if routes[i] != netip.MustParsePrefix(want) {
|
||||
t.Fatal("route ", i, ": expected ", want, ", got ", routes[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSystemRoutesSplitsDefaultIPv6(t *testing.T) {
|
||||
routes, err := buildSystemRoutes([]string{"::/0"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(routes) != 8 || routes[7] != netip.MustParsePrefix("8000::/1") {
|
||||
t.Fatal("unexpected v6 split: ", routes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSystemRoutesDeduplicates(t *testing.T) {
|
||||
routes, err := buildSystemRoutes([]string{"10.0.0.0/8", "10.1.2.3/8", "0.0.0.0/0", "0.0.0.0/0"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(routes) != 9 { // 10.0.0.0/8 once + 8 splits once
|
||||
t.Fatal("expected 9 routes, got ", routes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSystemRoutesRejectsGarbage(t *testing.T) {
|
||||
if _, err := buildSystemRoutes([]string{"10.0.0.0/33"}); err == nil {
|
||||
t.Fatal("expected error for invalid route")
|
||||
}
|
||||
}
|
||||
|
||||
func routeMessage(dst, mask route.Addr) *route.RouteMessage {
|
||||
addrs := make([]route.Addr, unix.RTAX_NETMASK+1)
|
||||
addrs[unix.RTAX_DST] = dst
|
||||
addrs[unix.RTAX_NETMASK] = mask
|
||||
return &route.RouteMessage{Addrs: addrs}
|
||||
}
|
||||
|
||||
func TestDefaultRouteFamilyMatchesIPv4Default(t *testing.T) {
|
||||
family, ok := defaultRouteFamily(routeMessage(&route.Inet4Addr{}, &route.Inet4Addr{}))
|
||||
if !ok || family != unix.AF_INET {
|
||||
t.Fatal("expected IPv4 default route match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRouteFamilyMatchesIPv6Default(t *testing.T) {
|
||||
family, ok := defaultRouteFamily(routeMessage(&route.Inet6Addr{}, &route.Inet6Addr{}))
|
||||
if !ok || family != unix.AF_INET6 {
|
||||
t.Fatal("expected IPv6 default route match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRouteFamilyRejectsNonDefault(t *testing.T) {
|
||||
if _, ok := defaultRouteFamily(routeMessage(
|
||||
&route.Inet4Addr{IP: [4]byte{10, 0, 0, 0}},
|
||||
&route.Inet4Addr{IP: [4]byte{255, 0, 0, 0}},
|
||||
)); ok {
|
||||
t.Fatal("non-default destination must not match")
|
||||
}
|
||||
if _, ok := defaultRouteFamily(routeMessage(
|
||||
&route.Inet4Addr{},
|
||||
&route.Inet4Addr{IP: [4]byte{255, 0, 0, 0}},
|
||||
)); ok {
|
||||
t.Fatal("non-zero mask must not match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRouteFamilyRejectsShortAddrs(t *testing.T) {
|
||||
if _, ok := defaultRouteFamily(&route.RouteMessage{}); ok {
|
||||
t.Fatal("message without addresses must not match")
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ var _ GVisorDevice = (*WindowsTun)(nil)
|
||||
// interface with the same name exist, it tried to be reused.
|
||||
func NewTun(options *Config) (Tun, error) {
|
||||
// instantiate wintun adapter
|
||||
adapter, err := open(options.Name)
|
||||
adapter, err := open(options.Name, options.Desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -73,12 +73,12 @@ func NewTun(options *Config) (Tun, error) {
|
||||
return tun, nil
|
||||
}
|
||||
|
||||
func open(name string) (*wintun.Adapter, error) {
|
||||
func open(name, desc string) (*wintun.Adapter, error) {
|
||||
// generate a deterministic GUID from the adapter name
|
||||
id := md5.Sum([]byte(name))
|
||||
guid := (*windows.GUID)(unsafe.Pointer(&id[0]))
|
||||
// try to create adapter anew
|
||||
adapter, err := wintun.CreateAdapter(name, "Xray", guid)
|
||||
adapter, err := wintun.CreateAdapter(name, desc, guid)
|
||||
if err == nil {
|
||||
return adapter, nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"hash/crc64"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
@@ -218,10 +219,17 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
return nil
|
||||
}
|
||||
|
||||
var enablePadding = false
|
||||
var enablePadding atomic.Bool
|
||||
|
||||
func shouldEnablePadding(s protocol.SecurityType) bool {
|
||||
return enablePadding || s == protocol.SecurityType_AES128_GCM || s == protocol.SecurityType_CHACHA20_POLY1305 || s == protocol.SecurityType_AUTO
|
||||
return enablePadding.Load() || s == protocol.SecurityType_AES128_GCM || s == protocol.SecurityType_CHACHA20_POLY1305 || s == protocol.SecurityType_AUTO
|
||||
}
|
||||
|
||||
func reloadEnvSettings() error {
|
||||
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
||||
paddingValue := platform.NewEnvFlag(platform.UseVmessPadding).GetValue(func() string { return defaultFlagValue })
|
||||
enablePadding.Store(paddingValue != defaultFlagValue)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -229,10 +237,5 @@ func init() {
|
||||
return New(ctx, config.(*Config))
|
||||
}))
|
||||
|
||||
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
||||
|
||||
paddingValue := platform.NewEnvFlag(platform.UseVmessPadding).GetValue(func() string { return defaultFlagValue })
|
||||
if paddingValue != defaultFlagValue {
|
||||
enablePadding = true
|
||||
}
|
||||
platform.RegisterEnvReload(reloadEnvSettings)
|
||||
}
|
||||
|
||||
+63
-27
@@ -5,9 +5,10 @@ import (
|
||||
"fmt"
|
||||
gonet "net"
|
||||
"net/netip"
|
||||
reflect "reflect"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
|
||||
@@ -30,6 +31,11 @@ import (
|
||||
"golang.zx2c4.com/wireguard/device"
|
||||
)
|
||||
|
||||
type entry struct {
|
||||
got []net.IP
|
||||
time time.Time
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
conf *DeviceConfig
|
||||
policyManager policy.Manager
|
||||
@@ -43,6 +49,11 @@ type Handler struct {
|
||||
tnet *Net
|
||||
dev *device.Device
|
||||
mu sync.Mutex
|
||||
|
||||
// TODO: cache cleanup loop
|
||||
local bool
|
||||
cache map[string]entry
|
||||
cacheMu sync.Mutex
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
@@ -57,7 +68,7 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
if len(tag) > 0 && p.ForSystem().Stats.OutboundUplink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "outbound>>>" + tag + ">>>traffic>>>uplink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
uplinkCounter = c
|
||||
}
|
||||
@@ -65,7 +76,7 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
if len(tag) > 0 && p.ForSystem().Stats.OutboundDownlink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "outbound>>>" + tag + ">>>traffic>>>downlink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
downlinkCounter = c
|
||||
}
|
||||
@@ -98,6 +109,20 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
local := false
|
||||
dns := conf.DNS
|
||||
if len(dns) == 0 {
|
||||
dns = []string{"1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"}
|
||||
}
|
||||
if len(dns) == 1 && dns[0] == "local" {
|
||||
local = true
|
||||
dns = nil
|
||||
}
|
||||
dnses := make([]netip.Addr, 0, len(dns))
|
||||
for _, dns := range dns {
|
||||
dnses = append(dnses, netip.MustParseAddr(dns))
|
||||
}
|
||||
|
||||
kernelTunSupported, err := KernelTunSupported()
|
||||
if err != nil {
|
||||
errors.LogWarningInner(context.Background(), err, "Failed to check kernel TUN support")
|
||||
@@ -106,10 +131,10 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
var tnet *Net
|
||||
if !conf.NoKernelTun && kernelTunSupported {
|
||||
errors.LogWarning(context.Background(), "Using kernel TUN")
|
||||
tun, tnet, err = createKernelTun(localAddresses, []netip.Addr{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("1.0.0.1"), netip.MustParseAddr("2606:4700:4700::1111"), netip.MustParseAddr("2606:4700:4700::1001")}, int(conf.Mtu))
|
||||
tun, tnet, err = createKernelTun(localAddresses, dnses, int(conf.Mtu))
|
||||
} else {
|
||||
errors.LogWarning(context.Background(), "Using gVisor TUN")
|
||||
tun, tnet, _, err = CreateNetTUN(localAddresses, []netip.Addr{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("1.0.0.1"), netip.MustParseAddr("2606:4700:4700::1111"), netip.MustParseAddr("2606:4700:4700::1001")}, int(conf.Mtu), true)
|
||||
tun, tnet, _, err = CreateNetTUN(localAddresses, dnses, int(conf.Mtu), true)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -126,6 +151,9 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
|
||||
tun: tun,
|
||||
tnet: tnet,
|
||||
|
||||
local: local,
|
||||
cache: make(map[string]entry),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -138,14 +166,9 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
}
|
||||
ob.Name = "wireguard"
|
||||
ob.CanSpliceCopy = 3
|
||||
dialer.SetOutboundGateway(ctx, ob)
|
||||
|
||||
if h.dev == nil {
|
||||
if err := h.init(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.dev.Up(); err != nil {
|
||||
if err := h.init(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -258,8 +281,11 @@ func (h *Handler) Close() (err error) {
|
||||
func (h *Handler) init(ctx context.Context) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if h.tun == nil {
|
||||
return errors.New("closed")
|
||||
}
|
||||
if h.dev != nil {
|
||||
return nil
|
||||
return h.dev.Up()
|
||||
}
|
||||
resolveFunc := h.resolveLocal
|
||||
listenFunc := func() (net.PacketConn, error) {
|
||||
@@ -345,31 +371,34 @@ func (h *Handler) init(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (h *Handler) resolveLocal(host string) (net.IP, error) {
|
||||
return resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, error) {
|
||||
ips, _, err := h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
|
||||
return ips, err
|
||||
return h.resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, uint32, error) {
|
||||
return h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) resolveRemote(host string) (net.IP, error) {
|
||||
return resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, error) {
|
||||
addrs, err := h.tnet.LookupHost(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return h.resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, uint32, error) {
|
||||
if h.local {
|
||||
return h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
|
||||
}
|
||||
ips := make([]net.IP, 0, len(addrs))
|
||||
for _, addr := range addrs {
|
||||
ips = append(ips, net.ParseIP(addr))
|
||||
}
|
||||
return ips, nil
|
||||
return h.tnet.LookupHost(host)
|
||||
})
|
||||
}
|
||||
|
||||
func resolveDomain(host string, strategy DeviceConfig_DomainStrategy, lookupIP func(host string) ([]net.IP, error)) (net.IP, error) {
|
||||
func (h *Handler) resolveDomain(host string, strategy DeviceConfig_DomainStrategy, lookupIP func(host string) ([]net.IP, uint32, error)) (net.IP, error) {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
return ip, nil
|
||||
}
|
||||
ips, err := lookupIP(host)
|
||||
h.cacheMu.Lock()
|
||||
if entry, ok := h.cache[host]; ok {
|
||||
if time.Now().Before(entry.time) {
|
||||
h.cacheMu.Unlock()
|
||||
return entry.got[dice.Roll(len(entry.got))], nil
|
||||
}
|
||||
delete(h.cache, host)
|
||||
}
|
||||
h.cacheMu.Unlock()
|
||||
ips, ttl, err := lookupIP(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -409,6 +438,13 @@ func resolveDomain(host string, strategy DeviceConfig_DomainStrategy, lookupIP f
|
||||
if len(got) == 0 {
|
||||
return nil, dns.ErrEmptyResponse
|
||||
}
|
||||
entry := entry{
|
||||
got: got,
|
||||
time: time.Now().Add(time.Duration(ttl) * time.Second),
|
||||
}
|
||||
h.cacheMu.Lock()
|
||||
h.cache[host] = entry
|
||||
h.cacheMu.Unlock()
|
||||
return got[dice.Roll(len(got))], nil
|
||||
}
|
||||
|
||||
|
||||
@@ -164,6 +164,7 @@ type DeviceConfig struct {
|
||||
DomainStrategy DeviceConfig_DomainStrategy `protobuf:"varint,7,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.proxy.wireguard.DeviceConfig_DomainStrategy" json:"domain_strategy,omitempty"`
|
||||
IsClient bool `protobuf:"varint,8,opt,name=is_client,json=isClient,proto3" json:"is_client,omitempty"`
|
||||
NoKernelTun bool `protobuf:"varint,9,opt,name=no_kernel_tun,json=noKernelTun,proto3" json:"no_kernel_tun,omitempty"`
|
||||
DNS []string `protobuf:"bytes,10,rep,name=DNS,proto3" json:"DNS,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -261,6 +262,13 @@ func (x *DeviceConfig) GetNoKernelTun() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *DeviceConfig) GetDNS() []string {
|
||||
if x != nil {
|
||||
return x.DNS
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_proxy_wireguard_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_proxy_wireguard_config_proto_rawDesc = "" +
|
||||
@@ -275,7 +283,7 @@ const file_proxy_wireguard_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"keep_alive\x18\x04 \x01(\tR\tkeepAlive\x12\x1f\n" +
|
||||
"\vallowed_ips\x18\x05 \x03(\tR\n" +
|
||||
"allowedIps\"\xdc\x03\n" +
|
||||
"allowedIps\"\xee\x03\n" +
|
||||
"\fDeviceConfig\x12\x1d\n" +
|
||||
"\n" +
|
||||
"secret_key\x18\x01 \x01(\tR\tsecretKey\x12\x1a\n" +
|
||||
@@ -286,7 +294,9 @@ const file_proxy_wireguard_config_proto_rawDesc = "" +
|
||||
"\breserved\x18\x06 \x01(\fR\breserved\x12Z\n" +
|
||||
"\x0fdomain_strategy\x18\a \x01(\x0e21.xray.proxy.wireguard.DeviceConfig.DomainStrategyR\x0edomainStrategy\x12\x1b\n" +
|
||||
"\tis_client\x18\b \x01(\bR\bisClient\x12\"\n" +
|
||||
"\rno_kernel_tun\x18\t \x01(\bR\vnoKernelTun\"\\\n" +
|
||||
"\rno_kernel_tun\x18\t \x01(\bR\vnoKernelTun\x12\x10\n" +
|
||||
"\x03DNS\x18\n" +
|
||||
" \x03(\tR\x03DNS\"\\\n" +
|
||||
"\x0eDomainStrategy\x12\f\n" +
|
||||
"\bFORCE_IP\x10\x00\x12\r\n" +
|
||||
"\tFORCE_IP4\x10\x01\x12\r\n" +
|
||||
|
||||
@@ -34,4 +34,5 @@ message DeviceConfig {
|
||||
DomainStrategy domain_strategy = 7;
|
||||
bool is_client = 8;
|
||||
bool no_kernel_tun = 9;
|
||||
repeated string DNS = 10;
|
||||
}
|
||||
@@ -248,7 +248,7 @@ var (
|
||||
errTimeout = errors.New("i/o timeout")
|
||||
)
|
||||
|
||||
func (net *Net) LookupHost(host string) (addrs []string, err error) {
|
||||
func (net *Net) LookupHost(host string) (addrs []net.IP, ttl uint32, err error) {
|
||||
return net.LookupContextHost(context.Background(), host)
|
||||
}
|
||||
|
||||
@@ -567,9 +567,9 @@ func (tnet *Net) tryOneName(ctx context.Context, name string, qtype dnsmessage.T
|
||||
return dnsmessage.Parser{}, "", lastErr
|
||||
}
|
||||
|
||||
func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string, error) {
|
||||
func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]net.IP, uint32, error) {
|
||||
if host == "" || (!tnet.hasV6 && !tnet.hasV4) {
|
||||
return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
|
||||
return nil, 0, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
|
||||
}
|
||||
zlen := len(host)
|
||||
if strings.IndexByte(host, ':') != -1 {
|
||||
@@ -578,11 +578,11 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string,
|
||||
}
|
||||
}
|
||||
if ip, err := netip.ParseAddr(host[:zlen]); err == nil {
|
||||
return []string{ip.String()}, nil
|
||||
return []net.IP{ip.AsSlice()}, 0, nil
|
||||
}
|
||||
|
||||
if !isDomainName(host) {
|
||||
return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
|
||||
return nil, 0, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
|
||||
}
|
||||
type result struct {
|
||||
p dnsmessage.Parser
|
||||
@@ -611,6 +611,7 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string,
|
||||
lane <- result{p, server, err}
|
||||
}()
|
||||
}
|
||||
ttl := uint32(300)
|
||||
for l := 0; l < lanes; l++ {
|
||||
result := <-lane
|
||||
if result.error != nil {
|
||||
@@ -644,6 +645,7 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string,
|
||||
}
|
||||
break loop
|
||||
}
|
||||
ttl = min(ttl, h.TTL)
|
||||
addrsV4 = append(addrsV4, netip.AddrFrom4(a.A))
|
||||
|
||||
case dnsmessage.TypeAAAA:
|
||||
@@ -656,6 +658,7 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string,
|
||||
}
|
||||
break loop
|
||||
}
|
||||
ttl = min(ttl, h.TTL)
|
||||
addrsV6 = append(addrsV6, netip.AddrFrom16(aaaa.AAAA))
|
||||
|
||||
default:
|
||||
@@ -680,11 +683,11 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string,
|
||||
}
|
||||
|
||||
if len(addrs) == 0 && lastErr != nil {
|
||||
return nil, lastErr
|
||||
return nil, 0, lastErr
|
||||
}
|
||||
saddrs := make([]string, 0, len(addrs))
|
||||
ips := make([]net.IP, 0, len(addrs))
|
||||
for _, ip := range addrs {
|
||||
saddrs = append(saddrs, ip.String())
|
||||
ips = append(ips, ip.AsSlice())
|
||||
}
|
||||
return saddrs, nil
|
||||
return ips, ttl, nil
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) {
|
||||
if len(tag) > 0 && p.ForSystem().Stats.InboundUplink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "inbound>>>" + tag + ">>>traffic>>>uplink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
uplinkCounter = c
|
||||
}
|
||||
@@ -74,7 +74,7 @@ func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) {
|
||||
if len(tag) > 0 && p.ForSystem().Stats.InboundDownlink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "inbound>>>" + tag + ">>>traffic>>>downlink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
downlinkCounter = c
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user