Compare commits

...
Author SHA1 Message Date
8d0d2bbd4f fix: format transport pipe constructor
Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-09-07 22:43:21 +00:00
copilot-swe-agent[bot]andGitHub 6539270b98 Initial plan 2026-09-07 22:41:39 +00:00
eef6e63bc1 XHTTP client: Fix a data race in WaitReadCloser (#6694)
https://github.com/XTLS/Xray-core/pull/6694#issuecomment-5575759500

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-09-07 22:38:16 +00:00
Hossin AsaadiandGitHub 6ce8dc53e7 common/buf/writer.go: Fix buffered writes failing when payload exceeds remaining capacity (#6723)
Fixes https://github.com/XTLS/Xray-core/issues/5287
2026-09-07 21:14:41 +00:00
01a034be53 Blackhole outbound: Add customizable response data support (#6713)
https://github.com/XTLS/Xray-core/pull/6711#issuecomment-5508683394

---------

Co-authored-by: Raaad1on <djilyawhite@gmail.com>
2026-09-07 21:09:58 +00:00
XXcipherXandGitHub de2caf3cef Hysteria server: Fix Unix masquerade socket path (#6705)
https://github.com/XTLS/Xray-core/pull/6705#issuecomment-5489788730
2026-09-07 21:01:45 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
cecc88f43c Bump google.golang.org/grpc from 1.83.1 to 1.83.2 (#6704)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.83.1 to 1.83.2.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.83.1...v1.83.2)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.83.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-07 20:42:22 +00:00
pexcnandGitHub cd4ce973e9 WebSocket client: Avoid panic before real dialing in delayDialConn (#6544)
Fixes https://github.com/teddysun/xray-plugin-android/issues/11
2026-09-01 04:57:10 +00:00
patternihaandGitHub fc7b980636 Sockopt: Fix indentation of the CustomSockopt block for Linux (#6568)
https://github.com/XTLS/Xray-core/pull/6568#issuecomment-5488145252

Fixes https://github.com/XTLS/Xray-core/issues/6564
Fixes https://github.com/XTLS/Xray-core/issues/6602
2026-09-01 04:47:55 +00:00
YounesandRPRX 8ee131cbbb HTTP inbound: Fix a potential panic in readResponseAndHandle100Continue() (#6545) 2026-09-01 02:59:56 +03:00
Jorik_SmithandGitHub 2776ea6d74 SS2022 outbound: Close connection to server on early returns (#6698)
https://github.com/XTLS/Xray-core/pull/6698#issuecomment-5468662565
2026-08-31 23:59:04 +00:00
meanwhile131andGitHub 5e245b082e Routing: Export HealthCheckSettings (#6680) 2026-08-30 12:19:54 +00:00
d9c54026c5 Sniffing: Support QUICv2 (#6695)
Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-08-30 12:15:25 +00:00
brookwkoandGitHub c1958dba04 TUN inbound: Support autoSystemRoutingTable and autoOutboundsInterface on FreeBSD as well (#6691)
https://github.com/XTLS/Xray-core/pull/6691#issuecomment-5445818753

Closes https://github.com/XTLS/Xray-core/issues/5594#issuecomment-4789101025
2026-08-27 22:32:45 +00:00
540b9070f5 Transport: Bind the UDP outbound socket in the destination family (#6688)
https://github.com/XTLS/Xray-core/pull/6688#issuecomment-5445559197

Completes https://github.com/XTLS/Xray-core/pull/6624#issuecomment-5430488664

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-08-27 21:46:26 +00:00
28 changed files with 1299 additions and 359 deletions
+3 -1
View File
@@ -118,7 +118,9 @@ func (w *BufferedWriter) Write(b []byte) (int, error) {
nBytes, err := w.buffer.Write(b)
totalBytes += nBytes
if err != nil {
// ErrBufferFull means a partial write, so flush below and continue
if err != nil && err != ErrBufferFull {
return totalBytes, err
}
if !w.buffered || w.buffer.IsFull() {
-8
View File
@@ -1,18 +1,10 @@
package quic
import (
"crypto"
"crypto/cipher"
_ "crypto/tls"
_ "unsafe"
)
type CipherSuiteTLS13 struct {
ID uint16
KeyLen int
AEAD func(key, fixedNonce []byte) cipher.AEAD
Hash crypto.Hash
}
//go:linkname AEADAESGCMTLS13 crypto/tls.aeadAESGCMTLS13
func AEADAESGCMTLS13(key, nonceMask []byte) cipher.AEAD
+89 -74
View File
@@ -3,7 +3,6 @@ package quic
import (
"crypto"
"crypto/aes"
"crypto/tls"
"encoding/binary"
"io"
@@ -28,22 +27,43 @@ func (s SniffHeader) Domain() string {
return s.domain
}
const (
versionDraft29 uint32 = 0xff00001d
version1 uint32 = 0x1
var (
errNotQUIC = errors.New("not quic")
errNotQUICInitial = errors.New("not initial packet")
)
type quicVersionSpec struct {
ver uint32
typeInitial byte
initialSalt []byte
labelPrefix string
}
var (
quicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99}
quicSalt = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a}
initialSuite = &CipherSuiteTLS13{
ID: tls.TLS_AES_128_GCM_SHA256,
KeyLen: 16,
AEAD: AEADAESGCMTLS13,
Hash: crypto.SHA256,
quicDraft29 = quicVersionSpec{
ver: 0xff00001d,
typeInitial: 0b00,
initialSalt: []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99},
labelPrefix: "quic",
}
quicV1 = quicVersionSpec{
ver: 0x1,
typeInitial: 0b00,
initialSalt: []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a},
labelPrefix: "quic",
}
quicV2 = quicVersionSpec{
ver: 0x6b3343cf,
typeInitial: 0b01,
initialSalt: []byte{0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, 0xf9, 0xbd, 0x2e, 0xd9},
labelPrefix: "quicv2",
}
quicVersionSpecMap = map[uint32]*quicVersionSpec{
quicDraft29.ver: &quicDraft29,
quicV1.ver: &quicV1,
quicV2.ver: &quicV2,
}
errNotQuic = errors.New("not quic")
errNotQuicInitial = errors.New("not initial packet")
)
func SniffQUIC(b []byte) (*SniffHeader, error) {
@@ -63,60 +83,61 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
buffer := buf.FromBytes(b)
typeByte, err := buffer.ReadByte()
if err != nil {
return nil, errNotQuic
return nil, errNotQUIC
}
isLongHeader := typeByte&0x80 > 0
if !isLongHeader || typeByte&0x40 == 0 {
return nil, errNotQuicInitial
return nil, errNotQUICInitial
}
vb, err := buffer.ReadBytes(4)
if err != nil {
return nil, errNotQuic
return nil, errNotQUIC
}
versionNumber := binary.BigEndian.Uint32(vb)
if versionNumber != 0 && typeByte&0x40 == 0 {
return nil, errNotQuic
} else if versionNumber != versionDraft29 && versionNumber != version1 {
return nil, errNotQuic
var s *quicVersionSpec
if v, ok := quicVersionSpecMap[versionNumber]; ok {
s = v
} else {
return nil, errNotQUIC
}
packetType := (typeByte & 0x30) >> 4
isQuicInitial := packetType == 0x0
var destConnID []byte
if l, err := buffer.ReadByte(); err != nil {
return nil, errNotQuic
return nil, errNotQUIC
} else if destConnID, err = buffer.ReadBytes(int32(l)); err != nil {
return nil, errNotQuic
return nil, errNotQUIC
}
if l, err := buffer.ReadByte(); err != nil {
return nil, errNotQuic
return nil, errNotQUIC
} else if common.Error2(buffer.ReadBytes(int32(l))) != nil {
return nil, errNotQuic
return nil, errNotQUIC
}
if isQuicInitial { // Only initial packets have token, see https://datatracker.ietf.org/doc/html/rfc9000#section-17.2.2
tokenLen, err := readShortQuicVarint(buffer)
packetType := (typeByte & 0x30) >> 4
isQUICInitial := packetType == s.typeInitial
if isQUICInitial { // Only initial packets have token, see https://datatracker.ietf.org/doc/html/rfc9000#section-17.2.2
tokenLen, err := readShortQUICVarint(buffer)
if err != nil || tokenLen > int32(len(b)) {
return nil, errNotQuic
return nil, errNotQUIC
}
if _, err = buffer.ReadBytes(tokenLen); err != nil {
return nil, errNotQuic
return nil, errNotQUIC
}
}
packetLen, err := readShortQuicVarint(buffer)
packetLen, err := readShortQUICVarint(buffer)
if err != nil {
return nil, errNotQuic
return nil, errNotQUIC
}
// packetLen is impossible to be shorter than this
if packetLen < 4 {
return nil, errNotQuic
return nil, errNotQUIC
}
hdrLen := len(b) - int(buffer.Len())
@@ -125,26 +146,22 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
}
restPayload := b[hdrLen+int(packetLen):]
if !isQuicInitial { // Skip this packet if it's not initial packet
if !isQUICInitial { // Skip this packet if it's not initial packet
b = restPayload
continue
}
var salt []byte
if versionNumber == version1 {
salt = quicSalt
} else {
salt = quicSaltOld
}
salt := s.initialSalt
label := s.labelPrefix
initialSecret := hkdf.Extract(crypto.SHA256.New, destConnID, salt)
secret := hkdfExpandLabel(crypto.SHA256, initialSecret, []byte{}, "client in", crypto.SHA256.Size())
hpKey := hkdfExpandLabel(initialSuite.Hash, secret, []byte{}, "quic hp", initialSuite.KeyLen)
secret := hkdfExpandLabel(initialSecret, "client in", crypto.SHA256.Size())
hpKey := hkdfExpandLabel(secret, label+" hp", 16)
block, err := aes.NewCipher(hpKey)
if err != nil {
return nil, err
}
if len(b) < hdrLen+4+block.BlockSize() {
return nil, errNotQuic
return nil, errNotQUIC
}
cache.Clear()
mask := cache.Extend(int32(block.BlockSize()))
@@ -155,8 +172,8 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
b[hdrLen+i] ^= mask[i+1]
}
key := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic key", 16)
iv := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12)
key := hkdfExpandLabel(secret, label+" key", 16)
iv := hkdfExpandLabel(secret, label+" iv", 12)
cipher := AEADAESGCMTLS13(key, iv)
nonce := cache.Extend(int32(cipher.NonceSize()))
@@ -181,44 +198,44 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
case 0x00: // PADDING frame
case 0x01: // PING frame
case 0x02, 0x03: // ACK frame
if _, err = readShortQuicVarint(buffer); err != nil { // Field: Largest Acknowledged
if _, err = readShortQUICVarint(buffer); err != nil { // Field: Largest Acknowledged
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Delay
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ACK Delay
return nil, io.ErrUnexpectedEOF
}
ackRangeCount, err := readShortQuicVarint(buffer) // Field: ACK Range Count
ackRangeCount, err := readShortQUICVarint(buffer) // Field: ACK Range Count
if err != nil {
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQuicVarint(buffer); err != nil { // Field: First ACK Range
if _, err = readShortQUICVarint(buffer); err != nil { // Field: First ACK Range
return nil, io.ErrUnexpectedEOF
}
for i := 0; i < int(ackRangeCount); i++ { // Field: ACK Range
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Range -> Gap
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ACK Range -> Gap
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Range -> ACK Range Length
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ACK Range -> ACK Range Length
return nil, io.ErrUnexpectedEOF
}
}
if frameType == 0x03 {
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ECN Counts -> ECT0 Count
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ECN Counts -> ECT0 Count
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ECN Counts -> ECT1 Count
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ECN Counts -> ECT1 Count
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQuicVarint(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count
if _, err = readShortQUICVarint(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count
return nil, io.ErrUnexpectedEOF
}
}
case 0x06: // CRYPTO frame, we will use this frame
offset, err := readShortQuicVarint(buffer) // Field: Offset
offset, err := readShortQUICVarint(buffer) // Field: Offset
if err != nil {
return nil, io.ErrUnexpectedEOF
}
length, err := readShortQuicVarint(buffer) // Field: Length
length, err := readShortQUICVarint(buffer) // Field: Length
if err != nil || length > buffer.Len() {
return nil, io.ErrUnexpectedEOF
}
@@ -234,13 +251,13 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
return nil, io.ErrUnexpectedEOF
}
case 0x1c: // CONNECTION_CLOSE frame, only 0x1c is permitted in initial packet
if _, err = readShortQuicVarint(buffer); err != nil { // Field: Error Code
if _, err = readShortQUICVarint(buffer); err != nil { // Field: Error Code
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQuicVarint(buffer); err != nil { // Field: Frame Type
if _, err = readShortQUICVarint(buffer); err != nil { // Field: Frame Type
return nil, io.ErrUnexpectedEOF
}
length, err := readShortQuicVarint(buffer) // Field: Reason Phrase Length
length, err := readShortQUICVarint(buffer) // Field: Reason Phrase Length
if err != nil {
return nil, io.ErrUnexpectedEOF
}
@@ -250,7 +267,7 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
default:
// Only above frame types are permitted in initial packet.
// See https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2.2-8
return nil, errNotQuicInitial
return nil, errNotQUICInitial
}
}
@@ -268,35 +285,33 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
return nil, protocol.ErrProtoNeedMoreData
}
func hkdfExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte {
b := make([]byte, 3, 3+6+len(label)+1+len(context))
binary.BigEndian.PutUint16(b, uint16(length))
b[2] = uint8(6 + len(label))
b = append(b, []byte("tls13 ")...)
b = append(b, []byte(label)...)
b = b[:3+6+len(label)+1]
b[3+6+len(label)] = uint8(len(context))
b = append(b, context...)
func hkdfExpandLabel(secret []byte, label string, length int) []byte {
b := make([]byte, 0, 2+1+6+len(label)+1)
b = binary.BigEndian.AppendUint16(b, uint16(length))
b = append(b, byte(6+len(label)))
b = append(b, "tls13 "...)
b = append(b, label...)
b = append(b, 0) // context
out := make([]byte, length)
n, err := hkdf.Expand(hash.New, secret, b).Read(out)
n, err := hkdf.Expand(crypto.SHA256.New, secret, b).Read(out)
if err != nil || n != length {
panic("quic: HKDF-Expand-Label invocation failed unexpectedly")
}
return out
}
// readShortQuicVarint wraps quicvarint.Read with a max limit for length related fields.
// readShortQUICVarint wraps quicvarint.Read with a max limit for length related fields.
// we only handle QUIC Initial so these numbers should not exceed 65535
// returns int32 to reduce type conversion
func readShortQuicVarint(reader io.ByteReader) (int32, error) {
func readShortQUICVarint(reader io.ByteReader) (int32, error) {
v, err := quicvarint.Read(reader)
if err != nil {
return 0, err
}
if v > 65535 {
// not used(
return 0, errNotQuicInitial
return 0, errNotQUICInitial
}
return int32(v), nil
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -32,7 +32,7 @@ require (
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/windows v1.0.1
google.golang.org/grpc v1.83.1
google.golang.org/grpc v1.83.2
google.golang.org/protobuf v1.36.12
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
h12.io/socks v1.0.3
+2 -2
View File
@@ -160,8 +160,8 @@ gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y=
google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+20 -30
View File
@@ -1,52 +1,42 @@
package conf
import (
"encoding/json"
"encoding/base64"
"strings"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/proxy/blackhole"
"google.golang.org/protobuf/proto"
)
type NoneResponse struct{}
func (*NoneResponse) Build() (proto.Message, error) {
return new(blackhole.NoneResponse), nil
}
type HTTPResponse struct{}
func (*HTTPResponse) Build() (proto.Message, error) {
return new(blackhole.HTTPResponse), nil
type ResponseConfig struct {
Type string `json:"type"`
CustomResponseData string `json:"customResponseData"`
}
type BlackholeConfig struct {
Response json.RawMessage `json:"response"`
Response *ResponseConfig `json:"response"`
}
func (v *BlackholeConfig) Build() (proto.Message, error) {
config := new(blackhole.Config)
if v.Response != nil {
response, _, err := configLoader.Load(v.Response)
if err != nil {
return nil, errors.New("Config: Failed to parse Blackhole response config.").Base(err)
responseName := strings.ToLower(v.Response.Type)
switch responseName {
case "none", "":
config.Response = &blackhole.Response{Type: "none"}
case "http":
config.Response = &blackhole.Response{Type: "http"}
case "custom":
data, err := base64.StdEncoding.DecodeString(v.Response.CustomResponseData)
if err != nil {
return nil, errors.New("failed to decode custom response data: " + err.Error())
}
config.Response = &blackhole.Response{Type: "custom", CustomResponseData: data}
default:
return nil, errors.New("unknown blackhole response: " + responseName)
}
responseSettings, err := response.(Buildable).Build()
if err != nil {
return nil, err
}
config.Response = serial.ToTypedMessage(responseSettings)
}
return config, nil
}
var configLoader = NewJSONConfigLoader(
ConfigCreatorCache{
"none": func() interface{} { return new(NoneResponse) },
"http": func() interface{} { return new(HTTPResponse) },
},
"type",
"",
)
+25 -2
View File
@@ -3,7 +3,6 @@ package conf_test
import (
"testing"
"github.com/xtls/xray-core/common/serial"
. "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/proxy/blackhole"
)
@@ -22,7 +21,7 @@ func TestHTTPResponseJSON(t *testing.T) {
}`,
Parser: loadJSON(creator),
Output: &blackhole.Config{
Response: serial.ToTypedMessage(&blackhole.HTTPResponse{}),
Response: &blackhole.Response{Type: "http"},
},
},
{
@@ -32,3 +31,27 @@ func TestHTTPResponseJSON(t *testing.T) {
},
})
}
func TestCustomResponseJSON(t *testing.T) {
creator := func() Buildable {
return new(BlackholeConfig)
}
runMultiTestCase(t, []TestCase{
{
Input: `{
"response": {
"type": "custom",
"customResponseData": "Y3VzdG9tIHJlc3BvbnNl"
}
}`,
Parser: loadJSON(creator),
Output: &blackhole.Config{
Response: &blackhole.Response{
Type: "custom",
CustomResponseData: []byte("custom response"),
},
},
},
})
}
+1 -1
View File
@@ -23,7 +23,7 @@ func (o *ObservatoryConfig) Build() (proto.Message, error) {
type BurstObservatoryConfig struct {
SubjectSelector []string `json:"subjectSelector"`
// health check settings
HealthCheck *healthCheckSettings `json:"pingConfig,omitempty"`
HealthCheck *HealthCheckSettings `json:"pingConfig,omitempty"`
}
func (b BurstObservatoryConfig) Build() (proto.Message, error) {
+3 -3
View File
@@ -43,8 +43,8 @@ type strategyLeastLoadConfig struct {
Tolerance float64 `json:"tolerance,omitempty"`
}
// healthCheckSettings holds settings for health Checker
type healthCheckSettings struct {
// HealthCheckSettings holds settings for health Checker
type HealthCheckSettings struct {
Destination string `json:"destination"`
Connectivity string `json:"connectivity"`
Interval duration.Duration `json:"interval"`
@@ -53,7 +53,7 @@ type healthCheckSettings struct {
HttpMethod string `json:"httpMethod"`
}
func (h healthCheckSettings) Build() (proto.Message, error) {
func (h HealthCheckSettings) Build() (proto.Message, error) {
var httpMethod string
if h.HttpMethod == "" {
httpMethod = "HEAD"
+2 -2
View File
@@ -10,7 +10,7 @@ import (
)
type CustomSockoptConfig struct {
Syetem string `json:"system"`
System string `json:"system"`
Network string `json:"network"`
Level string `json:"level"`
Opt string `json:"opt"`
@@ -124,7 +124,7 @@ func (c *SocketConfig) Build() (*internet.SocketConfig, error) {
for _, copt := range c.CustomSockopt {
customSockopt := &internet.CustomSockopt{
System: copt.Syetem,
System: copt.System,
Network: copt.Network,
Level: copt.Level,
Opt: copt.Opt,
+31 -6
View File
@@ -2,12 +2,15 @@
package blackhole
import (
"bytes"
"context"
"net/http"
"time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/dice"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/signal"
@@ -17,14 +20,34 @@ import (
// Handler is an outbound connection that silently swallow the entire payload.
type Handler struct {
response ResponseConfig
response []byte
}
var http403response = http.Response{
StatusCode: 403,
ProtoMajor: 1,
ProtoMinor: 1,
Header: http.Header{
"Connection": {"close"},
"Cache-Control": {"max-age=3600, public"},
},
}
// New creates a new blackhole handler.
func New(ctx context.Context, config *Config) (*Handler, error) {
response, err := config.GetInternalResponse()
if err != nil {
return nil, err
response := []byte{}
if config.Response != nil {
switch config.Response.Type {
case "", "none":
case "http":
var data bytes.Buffer
common.Must(http403response.Write(&data))
response = data.Bytes()
case "custom":
response = config.Response.CustomResponseData
default:
return nil, errors.New("unknown blackhole response: " + config.Response.Type)
}
}
return &Handler{
response: response,
@@ -37,8 +60,10 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
ob := outbounds[len(outbounds)-1]
ob.Name = "blackhole"
nBytes := h.response.WriteTo(link.Writer)
if nBytes > 0 {
if len(h.response) > 0 {
mbc := buf.MultiBufferContainer{}
common.Must2(mbc.Write(h.response))
link.Writer.WriteMultiBuffer(mbc.MultiBuffer)
// Sleep a little here to make sure the response is sent to client.
time.Sleep(time.Second)
}
+45 -11
View File
@@ -1,12 +1,15 @@
package blackhole_test
import (
"bufio"
"bytes"
"context"
"crypto/rand"
"net/http"
"testing"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/proxy/blackhole"
"github.com/xtls/xray-core/transport"
@@ -16,27 +19,58 @@ import (
func TestBlackholeHTTPResponse(t *testing.T) {
ctx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{}})
handler, err := blackhole.New(ctx, &blackhole.Config{
Response: serial.ToTypedMessage(&blackhole.HTTPResponse{}),
Response: &blackhole.Response{Type: "http"},
})
common.Must(err)
reader, writer := pipe.New(pipe.WithoutSizeLimit())
var mb buf.MultiBuffer
var rerr error
dataCh := make(chan buf.MultiBuffer, 1)
go func() {
b, e := reader.ReadMultiBuffer()
mb = b
rerr = e
mb := common.Must2(reader.ReadMultiBuffer())
dataCh <- mb
}()
link := transport.Link{
Reader: reader,
Writer: writer,
}
common.Must(handler.Process(ctx, &link, nil))
common.Must(rerr)
if mb.IsEmpty() {
t.Error("expect http response, but nothing")
mb := <-dataCh
data := make([]byte, mb.Len())
mb.Copy(data)
resp := common.Must2(http.ReadResponse(bufio.NewReader(bytes.NewBuffer(data)), nil))
if resp.StatusCode != 403 {
t.Errorf("expected 403 response, got %d", resp.StatusCode)
}
}
func TestBlackholeCustomResponse(t *testing.T) {
ctx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{}})
// slightly bigger than a buffer
expected := make([]byte, buf.Size+1000)
if _, err := rand.Read(expected); err != nil {
t.Fatal(err)
}
handler, err := blackhole.New(ctx, &blackhole.Config{
Response: &blackhole.Response{
Type: "custom",
CustomResponseData: expected,
},
})
common.Must(err)
reader, writer := pipe.New(pipe.WithoutSizeLimit())
var actual buf.MultiBuffer
var rerr error
go func() {
actual, rerr = reader.ReadMultiBuffer()
}()
link := transport.Link{Reader: reader, Writer: writer}
common.Must(handler.Process(ctx, &link, nil))
common.Must(rerr)
if actual.String() != string(expected) {
t.Errorf("custom response mismatch")
}
}
-47
View File
@@ -1,47 +0,0 @@
package blackhole
import (
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
)
const (
http403response = `HTTP/1.1 403 Forbidden
Connection: close
Cache-Control: max-age=3600, public
Content-Length: 0
`
)
// ResponseConfig is the configuration for blackhole responses.
type ResponseConfig interface {
// WriteTo writes a predefined response to the specified buffer.
WriteTo(buf.Writer) int32
}
// WriteTo implements ResponseConfig.WriteTo().
func (*NoneResponse) WriteTo(buf.Writer) int32 { return 0 }
// WriteTo implements ResponseConfig.WriteTo().
func (*HTTPResponse) WriteTo(writer buf.Writer) int32 {
b := buf.New()
common.Must2(b.WriteString(http403response))
n := b.Len()
writer.WriteMultiBuffer(buf.MultiBuffer{b})
return n
}
// GetInternalResponse converts response settings from proto to internal data structure.
func (c *Config) GetInternalResponse() (ResponseConfig, error) {
if c.GetResponse() == nil {
return new(NoneResponse), nil
}
config, err := c.GetResponse().GetInstance()
if err != nil {
return nil, err
}
return config.(ResponseConfig), nil
}
+37 -59
View File
@@ -7,7 +7,6 @@
package blackhole
import (
serial "github.com/xtls/xray-core/common/serial"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
@@ -22,26 +21,28 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type NoneResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
type Response struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
CustomResponseData []byte `protobuf:"bytes,2,opt,name=custom_response_data,json=customResponseData,proto3" json:"custom_response_data,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *NoneResponse) Reset() {
*x = NoneResponse{}
func (x *Response) Reset() {
*x = Response{}
mi := &file_proxy_blackhole_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *NoneResponse) String() string {
func (x *Response) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NoneResponse) ProtoMessage() {}
func (*Response) ProtoMessage() {}
func (x *NoneResponse) ProtoReflect() protoreflect.Message {
func (x *Response) ProtoReflect() protoreflect.Message {
mi := &file_proxy_blackhole_config_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -53,57 +54,35 @@ func (x *NoneResponse) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use NoneResponse.ProtoReflect.Descriptor instead.
func (*NoneResponse) Descriptor() ([]byte, []int) {
// Deprecated: Use Response.ProtoReflect.Descriptor instead.
func (*Response) Descriptor() ([]byte, []int) {
return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{0}
}
type HTTPResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *HTTPResponse) Reset() {
*x = HTTPResponse{}
mi := &file_proxy_blackhole_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *HTTPResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HTTPResponse) ProtoMessage() {}
func (x *HTTPResponse) ProtoReflect() protoreflect.Message {
mi := &file_proxy_blackhole_config_proto_msgTypes[1]
func (x *Response) GetType() string {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
return x.Type
}
return mi.MessageOf(x)
return ""
}
// Deprecated: Use HTTPResponse.ProtoReflect.Descriptor instead.
func (*HTTPResponse) Descriptor() ([]byte, []int) {
return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{1}
func (x *Response) GetCustomResponseData() []byte {
if x != nil {
return x.CustomResponseData
}
return nil
}
type Config struct {
state protoimpl.MessageState `protogen:"open.v1"`
Response *serial.TypedMessage `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"`
Response *Response `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Config) Reset() {
*x = Config{}
mi := &file_proxy_blackhole_config_proto_msgTypes[2]
mi := &file_proxy_blackhole_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -115,7 +94,7 @@ func (x *Config) String() string {
func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_proxy_blackhole_config_proto_msgTypes[2]
mi := &file_proxy_blackhole_config_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -128,10 +107,10 @@ func (x *Config) ProtoReflect() protoreflect.Message {
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) {
return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{2}
return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{1}
}
func (x *Config) GetResponse() *serial.TypedMessage {
func (x *Config) GetResponse() *Response {
if x != nil {
return x.Response
}
@@ -142,11 +121,12 @@ var File_proxy_blackhole_config_proto protoreflect.FileDescriptor
const file_proxy_blackhole_config_proto_rawDesc = "" +
"\n" +
"\x1cproxy/blackhole/config.proto\x12\x14xray.proxy.blackhole\x1a!common/serial/typed_message.proto\"\x0e\n" +
"\fNoneResponse\"\x0e\n" +
"\fHTTPResponse\"F\n" +
"\x06Config\x12<\n" +
"\bresponse\x18\x01 \x01(\v2 .xray.common.serial.TypedMessageR\bresponseB^\n" +
"\x1cproxy/blackhole/config.proto\x12\x14xray.proxy.blackhole\"P\n" +
"\bResponse\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x120\n" +
"\x14custom_response_data\x18\x02 \x01(\fR\x12customResponseData\"D\n" +
"\x06Config\x12:\n" +
"\bresponse\x18\x01 \x01(\v2\x1e.xray.proxy.blackhole.ResponseR\bresponseB^\n" +
"\x18com.xray.proxy.blackholeP\x01Z)github.com/xtls/xray-core/proxy/blackhole\xaa\x02\x14Xray.Proxy.Blackholeb\x06proto3"
var (
@@ -161,15 +141,13 @@ func file_proxy_blackhole_config_proto_rawDescGZIP() []byte {
return file_proxy_blackhole_config_proto_rawDescData
}
var file_proxy_blackhole_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_proxy_blackhole_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_proxy_blackhole_config_proto_goTypes = []any{
(*NoneResponse)(nil), // 0: xray.proxy.blackhole.NoneResponse
(*HTTPResponse)(nil), // 1: xray.proxy.blackhole.HTTPResponse
(*Config)(nil), // 2: xray.proxy.blackhole.Config
(*serial.TypedMessage)(nil), // 3: xray.common.serial.TypedMessage
(*Response)(nil), // 0: xray.proxy.blackhole.Response
(*Config)(nil), // 1: xray.proxy.blackhole.Config
}
var file_proxy_blackhole_config_proto_depIdxs = []int32{
3, // 0: xray.proxy.blackhole.Config.response:type_name -> xray.common.serial.TypedMessage
0, // 0: xray.proxy.blackhole.Config.response:type_name -> xray.proxy.blackhole.Response
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
@@ -188,7 +166,7 @@ func file_proxy_blackhole_config_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_blackhole_config_proto_rawDesc), len(file_proxy_blackhole_config_proto_rawDesc)),
NumEnums: 0,
NumMessages: 3,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
+5 -6
View File
@@ -6,12 +6,11 @@ option go_package = "github.com/xtls/xray-core/proxy/blackhole";
option java_package = "com.xray.proxy.blackhole";
option java_multiple_files = true;
import "common/serial/typed_message.proto";
message NoneResponse {}
message HTTPResponse {}
message Response {
string type = 1;
bytes custom_response_data = 2;
}
message Config {
xray.common.serial.TypedMessage response = 1;
Response response = 1;
}
+7 -14
View File
@@ -1,26 +1,19 @@
package blackhole_test
import (
"bufio"
"net/http"
"context"
"testing"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
. "github.com/xtls/xray-core/proxy/blackhole"
"github.com/xtls/xray-core/proxy/blackhole"
)
func TestHTTPResponse(t *testing.T) {
buffer := buf.New()
httpResponse := new(HTTPResponse)
httpResponse.WriteTo(buf.NewWriter(buffer))
reader := bufio.NewReader(buffer)
response, err := http.ReadResponse(reader, nil)
handler, err := blackhole.New(context.Background(), &blackhole.Config{
Response: &blackhole.Response{Type: "http"},
})
common.Must(err)
if response.StatusCode != 403 {
t.Error("expected status code 403, but got ", response.StatusCode)
if handler == nil {
t.Error("expected HTTP response handler")
}
}
+1 -1
View File
@@ -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 {
+49
View File
@@ -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())
}
}
+1
View File
@@ -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())
+729 -25
View File
@@ -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")
}
+150
View File
@@ -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")
}
}
+1 -1
View File
@@ -231,8 +231,8 @@ func Listen(ctx context.Context, address net.Address, port net.Port, streamSetti
transport.TLSClientConfig.InsecureSkipVerify = true
}
case "", "unix":
u = &url.URL{Scheme: "http", Host: "localhost"}
path := u.Path
u = &url.URL{Scheme: "http", Host: "localhost"}
dialer := &net.Dialer{Timeout: 30 * time.Second}
transport = transport.Clone()
transport.Proxy = nil
+34 -33
View File
@@ -169,41 +169,42 @@ func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig)
return errors.New("failed to set TCP_MAXSEG", err)
}
}
if len(config.CustomSockopt) > 0 {
for _, custom := range config.CustomSockopt {
if custom.System != "" && custom.System != runtime.GOOS {
errors.LogDebug(context.Background(), "CustomSockopt system not match: ", "want ", custom.System, " got ", runtime.GOOS)
continue
}
if len(config.CustomSockopt) > 0 {
for _, custom := range config.CustomSockopt {
if custom.System != "" && custom.System != runtime.GOOS {
errors.LogDebug(context.Background(), "CustomSockopt system not match: ", "want ", custom.System, " got ", runtime.GOOS)
continue
}
// Skip unwanted network type
// network might be tcp4 or tcp6
// use HasPrefix so that "tcp" can match tcp4/6 with "tcp" if user want to control all tcp (udp is also the same)
// if it is empty, strings.HasPrefix will always return true to make it apply for all networks
if !strings.HasPrefix(network, custom.Network) {
continue
}
level := 0x6 // default TCP
var opt int
if len(custom.Opt) == 0 {
return errors.New("No opt!")
} else {
opt, _ = strconv.Atoi(custom.Opt)
}
if custom.Level != "" {
level, _ = strconv.Atoi(custom.Level)
}
if custom.Type == "int" {
value, _ := strconv.Atoi(custom.Value)
if err := syscall.SetsockoptInt(int(fd), level, opt, value); err != nil {
return errors.New("failed to set CustomSockoptInt", opt, value, err)
}
// Skip unwanted network type
// network might be tcp4 or tcp6
// use HasPrefix so that "tcp" can match tcp4/6 with "tcp" if user want to control all tcp (udp is also the same)
// if it is empty, strings.HasPrefix will always return true to make it apply for all networks
if !strings.HasPrefix(network, custom.Network) {
continue
}
level := 0x6 // default TCP
var opt int
if len(custom.Opt) == 0 {
return errors.New("No opt!")
} else {
opt, _ = strconv.Atoi(custom.Opt)
}
if custom.Level != "" {
level, _ = strconv.Atoi(custom.Level)
}
if custom.Type == "int" {
value, _ := strconv.Atoi(custom.Value)
if err := syscall.SetsockoptInt(int(fd), level, opt, value); err != nil {
return errors.New("failed to set CustomSockoptInt", opt, value, err)
}
} else if custom.Type == "str" {
if err := syscall.SetsockoptString(int(fd), level, opt, custom.Value); err != nil {
return errors.New("failed to set CustomSockoptString", opt, custom.Value, err)
}
} else {
return errors.New("unknown CustomSockopt type:", custom.Type)
} else if custom.Type == "str" {
if err := syscall.SetsockoptString(int(fd), level, opt, custom.Value); err != nil {
return errors.New("failed to set CustomSockoptString", opt, custom.Value, err)
}
} else {
return errors.New("unknown CustomSockopt type:", custom.Type)
}
}
}
+17 -20
View File
@@ -68,7 +68,7 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, sessio
}
c.transportConfig.FillStreamRequest(req, sessionId, "")
wrc = &WaitReadCloser{Wait: make(chan struct{})}
wrc = &WaitReadCloser{wait: done.New()}
go func() {
resp, err := c.client.Do(req)
if err != nil {
@@ -188,38 +188,35 @@ func (c *DefaultDialerClient) Close() error {
}
type WaitReadCloser struct {
Wait chan struct{}
io.ReadCloser
wait *done.Instance
reader atomic.Pointer[io.ReadCloser]
}
func (w *WaitReadCloser) Set(rc io.ReadCloser) {
w.ReadCloser = rc
defer func() {
if recover() != nil {
rc.Close()
w.reader.Store(&rc)
if w.wait.Done() {
if p := w.reader.Swap(nil); p != nil {
(*p).Close()
}
}()
close(w.Wait)
}
w.wait.Close()
}
func (w *WaitReadCloser) Read(b []byte) (int, error) {
if w.ReadCloser == nil {
if <-w.Wait; w.ReadCloser == nil {
rc := w.reader.Load()
if rc == nil {
<-w.wait.Wait()
if rc = w.reader.Load(); rc == nil {
return 0, io.ErrClosedPipe
}
}
return w.ReadCloser.Read(b)
return (*rc).Read(b)
}
func (w *WaitReadCloser) Close() error {
if w.ReadCloser != nil {
return w.ReadCloser.Close()
w.wait.Close()
if p := w.reader.Swap(nil); p != nil {
return (*p).Close()
}
defer func() {
if recover() != nil && w.ReadCloser != nil {
w.ReadCloser.Close()
}
}()
close(w.Wait)
return nil
}
+14 -8
View File
@@ -50,18 +50,24 @@ func (d *DefaultSystemDialer) Dial(ctx context.Context, src net.Address, dest ne
errors.LogDebug(ctx, "dialing to "+dest.String())
if dest.Network == net.Network_UDP {
srcAddr := resolveSrcAddr(net.Network_UDP, src)
if srcAddr == nil {
srcAddr = &net.UDPAddr{
IP: []byte{0, 0, 0, 0},
Port: 0,
}
}
var lc net.ListenConfig
destAddr, err := net.ResolveUDPAddr("udp", dest.NetAddr())
if err != nil {
return nil, err
}
srcAddr := resolveSrcAddr(net.Network_UDP, src)
if srcAddr == nil {
// some OS don't support mapped IPv4 dual stack
// and need to select 0.0.0.0 or [::] manually based on the destination
wildcard := net.AnyIP.IP()
if destAddr.IP.To4() == nil {
wildcard = net.AnyIPv6.IP()
}
srcAddr = &net.UDPAddr{
IP: wildcard,
Port: 0,
}
}
var lc net.ListenConfig
lc.Control = func(network, address string, c syscall.RawConn) error {
for _, ctl := range Controllers {
if err := ctl(network, address, c); err != nil {
+19
View File
@@ -175,6 +175,25 @@ type delayDialConn struct {
streamSettings *internet.MemoryStreamConfig
}
// LocalAddr returns nil until the deferred WebSocket dial has completed.
// Without this method, Go promotes LocalAddr from the embedded net.Conn; the
// embedded interface is nil before the first Write, so the promoted call panics.
func (d *delayDialConn) LocalAddr() net.Addr {
if d.Conn == nil {
return nil
}
return d.Conn.LocalAddr()
}
// RemoteAddr returns nil until the deferred WebSocket dial has completed.
// See LocalAddr for why an explicit method is required here.
func (d *delayDialConn) RemoteAddr() net.Addr {
if d.Conn == nil {
return nil
}
return d.Conn.RemoteAddr()
}
func (d *delayDialConn) Write(b []byte) (int, error) {
if d.closed {
return 0, io.ErrClosedPipe
+4 -4
View File
@@ -63,8 +63,8 @@ func New(opts ...Option) (*Reader, *Writer) {
}
return &Reader{
pipe: p,
}, &Writer{
pipe: p,
}
pipe: p,
}, &Writer{
pipe: p,
}
}