mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-17 21:14:01 +00:00
WireGuard, MASQUE proxies: Use Xray-core's built-in lightweight TCP/IP stack instead of gVisor as well (#7156)
https://github.com/XTLS/Xray-core/pull/7130#issuecomment-6116258188 https://github.com/XTLS/Xray-core/pull/7130#issuecomment-6120742739 https://github.com/XTLS/Xray-core/pull/7130#issuecomment-6137264458 https://github.com/XTLS/Xray-core/pull/7156#issuecomment-6202933664 https://github.com/XTLS/Xray-core/pull/7156#issuecomment-6203239736
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
package wireguard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
gonet "net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
)
|
||||
|
||||
type closeHandler struct{}
|
||||
|
||||
func (closeHandler) HandleConnection(conn net.Conn, _ net.Destination) { conn.Close() }
|
||||
|
||||
// onesSum is the Internet checksum of b on top of initial, not inverted.
|
||||
func onesSum(b []byte, initial uint32) uint16 {
|
||||
sum := initial
|
||||
for ; len(b) >= 2; b = b[2:] {
|
||||
sum += uint32(binary.BigEndian.Uint16(b))
|
||||
}
|
||||
if len(b) == 1 {
|
||||
sum += uint32(b[0]) << 8
|
||||
}
|
||||
for sum>>16 != 0 {
|
||||
sum = sum>>16 + sum&0xffff
|
||||
}
|
||||
return uint16(sum)
|
||||
}
|
||||
|
||||
// The inbound answers ICMP echo requests from peers locally, the way the TUN
|
||||
// inbound does: ICMP is not proxied, but ping and connectivity checks through
|
||||
// the tunnel get a reply instead of timing out. The request goes in through
|
||||
// Write and the reply comes out of Read, as the WireGuard device uses them.
|
||||
func TestNetTUNEchoReply(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
src, dst string
|
||||
request byte
|
||||
reply byte
|
||||
}{
|
||||
{"10.66.0.2", "1.1.1.1", 8, 0},
|
||||
{"fd00::2", "2606:4700:4700::1111", 128, 129},
|
||||
} {
|
||||
dev, _, err := CreateNetTUN([]netip.Addr{netip.MustParseAddr("10.66.0.1"), netip.MustParseAddr("fd00::1")}, nil, 1420, closeHandler{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
src, dst := netip.MustParseAddr(test.src), netip.MustParseAddr(test.dst)
|
||||
payload := []byte("xray wireguard ping")
|
||||
message := append([]byte{test.request, 0, 0, 0, 0x12, 0x34, 0, 7}, payload...)
|
||||
|
||||
// The stack drops what has a wrong checksum, as what comes through a tunnel has come a long way.
|
||||
var pkt []byte
|
||||
if src.Is4() {
|
||||
pkt = make([]byte, 20, 20+len(message))
|
||||
pkt[0] = 0x45
|
||||
binary.BigEndian.PutUint16(pkt[2:], uint16(20+len(message)))
|
||||
pkt[8], pkt[9] = 64, 1
|
||||
copy(pkt[12:], src.AsSlice())
|
||||
copy(pkt[16:], dst.AsSlice())
|
||||
binary.BigEndian.PutUint16(pkt[10:], ^onesSum(pkt, 0))
|
||||
binary.BigEndian.PutUint16(message[2:], ^onesSum(message, 0))
|
||||
} else {
|
||||
pkt = make([]byte, 40, 40+len(message))
|
||||
pkt[0] = 0x60
|
||||
binary.BigEndian.PutUint16(pkt[4:], uint16(len(message)))
|
||||
pkt[6], pkt[7] = 58, 64
|
||||
copy(pkt[8:], src.AsSlice())
|
||||
copy(pkt[24:], dst.AsSlice())
|
||||
binary.BigEndian.PutUint16(message[2:], ^onesSum(message, uint32(onesSum(pkt[8:40], uint32(len(message))+58))))
|
||||
}
|
||||
pkt = append(pkt, message...)
|
||||
damaged := bytes.Clone(pkt)
|
||||
damaged[len(damaged)-1] ^= 1
|
||||
|
||||
// The stack answers inside Write, the reply waits for whoever reads the device.
|
||||
got := make(chan []byte, 1)
|
||||
const offset = 16
|
||||
go func() {
|
||||
b := make([]byte, 2048)
|
||||
sizes := make([]int, 1)
|
||||
if _, err := dev.Read([][]byte{b}, sizes, offset); err == nil {
|
||||
got <- b[offset : offset+sizes[0]]
|
||||
}
|
||||
}()
|
||||
if _, err := dev.Write([][]byte{append(make([]byte, offset), damaged...), append(make([]byte, offset), pkt...)}, offset); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var reply []byte
|
||||
select {
|
||||
case reply = <-got:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("no echo reply from the stack")
|
||||
}
|
||||
|
||||
header := len(pkt) - len(message)
|
||||
if len(reply) != len(pkt) || reply[0] != pkt[0] {
|
||||
t.Fatalf("reply of %d bytes, version byte %#x", len(reply), reply[0])
|
||||
}
|
||||
var replySrc, replyDst netip.Addr
|
||||
pseudo := uint32(0)
|
||||
if src.Is4() {
|
||||
replySrc, replyDst = netip.AddrFrom4([4]byte(reply[12:16])), netip.AddrFrom4([4]byte(reply[16:20]))
|
||||
if onesSum(reply[:20], 0) != 0xffff || reply[9] != 1 {
|
||||
t.Fatal("bad ipv4 header in the reply")
|
||||
}
|
||||
} else {
|
||||
replySrc, replyDst = netip.AddrFrom16([16]byte(reply[8:24])), netip.AddrFrom16([16]byte(reply[24:40]))
|
||||
if reply[6] != 58 {
|
||||
t.Fatalf("reply protocol %d, want icmpv6", reply[6])
|
||||
}
|
||||
pseudo = uint32(onesSum(reply[8:40], uint32(len(message))+58))
|
||||
}
|
||||
if replySrc != dst || replyDst != src {
|
||||
t.Fatalf("reply addresses %v -> %v, want %v -> %v", replySrc, replyDst, dst, src)
|
||||
}
|
||||
echo := reply[header:]
|
||||
if echo[0] != test.reply || echo[1] != 0 {
|
||||
t.Fatalf("reply type %d code %d, want echo reply", echo[0], echo[1])
|
||||
}
|
||||
// A reply to the damaged request, which came first, would not have this payload.
|
||||
if !bytes.Equal(echo[4:], message[4:]) {
|
||||
t.Fatalf("reply ident, seq and payload %x, want %x", echo[4:], message[4:])
|
||||
}
|
||||
if onesSum(echo, pseudo) != 0xffff {
|
||||
t.Fatal("bad icmp checksum in the reply")
|
||||
}
|
||||
|
||||
dev.Close()
|
||||
dev.Close()
|
||||
if _, err := dev.Read([][]byte{make([]byte, 2048)}, make([]int, 1), 0); err == nil {
|
||||
t.Fatal("read from a closed device")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// testServer answers what comes through the device of a server: names over UDP port 53, except those that
|
||||
// start with "silent", and everything else with what it was sent.
|
||||
type testServer struct{}
|
||||
|
||||
func (testServer) HandleConnection(conn net.Conn, dest net.Destination) {
|
||||
defer conn.Close()
|
||||
b := make([]byte, 4096)
|
||||
for {
|
||||
n, err := conn.Read(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
answer := b[:n]
|
||||
if dest.Network == net.Network_UDP && dest.Port == 53 {
|
||||
var parser dnsmessage.Parser
|
||||
header, err := parser.Start(b[:n])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
question, err := parser.Question()
|
||||
if err != nil || strings.HasPrefix(question.Name.String(), "silent") {
|
||||
continue
|
||||
}
|
||||
builder := dnsmessage.NewBuilder(nil, dnsmessage.Header{ID: header.ID, Response: true, RecursionAvailable: true})
|
||||
builder.StartQuestions()
|
||||
builder.Question(question)
|
||||
builder.StartAnswers()
|
||||
builder.AResource(dnsmessage.ResourceHeader{Name: question.Name, Class: dnsmessage.ClassINET, TTL: 60}, dnsmessage.AResource{A: [4]byte{192, 0, 2, 1}})
|
||||
if answer, err = builder.Finish(); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if _, err := conn.Write(answer); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A client and a server, each with a stack on its device and the two devices joined as a tunnel joins them:
|
||||
// names are resolved, and TCP and UDP go through, as the outbound and the inbound use them.
|
||||
func TestNetTUNClientAndServer(t *testing.T) {
|
||||
server, _, err := CreateNetTUN([]netip.Addr{netip.MustParseAddr("10.66.0.1")}, nil, 1420, testServer{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
client, tnet, err := CreateNetTUN([]netip.Addr{netip.MustParseAddr("10.66.0.2")}, []netip.Addr{netip.MustParseAddr("10.66.0.53")}, 1420, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
// As in a tunnel, what reads a device does not wait for the other end to take the packet: a stack answers
|
||||
// inside Write, and its answer has to be read meanwhile.
|
||||
for _, pair := range [][2]tun.Device{{client, server}, {server, client}} {
|
||||
packets := make(chan []byte, 1024)
|
||||
go func() {
|
||||
defer close(packets)
|
||||
bufs, sizes := [][]byte{make([]byte, 2048)}, make([]int, 1)
|
||||
for {
|
||||
if _, err := pair[0].Read(bufs, sizes, 0); err != nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case packets <- bytes.Clone(bufs[0][:sizes[0]]):
|
||||
default:
|
||||
}
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
for packet := range packets {
|
||||
pair[1].Write([][]byte{packet}, 0)
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if addrs, err := tnet.LookupContextHost(ctx, "xray.example"); err != nil || len(addrs) != 1 || addrs[0] != "192.0.2.1" {
|
||||
t.Fatal("resolved to ", addrs, ", error: ", err)
|
||||
}
|
||||
// a server that does not answer is not waited for longer than the one who asks wants to wait
|
||||
short, cancelShort := context.WithTimeout(ctx, 200*time.Millisecond)
|
||||
start := time.Now()
|
||||
var timeout *gonet.DNSError
|
||||
if addrs, err := tnet.LookupContextHost(short, "silent.example"); !errors.As(err, &timeout) || !timeout.IsTimeout || time.Since(start) > 3*time.Second {
|
||||
t.Fatal("resolved to ", addrs, " after ", time.Since(start), ", error: ", err)
|
||||
}
|
||||
cancelShort()
|
||||
|
||||
sent := make([]byte, 1<<20)
|
||||
rand.Read(sent)
|
||||
for _, network := range []string{"tcp", "udp"} {
|
||||
conn, err := tnet.DialContext(ctx, network, "xray.example:7")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
size := len(sent)
|
||||
if network == "udp" {
|
||||
// more than the tunnel takes in one packet
|
||||
size = 3000
|
||||
}
|
||||
go conn.Write(sent[:size])
|
||||
received := make([]byte, size)
|
||||
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := io.ReadFull(conn, received)
|
||||
done <- err
|
||||
}()
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil || !bytes.Equal(received, sent[:size]) {
|
||||
t.Fatal(network, ": received something else than what was sent, error: ", err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatal(network, ": what was sent did not come back")
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// A packet of the stack is in its hands only until WritePacket returns, Read gets it as it was then.
|
||||
// A device that is closed is not waited for, also not by a stack that has something to send when it is closed.
|
||||
func TestNetTUNWritePacket(t *testing.T) {
|
||||
dev, tnet, err := CreateNetTUN([]netip.Addr{netip.MustParseAddr("10.66.0.2")}, nil, 1420, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, sizes := make([]byte, 16), make([]int, 1)
|
||||
for i := range 1000 {
|
||||
header, payload := []byte{byte(i), 1, 2, 3}, []byte{4, 5, 6, byte(i)}
|
||||
go func() {
|
||||
dev.(*netTun).WritePacket(header, payload)
|
||||
clear(header)
|
||||
clear(payload)
|
||||
}()
|
||||
if _, err := dev.Read([][]byte{b}, sizes, 0); err != nil || !bytes.Equal(b[:sizes[0]], []byte{byte(i), 1, 2, 3, 4, 5, 6, byte(i)}) {
|
||||
t.Fatal("read ", b[:sizes[0]], " of packet ", i, ", error: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// nobody reads now, and no more can wait to be read: a connection waits to send its SYN
|
||||
for range netTunQueue {
|
||||
dev.(*netTun).WritePacket([]byte{1}, nil)
|
||||
}
|
||||
dialed := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := tnet.DialContextTCPAddrPort(context.Background(), netip.MustParseAddrPort("192.0.2.1:80"))
|
||||
dialed <- err
|
||||
}()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
closed := make(chan struct{})
|
||||
go func() {
|
||||
dev.Close()
|
||||
close(closed)
|
||||
}()
|
||||
select {
|
||||
case <-closed:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("the device is not closed while a packet waits to be read")
|
||||
}
|
||||
if err := <-dialed; err == nil {
|
||||
t.Fatal("a connection through a closed device")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user