-
Notifications
You must be signed in to change notification settings - Fork 263
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
Changes from 4 commits
2e88e81
2ae005e
da4f950
4b2f58b
bffb7dc
1d1cdbb
5efdfa0
1b7eacd
d97a4f1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
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) | ||
} |
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 == ',' }) | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
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: