Skip to content

Commit 23f3f9d

Browse files
committed
ipn: new tcp-in-h2 proxy-type
1 parent f07473e commit 23f3f9d

File tree

8 files changed

+304
-16
lines changed

8 files changed

+304
-16
lines changed
File renamed without changes.
File renamed without changes.

intra/doh/doh.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ import (
3939
"sync"
4040
"time"
4141

42+
"github.com/celzero/firestack/intra/core/ipmap"
4243
"github.com/celzero/firestack/intra/dnsx"
43-
"github.com/celzero/firestack/intra/doh/ipmap"
4444
"github.com/celzero/firestack/intra/log"
4545
"github.com/celzero/firestack/intra/split"
4646
"github.com/celzero/firestack/intra/xdns"
@@ -116,9 +116,13 @@ func (t *transport) dial(network, addr string) (net.Conn, error) {
116116
// This is a POST-only DoH implementation, so the DoH template should be a URL.
117117
// `rawurl` is the DoH template in string form.
118118
// `addrs` is a list of domains or IP addresses to use as fallback, if the hostname
119-
// lookup fails or returns non-working addresses.
119+
//
120+
// lookup fails or returns non-working addresses.
121+
//
120122
// `dialer` is the dialer that the transport will use. The transport will modify the dialer's
121-
// timeout but will not mutate it otherwise.
123+
//
124+
// timeout but will not mutate it otherwise.
125+
//
122126
// `auth` will provide a client certificate if required by the TLS server.
123127
// `listener` will receive the status of each DNS query when it is complete.
124128
func NewTransport(id, rawurl string, addrs []string, dialer *net.Dialer) (dnsx.Transport, error) {

intra/ipn/http1.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func NewHTTPProxy(id string, c protect.Controller, po *settings.ProxyOptions) (P
3434
hp.Tr.Dial = protect.MakeNsDialer(c).Dial
3535
hp.Verbose = settings.Debug
3636
// todo: use user-preferred dns transport to dial urls?
37-
dialfn := hp.NewConnectDialToProxy(po.AsUrl())
37+
dialfn := hp.NewConnectDialToProxy(po.FullUrl())
3838

3939
if err != nil {
4040
log.W("proxy: err creating up http1(%v): %v", po, err)

intra/ipn/piph2.go

+276
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
// Copyright (c) 2023 RethinkDNS and its authors.
2+
//
3+
// This Source Code Form is subject to the terms of the Mozilla Public
4+
// License, v. 2.0. If a copy of the MPL was not distributed with this
5+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
7+
package ipn
8+
9+
import (
10+
"crypto/hmac"
11+
"crypto/rand"
12+
"crypto/sha256"
13+
"encoding/hex"
14+
"fmt"
15+
"io"
16+
"net"
17+
"net/http"
18+
"net/netip"
19+
"net/url"
20+
"strconv"
21+
"strings"
22+
"time"
23+
24+
"github.com/celzero/firestack/intra/core/ipmap"
25+
"github.com/celzero/firestack/intra/log"
26+
"github.com/celzero/firestack/intra/protect"
27+
"github.com/celzero/firestack/intra/settings"
28+
"github.com/celzero/firestack/intra/split"
29+
)
30+
31+
const (
32+
tlsHandshakeTimeout time.Duration = 3 * time.Second
33+
responseHeaderTimeout time.Duration = 3 * time.Second
34+
)
35+
36+
type piph2 struct {
37+
Proxy
38+
id string
39+
url string
40+
hostname string
41+
port int
42+
ips ipmap.IPMap
43+
token string // hex, client token
44+
sig string // hex, authorizer signed client token
45+
client http.Client
46+
dialer *net.Dialer
47+
status int
48+
}
49+
50+
type pipconn struct {
51+
Conn
52+
r io.ReadCloser
53+
w io.WriteCloser
54+
}
55+
56+
func (c *pipconn) Read(b []byte) (int, error) {
57+
if c.r == nil {
58+
return 0, io.EOF
59+
}
60+
return c.r.Read(b)
61+
}
62+
63+
func (c *pipconn) Write(b []byte) (int, error) {
64+
if c.w == nil {
65+
return 0, io.EOF
66+
}
67+
return c.w.Write(b)
68+
}
69+
70+
func (c *pipconn) Close() (err error) {
71+
if c.r != nil {
72+
c.r.Close()
73+
}
74+
if c.w != nil {
75+
err = c.w.Close()
76+
}
77+
return
78+
}
79+
80+
func (t *piph2) dial(network, addr string) (net.Conn, error) {
81+
log.D("piph2: dialing %s", addr)
82+
domain, portStr, err := net.SplitHostPort(addr)
83+
if err != nil {
84+
return nil, err
85+
}
86+
port, err := strconv.Atoi(portStr)
87+
if err != nil {
88+
return nil, err
89+
}
90+
91+
tcpaddr := func(ip net.IP) *net.TCPAddr {
92+
return &net.TCPAddr{IP: ip, Port: port}
93+
}
94+
95+
// TODO: Improve IP fallback strategy with parallelism and Happy Eyeballs.
96+
var conn net.Conn
97+
ips := t.ips.Get(domain)
98+
confirmed := ips.Confirmed()
99+
if confirmed != nil {
100+
if conn, err = split.DialWithSplitRetry(t.dialer, tcpaddr(confirmed), nil); err == nil {
101+
log.I("piph2: confirmed IP %s worked", confirmed.String())
102+
return conn, nil
103+
}
104+
log.D("piph2: confirmed IP %s failed with err %v", confirmed.String(), err)
105+
ips.Disconfirm(confirmed)
106+
}
107+
108+
log.D("piph2: trying all IPs")
109+
for _, ip := range ips.GetAll() {
110+
if ip.Equal(confirmed) {
111+
// Don't try this IP twice.
112+
continue
113+
}
114+
if conn, err = split.DialWithSplitRetry(t.dialer, tcpaddr(ip), nil); err == nil {
115+
log.I("piph2: found working IP: %s", ip.String())
116+
return conn, nil
117+
}
118+
}
119+
return nil, err
120+
}
121+
122+
func NewPipProxy(id string, ctl protect.Controller, po *settings.ProxyOptions) (Proxy, error) {
123+
rawurl := po.Url()
124+
parsedurl, err := url.Parse(rawurl)
125+
if err != nil {
126+
return nil, err
127+
}
128+
if parsedurl.Scheme != "https" {
129+
return nil, fmt.Errorf("bad scheme: %s", parsedurl.Scheme)
130+
}
131+
portStr := parsedurl.Port()
132+
var port int
133+
if len(portStr) > 0 {
134+
port, err = strconv.Atoi(portStr)
135+
if err != nil {
136+
return nil, err
137+
}
138+
} else {
139+
port = 443
140+
}
141+
142+
dialer := protect.MakeNsDialer(ctl)
143+
t := &piph2{
144+
id: id,
145+
url: rawurl,
146+
hostname: parsedurl.Hostname(),
147+
port: port,
148+
dialer: dialer,
149+
token: po.Auth.User,
150+
sig: po.Auth.Password,
151+
ips: ipmap.NewIPMap(dialer.Resolver),
152+
status: TOK,
153+
}
154+
155+
ipset := t.ips.Of(t.hostname, po.Addrs) // po.Addrs may be nil or empty
156+
if ipset.Empty() {
157+
// IPs instead resolved just-in-time with ipmap.Get in transport.dial
158+
log.W("piph2: zero bootstrap ips %s", t.hostname)
159+
}
160+
161+
// Override the dial function.
162+
t.client.Transport = &http.Transport{
163+
Dial: t.dial,
164+
ForceAttemptHTTP2: true,
165+
TLSHandshakeTimeout: tlsHandshakeTimeout,
166+
ResponseHeaderTimeout: responseHeaderTimeout,
167+
}
168+
return t, nil
169+
}
170+
171+
func (t *piph2) ID() string {
172+
return t.id
173+
}
174+
175+
func (t *piph2) Type() string {
176+
return PIPH2
177+
}
178+
179+
func (t *piph2) GetAddr() string {
180+
return t.hostname + ":" + strconv.Itoa(t.port)
181+
}
182+
183+
func (t *piph2) Stop() error {
184+
t.status = END
185+
return nil
186+
}
187+
188+
func (t *piph2) Status() int {
189+
return t.status
190+
}
191+
192+
func (t *piph2) claim(msg string) string {
193+
// hmac msg keyed by token's sig
194+
msgmac := hmac256(hex2byte(msg), hex2byte(t.sig))
195+
return t.token + ":" + t.sig + ":" + byte2hex(msgmac)
196+
}
197+
198+
func (t *piph2) Dial(network, addr string) (Conn, error) {
199+
if t.status == END {
200+
return nil, errProxyStopped
201+
}
202+
203+
if network != "tcp" {
204+
return nil, errUnexpectedProxy
205+
}
206+
url, err := url.Parse(t.url)
207+
if err != nil {
208+
return nil, err
209+
}
210+
ipp, err := netip.ParseAddrPort(addr)
211+
if err != nil {
212+
return nil, err
213+
}
214+
215+
if !strings.HasSuffix(url.Path, "/") {
216+
url.Path += "/"
217+
}
218+
url.Path += ipp.Addr().String() + "/" + strconv.Itoa(int(ipp.Port())) + "/" + network
219+
// ref: github.com/ginuerzh/gost/blob/1c62376e0880e/http2.go#L221
220+
readable, writable := io.Pipe()
221+
req, err := http.NewRequest(http.MethodPost, url.String(), readable)
222+
if err != nil {
223+
t.status = TKO
224+
return nil, err
225+
}
226+
msg, err := hexnonce(ipp)
227+
if err != nil {
228+
return nil, err
229+
}
230+
req.Header.Set("User-Agent", "")
231+
req.Header.Set("Content-Type", "application/octet-stream")
232+
req.Header.Set("x-nile-pip-claim", t.claim(msg))
233+
req.Header.Set("x-nile-pip-msg", msg)
234+
235+
res, err := t.client.Do(req)
236+
237+
if err != nil {
238+
t.status = TKO
239+
return nil, err
240+
}
241+
242+
t.status = TOK
243+
return &pipconn{
244+
r: res.Body,
245+
w: writable,
246+
}, nil
247+
}
248+
249+
func hmac256(m, k []byte) []byte {
250+
mac := hmac.New(sha256.New, k)
251+
mac.Write(m)
252+
return mac.Sum(nil)
253+
}
254+
255+
func hexnonce(ipport netip.AddrPort) (n string, err error) {
256+
nonce := make([]byte, 16)
257+
if _, err := rand.Read(nonce); err == nil {
258+
nonce = append(nonce, ipport.Addr().AsSlice()...)
259+
n = byte2hex(nonce)
260+
} else {
261+
log.E("piph2: hexnonce: err %v", err)
262+
}
263+
return
264+
}
265+
266+
func hex2byte(s string) []byte {
267+
b, err := hex.DecodeString(s)
268+
if err != nil {
269+
log.E("piph2: hex2byte: err %v", err)
270+
}
271+
return b
272+
}
273+
274+
func byte2hex(b []byte) string {
275+
return hex.EncodeToString(b)
276+
}

intra/ipn/proxies.go

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const (
2626
SOCKS5 = "socks5" // SOCKS5 proxy
2727
HTTP1 = "http1" // HTTP/1.1 proxy
2828
WG = "wg" // WireGuard-as-a-proxy
29+
PIPH2 = "piph2" // PIP: HTTP/2 proxy
2930
NOOP = "noop" // No proxy
3031

3132
// status of proxies

intra/ipn/proxy.go

+9-8
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
)
1717

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

@@ -40,9 +40,9 @@ func (pxr *proxifier) AddProxy(id, txt string) (p Proxy, err error) {
4040
var strurl string
4141
var usr string
4242
var pwd string
43-
43+
var u *url.URL
4444
// scheme://usr:[email protected]:8080/p/a/t/h?q&u=e&r=y
45-
u, err := url.Parse(txt)
45+
u, err = url.Parse(txt)
4646
if err != nil {
4747
return nil, err
4848
}
@@ -51,9 +51,9 @@ func (pxr *proxifier) AddProxy(id, txt string) (p Proxy, err error) {
5151
usr = u.User.Username() // usr
5252
pwd, _ = u.User.Password() // pwd
5353
}
54-
strurl = u.Host + u.RequestURI() // domain.tld:8080/p/a/t/h?q&u=e&r=y
55-
56-
opts := settings.NewAuthProxyOptions(u.Scheme, usr, pwd, strurl, u.Port())
54+
strurl = u.Host + u.RequestURI() // domain.tld:8080/p/a/t/h?q&u=e&r=y#f,r
55+
addrs := strings.Split(u.Fragment, ",")
56+
opts := settings.NewAuthProxyOptions(u.Scheme, usr, pwd, strurl, u.Port(), addrs)
5757

5858
switch u.Scheme {
5959
case "socks5":
@@ -62,11 +62,12 @@ func (pxr *proxifier) AddProxy(id, txt string) (p Proxy, err error) {
6262
fallthrough
6363
case "https":
6464
p, err = NewHTTPProxy(id, pxr.ctl, opts)
65+
case "piph2":
66+
p, err = NewPipProxy(id, pxr.ctl, opts)
6567
case "wg":
6668
err = fmt.Errorf("proxy: id must be prefixed with %s in %s for [%s]", WG, id, txt)
67-
fallthrough
6869
default:
69-
return nil, errProxyScheme
70+
err = errProxyScheme
7071
}
7172
}
7273

0 commit comments

Comments
 (0)