Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Backend that uses outline-sdk to replace tun2socks #485

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions Android/backend/intra/device.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2023 Jigsaw Operations LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package intra

import (
"errors"
"fmt"
"log"
"net"
"os"

"github.com/Jigsaw-Code/Intra/Android/backend/intra/internal/doh"
"github.com/Jigsaw-Code/Intra/Android/backend/intra/internal/sni"
"github.com/Jigsaw-Code/outline-sdk/network"
"github.com/Jigsaw-Code/outline-sdk/network/lwip2transport"
)

// SocketProtector is a way to make certain sockets or DNS lookups bypassing the VPN connection. This is only needed
// for devices running Android versions older than Lollipop (21). Once a socket is protected, data sent through it will
// go directly to the internet, bypassing the VPN. The Android VpnService implements the protect() method.
type SocketProtector doh.Protector

type IntraDevice struct {
t2s network.IPDevice
Comment on lines +35 to +36
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you are just extending the IPDevice, you can do this, so you don't need to specify all method wrappers:

Suggested change
type IntraDevice struct {
t2s network.IPDevice
type IntraDevice struct {
network.IPDevice

protector SocketProtector
listener eventListenerAdapter

sd doh.DoHStreamDialer
pp doh.DoHPacketProxy
sni sni.TCPSNIReporter
}

func NewIntraDevice(fakeDNS, serverURL, fallbackAddrs string, protector SocketProtector, listener EventListener) (d *IntraDevice, err error) {
log.Println("[debug] initializing Intra device...")

d = &IntraDevice{
protector: protector,
listener: eventListenerAdapter{listener},
}

fakeDNSAddr, err := net.ResolveUDPAddr("udp", fakeDNS)
if err != nil {
return nil, fmt.Errorf("failed to resolve fakeDNS: %w", err)
}

dohServer, err := doh.MakeTransport(serverURL, fallbackAddrs, d.protector, d.listener)
if err != nil {
return nil, fmt.Errorf("failed to create DoH transport: %w", err)
}

d.sni = sni.MakeTCPReporter(dohServer)

d.sd, err = doh.MakeDoHStreamDialer(fakeDNSAddr.AddrPort(), dohServer, d.protector, d.listener, d.sni)
if err != nil {
return nil, fmt.Errorf("failed to create stream dialer: %w", err)
}

d.pp, err = doh.MakeDoHPacketProxy(fakeDNSAddr.AddrPort(), dohServer, d.protector, d.listener)
if err != nil {
return nil, fmt.Errorf("failed to create packet proxy: %w", err)
}

if d.t2s, err = lwip2transport.ConfigureDevice(d.sd, d.pp); err != nil {
return nil, fmt.Errorf("failed to configure lwIP stack: %w", err)
}

log.Println("[info] Intra device initialized")
return
}

func (d *IntraDevice) Close() error { return d.t2s.Close() }
func (d *IntraDevice) Read(p []byte) (int, error) { return d.t2s.Read(p) }
func (d *IntraDevice) Write(p []byte) (int, error) { return d.t2s.Write(p) }

func (d *IntraDevice) UpdateDoHServer(serverURL, fallbackAddrs string) error {
fortuna marked this conversation as resolved.
Show resolved Hide resolved
dohServer, err := doh.MakeTransport(serverURL, fallbackAddrs, d.protector, d.listener)
if err != nil {
return fmt.Errorf("failed to create DoH transport: %w", err)
}
return errors.Join(
d.pp.SetDoHTransport(dohServer),
d.pp.SetDoHTransport(dohServer),
d.sni.SetDoHTransport(dohServer))
}

func (d *IntraDevice) EnableSNIReporter(filename, suffix, country string) error {
f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0600)
if err != nil {
return err
}
return d.sni.Configure(f, suffix, country)
}
22 changes: 22 additions & 0 deletions Android/backend/intra/internal/doh/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2023 Jigsaw Operations LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package doh

import "strings"

func parseFallbackAddrs(ipList string) []string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps move this to transport.go, since it's only used there?

// use FieldsFunc instead of Split because FieldsFunc can handle empty strings
return strings.FieldsFunc(ipList, func(c rune) bool { return c == ',' })
}
196 changes: 196 additions & 0 deletions Android/backend/intra/internal/doh/packet_proxy.go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's put IP-level stuff in its own package. Perhaps name it vpn?

Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// Copyright 2023 Jigsaw Operations LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package doh

import (
"errors"
"fmt"
"log"
"net"
"net/netip"
"sync/atomic"
"time"

intraLegacy "github.com/Jigsaw-Code/outline-go-tun2socks/intra"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move the code we need to this repo instead of depending on the old code.

"github.com/Jigsaw-Code/outline-go-tun2socks/intra/protect"
"github.com/Jigsaw-Code/outline-sdk/network"
"github.com/Jigsaw-Code/outline-sdk/transport"
)

type DoHPacketProxy interface {
network.PacketProxy

SetDoHTransport(DoHTransport) error
}

type dohPacketProxy struct {
fakeDNSAddr netip.AddrPort
dohServer atomic.Pointer[DoHTransport]
proxy network.PacketProxy
listener intraLegacy.UDPListener
}

var _ DoHPacketProxy = (*dohPacketProxy)(nil)

func MakeDoHPacketProxy(fakeDNS netip.AddrPort, dohServer DoHTransport, protector Protector, listener intraLegacy.UDPListener) (DoHPacketProxy, error) {
if dohServer == nil {
return nil, errors.New("dohServer is required")
}

pl := &transport.UDPPacketListener{
ListenConfig: *protect.MakeListenConfig(protector),
Address: ":0",
}

pp, err := network.NewPacketProxyFromPacketListener(pl)
if err != nil {
return nil, fmt.Errorf("failed to create packet proxy from listener: %w", err)
}

dohpp := &dohPacketProxy{
fakeDNSAddr: fakeDNS,
proxy: pp,
listener: listener,
}
dohpp.dohServer.Store(&dohServer)

return dohpp, nil
}

// NewSession implements PacketProxy.NewSession.
func (p *dohPacketProxy) NewSession(resp network.PacketResponseReceiver) (network.PacketRequestSender, error) {
log.Println("[debug] initializing a new UDP session...")
defer log.Println("[info] New UDP session initialized")

dohResp := &dohPacketRespReceiver{
PacketResponseReceiver: resp,
stats: &udpTrafficStats{
sessionStartTime: time.Now(),
},
listener: p.listener,
}
req, err := p.proxy.NewSession(dohResp)
if err != nil {
log.Printf("[error] failed to create UDP session: %v\n", err)
return nil, fmt.Errorf("failed to create new session: %w", err)
}
return &dohPacketReqSender{
PacketRequestSender: req,
proxy: p,
response: dohResp,
stats: dohResp.stats,
}, nil
}

// SetDoHTransport implements DoHPacketProxy.SetDoHTransport.
func (p *dohPacketProxy) SetDoHTransport(dohServer DoHTransport) error {
log.Println("[debug] updating DoH server for UDP sessions")
if dohServer == nil {
return errors.New("dohServer is required")
}
p.dohServer.Store(&dohServer)
log.Println("[info] DoH server updated for UDP sessions")
return nil
}

// DoH UDP session statistics data
type udpTrafficStats struct {
sessionStartTime time.Time
downloadBytes atomic.Int64
uploadBytes atomic.Int64
}

// DoH PacketRequestSender wrapper
type dohPacketReqSender struct {
network.PacketRequestSender

response *dohPacketRespReceiver
proxy *dohPacketProxy
stats *udpTrafficStats
}

// DoH PacketResponseReceiver wrapper
type dohPacketRespReceiver struct {
network.PacketResponseReceiver

stats *udpTrafficStats
listener intraLegacy.UDPListener
}

var _ network.PacketRequestSender = (*dohPacketReqSender)(nil)
var _ network.PacketResponseReceiver = (*dohPacketRespReceiver)(nil)

// WriteTo implements PacketRequestSender.WriteTo. It will query the DoH server if the packet a DNS packet.
func (req *dohPacketReqSender) WriteTo(p []byte, destination netip.AddrPort) (int, error) {
log.Printf("[debug] Sending raw UDP packet (%v bytes) to %v\n", len(p), destination)
if destination == req.proxy.fakeDNSAddr {
defer func() {
// conn was only used for this DNS query, so it's unlikely to be used again
// see: https://github.com/Jigsaw-Code/outline-go-tun2socks/blob/master/intra/udp.go#L144C3-L144C79
if req.stats.downloadBytes.Load() == 0 && req.stats.uploadBytes.Load() == 0 {
log.Println("[debug] DoH dedicated session finished, Closing...")
req.Close()
}
}()

log.Println("[debug] Doing DNS request over DoH server...")
resp, err := (*req.proxy.dohServer.Load()).Query(p)
if err != nil {
log.Printf("[error] DoH request failed: %v\n", err)
return 0, fmt.Errorf("DoH request error: %w", err)
}
if len(resp) == 0 {
log.Println("[error] DoH response is empty")
return 0, errors.New("empty DoH response")
}

log.Printf("[info] Write DoH response (%v bytes) from %v\n", len(resp), req.proxy.fakeDNSAddr)
return req.response.writeFrom(resp, net.UDPAddrFromAddrPort(req.proxy.fakeDNSAddr), false)
}

log.Printf("[debug] UDP Session: upload %v bytes to %v\n", len(p), destination)
req.stats.uploadBytes.Add(int64(len(p)))
return req.PacketRequestSender.WriteTo(p, destination)
}

// Close terminates the UDP session, and reports session stats to the listener.
func (resp *dohPacketRespReceiver) Close() error {
log.Println("[debug] UDP session terminating...")
defer log.Printf("[info] UDP session terminated: down = %v, up = %v\n", resp.stats.downloadBytes.Load(), resp.stats.uploadBytes.Load())
if resp.listener != nil {
resp.listener.OnUDPSocketClosed(&intraLegacy.UDPSocketSummary{
Duration: int32(time.Since(resp.stats.sessionStartTime)),
UploadBytes: resp.stats.uploadBytes.Load(),
DownloadBytes: resp.stats.downloadBytes.Load(),
})
}
return resp.PacketResponseReceiver.Close()
}

// WriteFrom implements PacketResponseReceiver.WriteFrom.
func (resp *dohPacketRespReceiver) WriteFrom(p []byte, source net.Addr) (int, error) {
log.Printf("[debug] Receiving raw UDP packet (%v bytes) from %v\n", len(p), source)
return resp.writeFrom(p, source, true)
}

// writeFrom writes to the underlying PacketResponseReceiver.
// It will also add len(p) to downloadBytes if doStat is true.
func (resp *dohPacketRespReceiver) writeFrom(p []byte, source net.Addr, doStat bool) (int, error) {
if doStat {
log.Printf("[debug] UDP Session: download %v bytes from %v\n", len(p), source)
resp.stats.downloadBytes.Add(int64(len(p)))
}
return resp.PacketResponseReceiver.WriteFrom(p, source)
}
19 changes: 19 additions & 0 deletions Android/backend/intra/internal/doh/protector.go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's really hard to follow with the code in two places.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright 2023 Jigsaw Operations LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package doh

import "github.com/Jigsaw-Code/outline-go-tun2socks/intra/protect"

type Protector = protect.Protector
Loading