Skip to content

Commit

Permalink
ipn: new tcp-in-h2 proxy-type
Browse files Browse the repository at this point in the history
  • Loading branch information
ignoramous committed Jun 21, 2023
1 parent f07473e commit 23f3f9d
Show file tree
Hide file tree
Showing 8 changed files with 304 additions and 16 deletions.
File renamed without changes.
File renamed without changes.
10 changes: 7 additions & 3 deletions intra/doh/doh.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ import (
"sync"
"time"

"github.com/celzero/firestack/intra/core/ipmap"
"github.com/celzero/firestack/intra/dnsx"
"github.com/celzero/firestack/intra/doh/ipmap"
"github.com/celzero/firestack/intra/log"
"github.com/celzero/firestack/intra/split"
"github.com/celzero/firestack/intra/xdns"
Expand Down Expand Up @@ -116,9 +116,13 @@ func (t *transport) dial(network, addr string) (net.Conn, error) {
// This is a POST-only DoH implementation, so the DoH template should be a URL.
// `rawurl` is the DoH template in string form.
// `addrs` is a list of domains or IP addresses to use as fallback, if the hostname
// lookup fails or returns non-working addresses.
//
// lookup fails or returns non-working addresses.
//
// `dialer` is the dialer that the transport will use. The transport will modify the dialer's
// timeout but will not mutate it otherwise.
//
// timeout but will not mutate it otherwise.
//
// `auth` will provide a client certificate if required by the TLS server.
// `listener` will receive the status of each DNS query when it is complete.
func NewTransport(id, rawurl string, addrs []string, dialer *net.Dialer) (dnsx.Transport, error) {
Expand Down
2 changes: 1 addition & 1 deletion intra/ipn/http1.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func NewHTTPProxy(id string, c protect.Controller, po *settings.ProxyOptions) (P
hp.Tr.Dial = protect.MakeNsDialer(c).Dial
hp.Verbose = settings.Debug
// todo: use user-preferred dns transport to dial urls?
dialfn := hp.NewConnectDialToProxy(po.AsUrl())
dialfn := hp.NewConnectDialToProxy(po.FullUrl())

if err != nil {
log.W("proxy: err creating up http1(%v): %v", po, err)
Expand Down
276 changes: 276 additions & 0 deletions intra/ipn/piph2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
// Copyright (c) 2023 RethinkDNS and its authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package ipn

import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"strconv"
"strings"
"time"

"github.com/celzero/firestack/intra/core/ipmap"
"github.com/celzero/firestack/intra/log"
"github.com/celzero/firestack/intra/protect"
"github.com/celzero/firestack/intra/settings"
"github.com/celzero/firestack/intra/split"
)

const (
tlsHandshakeTimeout time.Duration = 3 * time.Second
responseHeaderTimeout time.Duration = 3 * time.Second
)

type piph2 struct {
Proxy
id string
url string
hostname string
port int
ips ipmap.IPMap
token string // hex, client token
sig string // hex, authorizer signed client token
client http.Client
dialer *net.Dialer
status int
}

type pipconn struct {
Conn
r io.ReadCloser
w io.WriteCloser
}

func (c *pipconn) Read(b []byte) (int, error) {
if c.r == nil {
return 0, io.EOF
}
return c.r.Read(b)
}

func (c *pipconn) Write(b []byte) (int, error) {
if c.w == nil {
return 0, io.EOF
}
return c.w.Write(b)
}

func (c *pipconn) Close() (err error) {
if c.r != nil {
c.r.Close()
}
if c.w != nil {
err = c.w.Close()
}
return
}

func (t *piph2) dial(network, addr string) (net.Conn, error) {
log.D("piph2: dialing %s", addr)
domain, portStr, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
port, err := strconv.Atoi(portStr)
if err != nil {
return nil, err
}

tcpaddr := func(ip net.IP) *net.TCPAddr {
return &net.TCPAddr{IP: ip, Port: port}
}

// TODO: Improve IP fallback strategy with parallelism and Happy Eyeballs.
var conn net.Conn
ips := t.ips.Get(domain)
confirmed := ips.Confirmed()
if confirmed != nil {
if conn, err = split.DialWithSplitRetry(t.dialer, tcpaddr(confirmed), nil); err == nil {
log.I("piph2: confirmed IP %s worked", confirmed.String())
return conn, nil
}
log.D("piph2: confirmed IP %s failed with err %v", confirmed.String(), err)
ips.Disconfirm(confirmed)
}

log.D("piph2: trying all IPs")
for _, ip := range ips.GetAll() {
if ip.Equal(confirmed) {
// Don't try this IP twice.
continue
}
if conn, err = split.DialWithSplitRetry(t.dialer, tcpaddr(ip), nil); err == nil {
log.I("piph2: found working IP: %s", ip.String())
return conn, nil
}
}
return nil, err
}

func NewPipProxy(id string, ctl protect.Controller, po *settings.ProxyOptions) (Proxy, error) {
rawurl := po.Url()
parsedurl, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
if parsedurl.Scheme != "https" {
return nil, fmt.Errorf("bad scheme: %s", parsedurl.Scheme)
}
portStr := parsedurl.Port()
var port int
if len(portStr) > 0 {
port, err = strconv.Atoi(portStr)
if err != nil {
return nil, err
}
} else {
port = 443
}

dialer := protect.MakeNsDialer(ctl)
t := &piph2{
id: id,
url: rawurl,
hostname: parsedurl.Hostname(),
port: port,
dialer: dialer,
token: po.Auth.User,
sig: po.Auth.Password,
ips: ipmap.NewIPMap(dialer.Resolver),
status: TOK,
}

ipset := t.ips.Of(t.hostname, po.Addrs) // po.Addrs may be nil or empty
if ipset.Empty() {
// IPs instead resolved just-in-time with ipmap.Get in transport.dial
log.W("piph2: zero bootstrap ips %s", t.hostname)
}

// Override the dial function.
t.client.Transport = &http.Transport{
Dial: t.dial,
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: tlsHandshakeTimeout,
ResponseHeaderTimeout: responseHeaderTimeout,
}
return t, nil
}

func (t *piph2) ID() string {
return t.id
}

func (t *piph2) Type() string {
return PIPH2
}

func (t *piph2) GetAddr() string {
return t.hostname + ":" + strconv.Itoa(t.port)
}

func (t *piph2) Stop() error {
t.status = END
return nil
}

func (t *piph2) Status() int {
return t.status
}

func (t *piph2) claim(msg string) string {
// hmac msg keyed by token's sig
msgmac := hmac256(hex2byte(msg), hex2byte(t.sig))
return t.token + ":" + t.sig + ":" + byte2hex(msgmac)
}

func (t *piph2) Dial(network, addr string) (Conn, error) {
if t.status == END {
return nil, errProxyStopped
}

if network != "tcp" {
return nil, errUnexpectedProxy
}
url, err := url.Parse(t.url)
if err != nil {
return nil, err
}
ipp, err := netip.ParseAddrPort(addr)
if err != nil {
return nil, err
}

if !strings.HasSuffix(url.Path, "/") {
url.Path += "/"
}
url.Path += ipp.Addr().String() + "/" + strconv.Itoa(int(ipp.Port())) + "/" + network
// ref: github.com/ginuerzh/gost/blob/1c62376e0880e/http2.go#L221
readable, writable := io.Pipe()
req, err := http.NewRequest(http.MethodPost, url.String(), readable)
if err != nil {
t.status = TKO
return nil, err
}
msg, err := hexnonce(ipp)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "")
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("x-nile-pip-claim", t.claim(msg))
req.Header.Set("x-nile-pip-msg", msg)

res, err := t.client.Do(req)

if err != nil {
t.status = TKO
return nil, err
}

t.status = TOK
return &pipconn{
r: res.Body,
w: writable,
}, nil
}

func hmac256(m, k []byte) []byte {
mac := hmac.New(sha256.New, k)
mac.Write(m)
return mac.Sum(nil)
}

func hexnonce(ipport netip.AddrPort) (n string, err error) {
nonce := make([]byte, 16)
if _, err := rand.Read(nonce); err == nil {
nonce = append(nonce, ipport.Addr().AsSlice()...)
n = byte2hex(nonce)
} else {
log.E("piph2: hexnonce: err %v", err)
}
return
}

func hex2byte(s string) []byte {
b, err := hex.DecodeString(s)
if err != nil {
log.E("piph2: hex2byte: err %v", err)
}
return b
}

func byte2hex(b []byte) string {
return hex.EncodeToString(b)
}
1 change: 1 addition & 0 deletions intra/ipn/proxies.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const (
SOCKS5 = "socks5" // SOCKS5 proxy
HTTP1 = "http1" // HTTP/1.1 proxy
WG = "wg" // WireGuard-as-a-proxy
PIPH2 = "piph2" // PIP: HTTP/2 proxy
NOOP = "noop" // No proxy

// status of proxies
Expand Down
17 changes: 9 additions & 8 deletions intra/ipn/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
)

func (pxr *proxifier) NewSocks5Proxy(id, user, pwd, ip, port string) (p Proxy, err error) {
opts := settings.NewAuthProxyOptions("socks5", user, pwd, ip, port)
opts := settings.NewAuthProxyOptions("socks5", user, pwd, ip, port, nil)
return NewSocks5Proxy(id, pxr.ctl, opts)
}

Expand All @@ -40,9 +40,9 @@ func (pxr *proxifier) AddProxy(id, txt string) (p Proxy, err error) {
var strurl string
var usr string
var pwd string

var u *url.URL
// scheme://usr:[email protected]:8080/p/a/t/h?q&u=e&r=y
u, err := url.Parse(txt)
u, err = url.Parse(txt)
if err != nil {
return nil, err
}
Expand All @@ -51,9 +51,9 @@ func (pxr *proxifier) AddProxy(id, txt string) (p Proxy, err error) {
usr = u.User.Username() // usr
pwd, _ = u.User.Password() // pwd
}
strurl = u.Host + u.RequestURI() // domain.tld:8080/p/a/t/h?q&u=e&r=y

opts := settings.NewAuthProxyOptions(u.Scheme, usr, pwd, strurl, u.Port())
strurl = u.Host + u.RequestURI() // domain.tld:8080/p/a/t/h?q&u=e&r=y#f,r
addrs := strings.Split(u.Fragment, ",")
opts := settings.NewAuthProxyOptions(u.Scheme, usr, pwd, strurl, u.Port(), addrs)

switch u.Scheme {
case "socks5":
Expand All @@ -62,11 +62,12 @@ func (pxr *proxifier) AddProxy(id, txt string) (p Proxy, err error) {
fallthrough
case "https":
p, err = NewHTTPProxy(id, pxr.ctl, opts)
case "piph2":
p, err = NewPipProxy(id, pxr.ctl, opts)
case "wg":
err = fmt.Errorf("proxy: id must be prefixed with %s in %s for [%s]", WG, id, txt)
fallthrough
default:
return nil, errProxyScheme
err = errProxyScheme
}
}

Expand Down
Loading

0 comments on commit 23f3f9d

Please sign in to comment.