Feature: add lwIP TCP/IP stack to tun listener

This commit is contained in:
yaling888
2021-09-30 04:05:52 +08:00
parent 433d35e866
commit 862174d21b
16 changed files with 443 additions and 117 deletions

View File

@ -0,0 +1,30 @@
package commons
import (
"github.com/Dreamacro/clash/component/resolver"
D "github.com/miekg/dns"
)
func RelayDnsPacket(payload []byte) ([]byte, error) {
msg := &D.Msg{}
if err := msg.Unpack(payload); err != nil {
return nil, err
}
r, err := resolver.ServeMsg(msg)
if err != nil {
return nil, err
}
for _, ans := range r.Answer {
header := ans.Header()
if header.Class == D.ClassINET && (header.Rrtype == D.TypeA || header.Rrtype == D.TypeAAAA) {
header.Ttl = 1
}
}
r.SetRcode(msg, r.Rcode)
r.Compress = true
return r.Pack()
}

View File

@ -195,6 +195,7 @@ func (t *gvisorAdapter) AsLinkEndpoint() (result stack.LinkEndpoint, err error)
n, err := t.device.Read(packet)
if err != nil && !t.device.IsClose() {
log.Errorln("can not read from tun: %v", err)
continue
}
var p tcpip.NetworkProtocolNumber
switch header.IPVersion(packet) {

View File

@ -0,0 +1,88 @@
package lwip
import (
"encoding/binary"
"io"
"net"
"time"
"github.com/Dreamacro/clash/component/resolver"
D "github.com/Dreamacro/clash/listener/tun/ipstack/commons"
"github.com/Dreamacro/clash/log"
"github.com/yaling888/go-lwip"
)
const defaultDnsReadTimeout = time.Second * 30
func shouldHijackDns(dnsIP net.IP, targetIp net.IP, targetPort int) bool {
if targetPort != 53 {
return false
}
return dnsIP.Equal(net.IPv4zero) || dnsIP.Equal(targetIp)
}
func hijackUDPDns(conn golwip.UDPConn, pkt []byte, addr *net.UDPAddr) {
go func() {
defer func(conn golwip.UDPConn) {
_ = conn.Close()
}(conn)
answer, err := D.RelayDnsPacket(pkt)
if err != nil {
return
}
_, _ = conn.WriteFrom(answer, addr)
}()
}
func hijackTCPDns(conn net.Conn) {
go func() {
defer func(conn net.Conn) {
_ = conn.Close()
}(conn)
for {
if err := conn.SetDeadline(time.Now().Add(defaultDnsReadTimeout)); err != nil {
return
}
var length uint16
if binary.Read(conn, binary.BigEndian, &length) != nil {
return
}
data := make([]byte, length)
_, err := io.ReadFull(conn, data)
if err != nil {
return
}
rb, err := D.RelayDnsPacket(data)
if err != nil {
continue
}
if binary.Write(conn, binary.BigEndian, uint16(len(rb))) != nil {
return
}
if _, err := conn.Write(rb); err != nil {
return
}
}
}()
}
type dnsHandler struct {
}
func NewDnsHandler() golwip.DnsHandler {
return &dnsHandler{}
}
func (d dnsHandler) ResolveIP(host string) (net.IP, error) {
log.Debugln("[TUN] lwip resolve ip for host: %s", host)
return resolver.ResolveIP(host)
}

View File

@ -0,0 +1,65 @@
package lwip
import (
"net"
"strconv"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
"github.com/Dreamacro/clash/log"
"github.com/yaling888/go-lwip"
)
type tcpHandler struct {
dnsIP net.IP
tcpIn chan<- C.ConnContext
}
func NewTCPHandler(dnsIP net.IP, tcpIn chan<- C.ConnContext) golwip.TCPConnHandler {
return &tcpHandler{dnsIP, tcpIn}
}
func (h *tcpHandler) Handle(conn net.Conn, target *net.TCPAddr) error {
if shouldHijackDns(h.dnsIP, target.IP, target.Port) {
hijackTCPDns(conn)
if log.Level() == log.DEBUG {
log.Debugln("[TUN] hijack dns tcp: %s:%d", target.IP.String(), target.Port)
}
return nil
}
if conn.RemoteAddr() == nil {
_ = conn.Close()
return nil
}
//if err := conn.SetDeadline(time.Now().Add(defaultDnsReadTimeout)); err != nil {
// _ = conn.Close()
// return nil
//}
src, _ := conn.LocalAddr().(*net.TCPAddr)
dst, _ := conn.RemoteAddr().(*net.TCPAddr)
addrType := C.AtypIPv4
if dst.IP.To4() == nil {
addrType = C.AtypIPv6
}
metadata := &C.Metadata{
NetWork: C.TCP,
Type: C.TUN,
SrcIP: src.IP,
DstIP: dst.IP,
SrcPort: strconv.Itoa(src.Port),
DstPort: strconv.Itoa(dst.Port),
AddrType: addrType,
Host: "",
}
go func(conn net.Conn, metadata *C.Metadata) {
h.tcpIn <- context.NewConnContext(conn, metadata)
}(conn, metadata)
return nil
}

View File

@ -0,0 +1,101 @@
package lwip
import (
"io"
"net"
"sync"
"github.com/Dreamacro/clash/adapter/inbound"
"github.com/Dreamacro/clash/config"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/listener/tun/dev"
"github.com/Dreamacro/clash/listener/tun/ipstack"
"github.com/Dreamacro/clash/log"
"github.com/yaling888/go-lwip"
)
type lwipAdapter struct {
device dev.TunDevice
lwipStack golwip.LWIPStack
lock sync.Mutex
mtu int
stackName string
dnsListen string
autoRoute bool
}
func NewAdapter(device dev.TunDevice, conf config.Tun, mtu int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) (ipstack.TunAdapter, error) {
adapter := &lwipAdapter{
device: device,
mtu: mtu,
stackName: conf.Stack,
dnsListen: conf.DNSListen,
autoRoute: conf.AutoRoute,
}
adapter.lock.Lock()
defer adapter.lock.Unlock()
//adapter.stopLocked()
dnsHost, _, err := net.SplitHostPort(conf.DNSListen)
if err != nil {
return nil, err
}
dnsIP := net.ParseIP(dnsHost)
golwip.RegisterOutputFn(func(data []byte) (int, error) {
return device.Write(data)
})
// Setup TCP/IP stack.
lwipStack := golwip.NewLWIPStack(mtu)
adapter.lwipStack = lwipStack
golwip.RegisterDnsHandler(NewDnsHandler())
golwip.RegisterTCPConnHandler(NewTCPHandler(dnsIP, tcpIn))
golwip.RegisterUDPConnHandler(NewUDPHandler(dnsIP, udpIn))
// Copy packets from tun device to lwip stack, it's the loop.
go func(lwipStack golwip.LWIPStack, device dev.TunDevice, mtu int) {
_, err := io.CopyBuffer(lwipStack.(io.Writer), device, make([]byte, mtu))
if err != nil {
log.Errorln("copying data failed: %v", err)
}
}(lwipStack, device, mtu)
return adapter, nil
}
func (l *lwipAdapter) Stack() string {
return l.stackName
}
func (l *lwipAdapter) AutoRoute() bool {
return l.autoRoute
}
func (l *lwipAdapter) DNSListen() string {
return l.dnsListen
}
func (l *lwipAdapter) Close() {
l.lock.Lock()
defer l.lock.Unlock()
l.stopLocked()
}
func (l *lwipAdapter) stopLocked() {
if l.lwipStack != nil {
l.lwipStack.Close()
}
if l.device != nil {
_ = l.device.Close()
}
l.lwipStack = nil
l.device = nil
}

View File

@ -0,0 +1,77 @@
package lwip
import (
"io"
"net"
"github.com/Dreamacro/clash/adapter/inbound"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/transport/socks5"
"github.com/yaling888/go-lwip"
)
type udpPacket struct {
source *net.UDPAddr
payload []byte
sender golwip.UDPConn
}
func (u *udpPacket) Data() []byte {
return u.payload
}
func (u *udpPacket) WriteBack(b []byte, addr net.Addr) (n int, err error) {
_, ok := addr.(*net.UDPAddr)
if !ok {
return 0, io.ErrClosedPipe
}
return u.sender.WriteFrom(b, u.source)
}
func (u *udpPacket) Drop() {
}
func (u *udpPacket) LocalAddr() net.Addr {
return u.source
}
type udpHandler struct {
dnsIP net.IP
udpIn chan<- *inbound.PacketAdapter
}
func NewUDPHandler(dnsIP net.IP, udpIn chan<- *inbound.PacketAdapter) golwip.UDPConnHandler {
return &udpHandler{dnsIP, udpIn}
}
func (h *udpHandler) Connect(conn golwip.UDPConn, target *net.UDPAddr) error {
return nil
}
func (h *udpHandler) ReceiveTo(conn golwip.UDPConn, data []byte, addr *net.UDPAddr) error {
if shouldHijackDns(h.dnsIP, addr.IP, addr.Port) {
hijackUDPDns(conn, data, addr)
if log.Level() == log.DEBUG {
log.Debugln("[TUN] hijack dns udp: %s:%d", addr.IP.String(), addr.Port)
}
return nil
}
packet := &udpPacket{
source: conn.LocalAddr(),
payload: data,
sender: conn,
}
go func(addr *net.UDPAddr, packet *udpPacket) {
select {
case h.udpIn <- inbound.NewPacket(socks5.ParseAddrToSocksAddr(addr), packet, C.TUN):
default:
}
}(addr, packet)
return nil
}

View File

@ -6,10 +6,7 @@ import (
"net"
"time"
"github.com/Dreamacro/clash/component/resolver"
D "github.com/miekg/dns"
D "github.com/Dreamacro/clash/listener/tun/ipstack/commons"
"github.com/kr328/tun2socket/binding"
"github.com/kr328/tun2socket/redirect"
)
@ -26,7 +23,7 @@ func shouldHijackDns(dnsAddr binding.Address, targetAddr binding.Address) bool {
func hijackUDPDns(pkt []byte, ep *binding.Endpoint, sender redirect.UDPSender) {
go func() {
answer, err := relayDnsPacket(pkt)
answer, err := D.RelayDnsPacket(pkt)
if err != nil {
return
@ -41,7 +38,9 @@ func hijackUDPDns(pkt []byte, ep *binding.Endpoint, sender redirect.UDPSender) {
func hijackTCPDns(conn net.Conn) {
go func() {
defer conn.Close()
defer func(conn net.Conn) {
_ = conn.Close()
}(conn)
for {
if err := conn.SetReadDeadline(time.Now().Add(defaultDnsReadTimeout)); err != nil {
@ -60,7 +59,7 @@ func hijackTCPDns(conn net.Conn) {
return
}
rb, err := relayDnsPacket(data)
rb, err := D.RelayDnsPacket(data)
if err != nil {
continue
}
@ -75,27 +74,3 @@ func hijackTCPDns(conn net.Conn) {
}
}()
}
func relayDnsPacket(payload []byte) ([]byte, error) {
msg := &D.Msg{}
if err := msg.Unpack(payload); err != nil {
return nil, err
}
r, err := resolver.ServeMsg(msg)
if err != nil {
return nil, err
}
for _, ans := range r.Answer {
header := ans.Header()
if header.Class == D.ClassINET && (header.Rrtype == D.TypeA || header.Rrtype == D.TypeAAAA) {
header.Ttl = 1
}
}
r.SetRcode(msg, r.Rcode)
r.Compress = true
return r.Pack()
}

View File

@ -22,6 +22,11 @@ func handleTCP(conn net.Conn, endpoint *binding.Endpoint, tcpIn chan<- C.ConnCon
Zone: "",
}
addrType := C.AtypIPv4
if dst.IP.To4() == nil {
addrType = C.AtypIPv6
}
metadata := &C.Metadata{
NetWork: C.TCP,
Type: C.TUN,
@ -29,7 +34,7 @@ func handleTCP(conn net.Conn, endpoint *binding.Endpoint, tcpIn chan<- C.ConnCon
DstIP: dst.IP,
SrcPort: strconv.Itoa(src.Port),
DstPort: strconv.Itoa(dst.Port),
AddrType: C.AtypIPv4,
AddrType: addrType,
Host: "",
}

View File

@ -11,6 +11,7 @@ import (
"github.com/Dreamacro/clash/listener/tun/dev"
"github.com/Dreamacro/clash/listener/tun/ipstack"
"github.com/Dreamacro/clash/listener/tun/ipstack/gvisor"
"github.com/Dreamacro/clash/listener/tun/ipstack/lwip"
"github.com/Dreamacro/clash/listener/tun/ipstack/system"
"github.com/Dreamacro/clash/log"
)
@ -33,7 +34,9 @@ func New(conf config.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.Pack
return nil, errors.New("unable to get device mtu")
}
if strings.EqualFold(stack, "system") {
if strings.EqualFold(stack, "lwip") {
tunAdapter, err = lwip.NewAdapter(device, conf, mtu, tcpIn, udpIn)
} else if strings.EqualFold(stack, "system") {
tunAdapter, err = system.NewAdapter(device, conf, mtu, tunAddress, tunAddress, func() {}, tcpIn, udpIn)
} else if strings.EqualFold(stack, "gvisor") {
tunAdapter, err = gvisor.NewAdapter(device, conf, tunAddress, tcpIn, udpIn)