diff --git a/intra/backend/ipn_proxies.go b/intra/backend/ipn_proxies.go index 8d489def..5f9fb0f6 100644 --- a/intra/backend/ipn_proxies.go +++ b/intra/backend/ipn_proxies.go @@ -14,6 +14,8 @@ const ( // see ipn/proxies.go Base = "Base" // does not proxy traffic; in sync w dnsx.NetNoProxy Exit = "Exit" // always connects to the Internet (exit node); in sync w dnsx.NetExitProxy Ingress = "Ingress" // incoming connections + Auto = "rpn" // auto uses ipn.Exit or any of the RPN proxies + RpnWg = WG + RPN // RPN Warp OrbotS5 = "OrbotSocks5" // Orbot: Base Tor-as-a-SOCKS5 proxy OrbotH1 = "OrbotHttp1" // Orbot: Base Tor-as-a-HTTP/1.1 proxy @@ -27,6 +29,7 @@ const ( // see ipn/proxies.go PIPWS = "pipws" // PIP: WebSockets proxy NOOP = "noop" // No proxy, ex: Base, Block INTERNET = "net" // egress network, ex: Exit + RPN = "rpn" // Rethink Proxy Network // status of proxies @@ -38,6 +41,10 @@ const ( // see ipn/proxies.go END = -3 // proxy stopped ) +type Rpn interface { + RegisterWarp(b64 string) ([]byte, error) +} + type Proxy interface { // ID returns the ID of this proxy. ID() string @@ -68,6 +75,8 @@ type Proxies interface { GetProxy(id string) (Proxy, error) // Router returns a lowest common denomination router for this multi-transport. Router() Router + // RPN returns the Rethink Proxy Network interface. + Rpn() Rpn // Refresh re-registers proxies and returns a csv of active ones. RefreshProxies() (string, error) } diff --git a/intra/core/ip.go b/intra/core/ip.go new file mode 100644 index 00000000..028c829e --- /dev/null +++ b/intra/core/ip.go @@ -0,0 +1,69 @@ +// Copyright (c) 2024 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/. +// +// This file incorporates work covered by the following copyright and +// permission notice: +// +// SPDX-License-Identifier: MIT + +// from: https://github.com/bepass-org/warp-plus/blob/19ac233cc/iputils/iputils.go + +package core + +import ( + "errors" + "fmt" + "math/big" + "math/rand" + "net/netip" + "time" +) + +// RandomIPFromPrefix returns a random IP from the provided CIDR prefix. +// Supports IPv4 and IPv6. Does not support mapped inputs. +func RandomIPFromPrefix(cidr netip.Prefix) (netip.Addr, error) { + startingAddress := cidr.Masked().Addr() + if startingAddress.Is4In6() { + return netip.Addr{}, errors.New("mapped v4 addresses not supported") + } + + prefixLen := cidr.Bits() + if prefixLen == -1 { + return netip.Addr{}, fmt.Errorf("invalid cidr: %s", cidr) + } + + // Initialise rand number generator + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + + // Find the bit length of the Host portion of the provided CIDR + // prefix + hostLen := big.NewInt(int64(startingAddress.BitLen() - prefixLen)) + + // Find the max value for our random number + max := new(big.Int).Exp(big.NewInt(2), hostLen, nil) + + // Generate the random number + randInt := new(big.Int).Rand(rng, max) + + // Get the first address in the CIDR prefix in 16-bytes form + startingAddress16 := startingAddress.As16() + + // Convert the first address into a decimal number + startingAddressInt := new(big.Int).SetBytes(startingAddress16[:]) + + // Add the random number to the decimal form of the starting address + // to get a random address in the desired range + randomAddressInt := new(big.Int).Add(startingAddressInt, randInt) + + // Convert the random address from decimal form back into netip.Addr + randomAddress, ok := netip.AddrFromSlice(randomAddressInt.FillBytes(make([]byte, 16))) + if !ok { + return netip.Addr{}, fmt.Errorf("failed to generate random IP from CIDR: %s", cidr) + } + + // Unmap any mapped v4 addresses before return + return randomAddress.Unmap(), nil +} diff --git a/intra/dialers/utls.go b/intra/dialers/utls.go new file mode 100644 index 00000000..9531b108 --- /dev/null +++ b/intra/dialers/utls.go @@ -0,0 +1,135 @@ +// Copyright (c) 2024 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/. +// +// This file incorporates work covered by the following copyright and +// permission notice: +// +// SPDX-License-Identifier: MIT + +// from: github.com/bepass-org/warp-plus/blob/19ac233cc/warp/tlsdial.go + +package dialers + +import ( + "io" + "net" + + "github.com/celzero/firestack/intra/core" + + utls "github.com/refraction-networking/utls" +) + +const utlsExtSniCurveId uint16 = 0x15 +const sniCurveSize = 1200 +const utlsVer = utls.VersionTLS12 + +var utlsDefaultCypherSuites = []uint16{ + utls.GREASE_PLACEHOLDER, + utls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + utls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + utls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + utls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + utls.TLS_AES_128_GCM_SHA256, // tls 1.3 + utls.FAKE_TLS_DHE_RSA_WITH_AES_256_CBC_SHA, + utls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + utls.TLS_RSA_WITH_AES_256_CBC_SHA, +} + +var utlsDefaultExt = []utls.TLSExtension{ + &sniCurveExt{ + curvelen: sniCurveSize, + pad: true, + }, + &utls.SupportedCurvesExtension{Curves: []utls.CurveID{utls.X25519, utls.CurveP256}}, + &utls.SupportedPointsExtension{SupportedPoints: []byte{0}}, // uncompressed + &utls.SessionTicketExtension{}, + &utls.ALPNExtension{AlpnProtocols: []string{"http/1.1"}}, + &utls.SignatureAlgorithmsExtension{ + SupportedSignatureAlgorithms: []utls.SignatureScheme{ + utls.ECDSAWithP256AndSHA256, + utls.ECDSAWithP384AndSHA384, + utls.ECDSAWithP521AndSHA512, + utls.PSSWithSHA256, + utls.PSSWithSHA384, + utls.PSSWithSHA512, + utls.PKCS1WithSHA256, + utls.PKCS1WithSHA384, + utls.PKCS1WithSHA512, + utls.ECDSAWithSHA1, + utls.PKCS1WithSHA1, + }, + }, + &utls.KeyShareExtension{KeyShares: []utls.KeyShare{ + {Group: utls.CurveID(utls.GREASE_PLACEHOLDER), Data: []byte{0}}, + {Group: utls.X25519}, + }}, + &utls.PSKKeyExchangeModesExtension{Modes: []uint8{1}}, // pskModeDHE +} + +// sniCurveExt implements SNICurve (0x15) extension +type sniCurveExt struct { + *utls.GenericExtension + curvelen int + pad bool // enabled if true +} + +// Len returns the length of the SNICurveExtension. +func (e *sniCurveExt) Len() int { + if e.pad { + return 4 + e.curvelen + } // extension disabled + return 0 +} + +// Read reads the SNICurveExtension. +func (e *sniCurveExt) Read(b []byte) (n int, err error) { + if !e.pad { // extension disabled + return 0, io.EOF + } + if len(b) < e.Len() { + return 0, io.ErrShortBuffer + } + // https://tools.ietf.org/html/rfc7627 + b[0] = byte(utlsExtSniCurveId >> 8) + b[1] = byte(utlsExtSniCurveId) + b[2] = byte(e.curvelen >> 8) + b[3] = byte(e.curvelen) + + bptr := core.Alloc() + buf := *bptr + buf = buf[:cap(buf)] + defer func() { + *bptr = buf + core.Recycle(bptr) + }() + + copy(b[4:], buf) + return e.Len(), io.EOF +} + +// utlsHello creates a TLS hello packet with SNICurve. +func utlsHello(conn net.Conn, config *utls.Config, sni string) (*utls.UConn, error) { + uconn := utls.UClient(conn, config, utls.HelloCustom) + spec := utls.ClientHelloSpec{ + TLSVersMax: utlsVer, + TLSVersMin: utlsVer, + CipherSuites: utlsDefaultCypherSuites, + Extensions: utlsDefaultExt, + GetSessionID: nil, + } + spec.Extensions = append(spec.Extensions, &utls.SNIExtension{ServerName: sni}) + err := uconn.ApplyPreset(&spec) + if err != nil { + return nil, err + } + + err = uconn.Handshake() + if err != nil { + return nil, err + } + + return uconn, nil +} diff --git a/intra/ipn/auto.go b/intra/ipn/auto.go new file mode 100644 index 00000000..afe4a18b --- /dev/null +++ b/intra/ipn/auto.go @@ -0,0 +1,167 @@ +// Copyright (c) 2024 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 ( + x "github.com/celzero/firestack/intra/backend" + "github.com/celzero/firestack/intra/core" + "github.com/celzero/firestack/intra/dialers" + "github.com/celzero/firestack/intra/log" + "github.com/celzero/firestack/intra/protect" +) + +// exit is a proxy that always dials out to the internet. +type auto struct { + protoagnostic + skiprefresh + pxr Proxies + rd *protect.RDial // this proxy as a RDial + addr string + status *core.Volatile[int] +} + +// NewExitProxy returns a new exit proxy. +func NewAutoProxy(pxr Proxies) *auto { + h := &auto{ + pxr: pxr, + addr: "127.5.51.52:5321", + status: core.NewVolatile(TUP), + } + h.rd = newRDial(h) + return h +} + +// Dial implements Proxy. +func (h *auto) Dial(network, addr string) (protect.Conn, error) { + if h.status.Load() == END { + return nil, errProxyStopped + } + + exit, exerr := h.pxr.ProxyFor(Exit) + warp, waerr := h.pxr.ProxyFor(RpnWg) + + // auto always splits + c, who, err := core.Race( + network+".dial-auto."+addr, + tlsHandshakeTimeout, + func() (protect.Conn, error) { + if exit == nil { + return nil, exerr + } + return localDialStrat(exit.Dialer(), network, addr) + }, func() (protect.Conn, error) { + if warp == nil { + return nil, waerr + } + return dialers.ProxyDial(warp.Dialer(), network, addr) + }, + ) + + if err != nil { + h.status.Store(TKO) + } else { + h.status.Store(TOK) + } + // adjust TCP keepalive config if c is a TCPConn + protect.SetKeepAliveConfigSockOpt(c) + log.I("proxy: auto: w(%d) dial(%s) to %s; err? %v", who, network, addr, err) + return c, err +} + +// Announce implements Proxy. +func (h *auto) Announce(network, local string) (protect.PacketConn, error) { + if h.status.Load() == END { + return nil, errProxyStopped + } + exit, _ := h.pxr.ProxyFor(Exit) + warp, _ := h.pxr.ProxyFor(RpnWg) + + // auto always splits + c, who, err := core.Race( + network+".announce-auto."+local, + tlsHandshakeTimeout, + func() (protect.PacketConn, error) { + if exit == nil { + return nil, errProxyNotFound + } + return dialers.ListenPacket(exit.Dialer(), network, local) + }, func() (protect.PacketConn, error) { + if warp == nil { + return nil, errProxyNotFound + } + return dialers.ListenPacket(warp.Dialer(), network, local) + }, + ) + + if err != nil { + h.status.Store(TKO) + } else { + h.status.Store(TOK) + } + log.I("proxy: auto: w(%d) listen(%s) to %s; err? %v", who, network, local, err) + return c, err +} + +// Accept implements Proxy. +func (h *auto) Accept(network, local string) (protect.Listener, error) { + if h.status.Load() == END { + return nil, errProxyStopped + } + if exit, err := h.pxr.ProxyFor(Exit); err == nil { + return dialers.Listen(exit.Dialer(), network, local) + } else { + return nil, err + } +} + +// Probe implements Proxy. +func (h *auto) Probe(network, local string) (protect.PacketConn, error) { + if h.status.Load() == END { + return nil, errProxyStopped + } + // todo: rpnwg + if exit, err := h.pxr.ProxyFor(Exit); err == nil { + return dialers.Probe(exit.Dialer(), network, local) + } else { + return nil, err + } +} + +func (h *auto) Dialer() *protect.RDial { + return h.rd +} + +// todo: return system DNS +func (h *auto) DNS() string { + return nodns +} + +func (h *auto) ID() string { + return RpnWg +} + +func (h *auto) Type() string { + return RPN +} + +func (*auto) Router() x.Router { + return PROXYGATEWAY +} + +func (h *auto) GetAddr() string { + return h.addr +} + +func (h *auto) Status() int { + return h.status.Load() +} + +func (h *auto) Stop() error { + h.status.Store(END) + log.I("proxy: exit: stopped") + return nil +} diff --git a/intra/ipn/proxies.go b/intra/ipn/proxies.go index fed5efe4..74cf0833 100644 --- a/intra/ipn/proxies.go +++ b/intra/ipn/proxies.go @@ -16,6 +16,7 @@ import ( x "github.com/celzero/firestack/intra/backend" "github.com/celzero/firestack/intra/core" "github.com/celzero/firestack/intra/dialers" + "github.com/celzero/firestack/intra/ipn/warp" "github.com/celzero/firestack/intra/log" "github.com/celzero/firestack/intra/netstack" "github.com/celzero/firestack/intra/protect" @@ -29,6 +30,7 @@ const ( Ingress = x.Ingress OrbotS5 = x.OrbotS5 OrbotH1 = x.OrbotH1 + RpnWg = x.RpnWg SOCKS5 = x.SOCKS5 HTTP1 = x.HTTP1 @@ -37,6 +39,7 @@ const ( PIPWS = x.PIPWS NOOP = x.NOOP INTERNET = x.INTERNET + RPN = x.RPN TNT = x.TNT TZZ = x.TZZ @@ -125,16 +128,24 @@ type proxifier struct { exit *exit // exit proxy, never changes base *base // base proxy, never changes grounded *ground // grounded proxy, never changes + auto *auto // auto proxy, never changes ctl protect.Controller rev netstack.GConnHandler // may be nil + warpc *warp.Client // warp registration, never changes obs x.ProxyListener protos string } -var _ x.Router = (*gw)(nil) -var _ x.Router = (*proxifier)(nil) +type Rpn interface { + x.Rpn + Warp() (Proxy, error) + Pip() (Proxy, error) +} var _ Proxies = (*proxifier)(nil) +var _ x.Rpn = (*proxifier)(nil) +var _ x.Router = (*proxifier)(nil) +var _ x.Router = (*gw)(nil) var _ protect.RDialer = (Proxy)(nil) func NewProxifier(pctx context.Context, c protect.Controller, o x.ProxyListener) *proxifier { @@ -152,9 +163,13 @@ func NewProxifier(pctx context.Context, c protect.Controller, o x.ProxyListener) pxr.exit = NewExitProxy(c) pxr.base = NewBaseProxy(c) pxr.grounded = NewGroundProxy() + pxr.auto = NewAutoProxy(pxr) + + pxr.warpc = warp.NewWarpClient(pctx, c) pxr.add(pxr.exit) // fixed pxr.add(pxr.base) // fixed pxr.add(pxr.grounded) // fixed + pxr.add(pxr.auto) log.I("proxy: new") context.AfterFunc(pctx, pxr.stopProxies) @@ -248,30 +263,22 @@ func (px *proxifier) ProxyFor(id string) (Proxy, error) { } // go.dev/play/p/xCug1W3OcMH - out := make(chan Proxy) - core.Go("pxr.ProxyFor", func() { + p, ok := core.Grx("pxr.ProxyFor: "+id, func() Proxy { px.RLock() defer px.RUnlock() - defer close(out) - if p, ok := px.p[id]; ok { - out <- p - } else { - out <- nil - } - }) + return px.p[id] + }, getproxytimeout) - select { - case p := <-out: - if p == nil || core.IsNil(p) { - return nil, errProxyNotFound - } - return p, nil - case <-time.After(getproxytimeout): - log.D("proxy: for: %s; timeout!", id) + if !ok { + log.W("proxy: for: %s; timeout!", id) // possibly a deadlock, so return an error return nil, errGetProxyTimeout } + if p == nil || core.IsNil(p) { + return nil, errProxyNotFound + } + return p, nil } // GetProxy implements x.Proxies. @@ -283,6 +290,10 @@ func (px *proxifier) Router() x.Router { return px } +func (px *proxifier) Rpn() x.Rpn { + return px +} + func (px *proxifier) stopProxies() { px.Lock() defer px.Unlock() @@ -494,6 +505,26 @@ func (px *proxifier) Contains(ipprefix string) bool { return false } +// Implements x.Rpn. +func (px *proxifier) RegisterWarp(pub string) ([]byte, error) { + id, err := px.warpc.Make(pub, "") + if err != nil { + log.E("proxy: warp: make for %s failed: %v", pub, err) + return nil, err + } + // create a byte writer and write the identity to it + + return id.Json() +} + +func (px *proxifier) Warp() (Proxy, error) { + return px.ProxyFor(RpnWg) +} + +func (px *proxifier) Pip() (Proxy, error) { + return px.ProxyFor(PIPWS) +} + func local(id string) bool { return id == Base || id == Block || id == Exit } diff --git a/intra/ipn/proxy.go b/intra/ipn/proxy.go index 3e39ae04..4b352ceb 100644 --- a/intra/ipn/proxy.go +++ b/intra/ipn/proxy.go @@ -32,7 +32,7 @@ func (pxr *proxifier) addProxy(id, txt string) (p Proxy, err error) { return nil, errAddProxy } // wireguard proxies have IDs starting with "wg" - if strings.HasPrefix(id, WG) { + if strings.HasPrefix(id, WG) || strings.Compare(id, RpnWg) == 0 { if p, _ = pxr.ProxyFor(id); p != nil { if wgp, ok := p.(WgProxy); ok && wgp.update(id, txt) { log.I("proxy: updating wg %s/%s", id, p.GetAddr()) diff --git a/intra/ipn/warp/api.go b/intra/ipn/warp/api.go new file mode 100644 index 00000000..cd3b576d --- /dev/null +++ b/intra/ipn/warp/api.go @@ -0,0 +1,280 @@ +// Copyright (c) 2024 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/. +// +// This file incorporates work covered by the following copyright and +// permission notice: +// +// SPDX-License-Identifier: MIT + +// from: github.com/bepass-org/warp-plus/blob/19ac233cc/warp/api.go + +package warp + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/netip" + "time" + + "github.com/celzero/firestack/intra/core" + "github.com/celzero/firestack/intra/dialers" + "github.com/celzero/firestack/intra/log" + "github.com/celzero/firestack/intra/protect" + "github.com/noql-net/certpool" + utls "github.com/refraction-networking/utls" +) + +type Client struct { + c http.Client + d *protect.RDial +} + +func NewWarpClient(ctx context.Context, ctl protect.Controller) *Client { + d := protect.MakeNsRDial("warpclient", ctl) + w := &Client{ + d: d, + } + w.c.Transport = &http.Transport{ + DialTLSContext: w.utlsDial, + } + return w +} + +func (w *Client) utlsDial(ctx context.Context, network, addr string) (net.Conn, error) { + host, _, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + cfg := &utls.Config{ + ServerName: host, + MinVersion: utls.VersionTLS12, + RootCAs: certpool.Roots(), + } + ip, err := core.RandomIPFromPrefix(cfip141) + if err != nil { + return nil, err + } + return dialers.DialWithUTls(w.d, cfg, netip.AddrPortFrom(ip, uint16(443))) +} + +func (w *Client) GetAcct(tok, deviceID string) (IdentityAccount, error) { + reqUrl := fmt.Sprintf("%s/reg/%s/account", apiBase, deviceID) + method := "GET" + + req, err := http.NewRequest(method, reqUrl, nil) + if err != nil { + return IdentityAccount{}, err + } + + for k, v := range defaultHeaders { + req.Header.Set(k, v) + } + req.Header.Set("Authorization", "Bearer "+tok) + + resp, err := w.c.Do(req) + if err != nil { + return IdentityAccount{}, err + } + defer core.Close(resp.Body) + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return IdentityAccount{}, fmt.Errorf("API request failed with status: %s", resp.Status) + } + + // convert response to byte array + responseData, err := io.ReadAll(resp.Body) + if err != nil { + return IdentityAccount{}, err + } + + var ia = IdentityAccount{} + if err := json.Unmarshal(responseData, &ia); err != nil { + return IdentityAccount{}, err + } + + return ia, nil +} + +func (w *Client) reg(publicKey string) (Identity, error) { + reqUrl := fmt.Sprintf("%s/reg", apiBase) + method := "POST" + + data := map[string]interface{}{ + "install_id": "", + "fcm_token": "", + "tos": time.Now().Format(time.RFC3339Nano), + "key": publicKey, + "type": "Android", + "model": "PC", + "locale": "en_US", + "warp_enabled": true, + } + + jsonBody, err := json.Marshal(data) + if err != nil { + return Identity{}, err + } + + req, err := http.NewRequest(method, reqUrl, bytes.NewBuffer(jsonBody)) + if err != nil { + return Identity{}, err + } + + for k, v := range defaultHeaders { + req.Header.Set(k, v) + } + + resp, err := w.c.Do(req) + if err != nil { + return Identity{}, err + } + defer core.Close(resp.Body) + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return Identity{}, fmt.Errorf("API request failed with status: %s", resp.Status) + } + + responseData, err := io.ReadAll(resp.Body) + if err != nil { + return Identity{}, err + } + + var id = Identity{} + if err := json.Unmarshal(responseData, &id); err != nil { + return Identity{}, err + } + + return id, nil +} + +func (w *Client) ResetLicense(authToken, deviceID string) (License, error) { + reqUrl := fmt.Sprintf("%s/reg/%s/account/license", apiBase, deviceID) + method := "POST" + + req, err := http.NewRequest(method, reqUrl, nil) + if err != nil { + return License{}, err + } + + for k, v := range defaultHeaders { + req.Header.Set(k, v) + } + req.Header.Set("Authorization", "Bearer "+authToken) + + resp, err := w.c.Do(req) + if err != nil { + return License{}, err + } + defer core.Close(resp.Body) + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return License{}, fmt.Errorf("API request failed with response: %s", resp.Status) + } + + responseData, err := io.ReadAll(resp.Body) + if err != nil { + return License{}, err + } + + var lc = License{} + if err := json.Unmarshal(responseData, &lc); err != nil { + return License{}, err + } + + return lc, nil +} + +func (w *Client) UpdateAcct(authToken, deviceID, license string) (IdentityAccount, error) { + reqUrl := fmt.Sprintf("%s/reg/%s/account", apiBase, deviceID) + method := "PUT" + + jsonBody, err := json.Marshal(map[string]interface{}{"license": license}) + if err != nil { + return IdentityAccount{}, err + } + + req, err := http.NewRequest(method, reqUrl, bytes.NewBuffer(jsonBody)) + if err != nil { + return IdentityAccount{}, err + } + + for k, v := range defaultHeaders { + req.Header.Set(k, v) + } + req.Header.Set("Authorization", "Bearer "+authToken) + + resp, err := w.c.Do(req) + if err != nil { + return IdentityAccount{}, err + } + defer core.Close(resp.Body) + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return IdentityAccount{}, fmt.Errorf("API request failed with status: %s", resp.Status) + } + + responseData, err := io.ReadAll(resp.Body) + if err != nil { + return IdentityAccount{}, err + } + + var ia = IdentityAccount{} + if err := json.Unmarshal(responseData, &ia); err != nil { + return IdentityAccount{}, err + } + + return ia, nil +} + +// from: github.com/bepass-org/warp-plus/blob/19ac233cc/warp/account.go + +func (w *Client) Load(id Identity, license string) (*Identity, error) { + if license != "" && id.Account.License != license { + log.I("updating account license key") + _, err := w.UpdateAcct(id.Token, id.ID, license) + if err != nil { + return nil, err + } + + iAcc, err := w.GetAcct(id.Token, id.ID) + if err != nil { + return nil, err + } + id.Account = iAcc + } + + log.I("successfully loaded warp identity") + return &id, nil +} + +func (w *Client) Make(pub, license string) (*Identity, error) { + log.I("creating new identity %s", pub) + id, err := w.reg(pub) + if err != nil { + return nil, err + } + + if license != "" { + log.I("updating account license key for %s", pub) + _, err := w.UpdateAcct(id.Token, id.ID, license) + if err != nil { + return nil, err + } + + ac, err := w.GetAcct(id.Token, id.ID) + if err != nil { + return nil, err + } + id.Account = ac + } + + return &id, nil +} diff --git a/intra/ipn/warp/cfg.go b/intra/ipn/warp/cfg.go new file mode 100644 index 00000000..331e4309 --- /dev/null +++ b/intra/ipn/warp/cfg.go @@ -0,0 +1,132 @@ +// Copyright (c) 2024 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/. +// +// This file incorporates work covered by the following copyright and +// permission notice: +// +// SPDX-License-Identifier: MIT + +// from: github.com/bepass-org/warp-plus/blob/19ac233cc/warp/endpoint.go + +package warp + +import ( + "errors" + "math/rand" + "net/netip" + "time" + + "github.com/celzero/firestack/intra/core" +) + +const apiBase string = "https://api.cloudflareclient.com/v0a4005" + +// 141.101.113.0 cloudflare ip fronting +var cfip141 = netip.MustParsePrefix("141.101.113.0/24") + +var ports = []uint16{ + 500, + 854, + 859, + 864, + 878, + 880, + 890, + 891, + 894, + 903, + 908, + 928, + 934, + 939, + 942, + 943, + 945, + 946, + 955, + 968, + 987, + 988, + 1002, + 1010, + 1014, + 1018, + 1070, + 1074, + 1180, + 1387, + 1701, + 1843, + 2371, + 2408, + 2506, + 3138, + 3476, + 3581, + 3854, + 4177, + 4198, + 4233, + 4500, + 5279, + 5956, + 7103, + 7152, + 7156, + 7281, + 7559, + 8319, + 8742, + 8854, + 8886, +} + +var cidrs4 = []netip.Prefix{ + netip.MustParsePrefix("162.159.192.0/24"), + netip.MustParsePrefix("162.159.193.0/24"), + netip.MustParsePrefix("162.159.195.0/24"), + netip.MustParsePrefix("188.114.96.0/24"), + netip.MustParsePrefix("188.114.97.0/24"), + netip.MustParsePrefix("188.114.98.0/24"), + netip.MustParsePrefix("188.114.99.0/24"), +} + +var cidrs6 = []netip.Prefix{ + netip.MustParsePrefix("2606:4700:d0::/64"), + netip.MustParsePrefix("2606:4700:d1::/64"), +} + +var defaultHeaders = map[string]string{ + "Content-Type": "application/json; charset=UTF-8", + "User-Agent": "okhttp/3.12.1", + "CF-Client-Version": "a-6.30-3596", +} + +func anyCidrs() (v4 netip.Prefix, v6 netip.Prefix) { + return cidrs4[rand.Intn(len(cidrs4))], cidrs6[rand.Intn(len(cidrs6))] +} + +func anyPort() uint16 { + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + return ports[rng.Intn(len(ports))] +} + +func Endpoints() (v4 netip.AddrPort, v6 netip.AddrPort, err error) { + cidr4, cidr6 := anyCidrs() + ip4, err4 := core.RandomIPFromPrefix(cidr4) + ip6, err6 := core.RandomIPFromPrefix(cidr6) + if err4 != nil && err6 != nil { + err = errors.Join(err4, err6) + return + } + if v4.IsValid() { + v4 = netip.AddrPortFrom(ip4, anyPort()) + } + if v6.IsValid() { + v6 = netip.AddrPortFrom(ip6, anyPort()) + } + return +} diff --git a/intra/ipn/warp/id.go b/intra/ipn/warp/id.go new file mode 100644 index 00000000..b07210e8 --- /dev/null +++ b/intra/ipn/warp/id.go @@ -0,0 +1,198 @@ +// Copyright (c) 2024 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/. +// +// This file incorporates work covered by the following copyright and +// permission notice: +// +// SPDX-License-Identifier: MIT + +// from: github.com/bepass-org/warp-plus/blob/19ac233cc/warp/api.go + +package warp + +import ( + "encoding/json" + "errors" + "fmt" + "io" + + "github.com/celzero/firestack/intra/log" +) + +var ( + errZeroIdentity = errors.New("warp: identity content empty") + errZeroPeers = errors.New("warp: no peers") +) + +type IdentityAccount struct { + Created string `json:"created"` + Updated string `json:"updated"` + License string `json:"license"` + PremiumData int64 `json:"premium_data"` + WarpPlus bool `json:"warp_plus"` + AccountType string `json:"account_type"` + ReferralRenewalCountdown int64 `json:"referral_renewal_countdown"` + Role string `json:"role"` + ID string `json:"id"` + Quota int64 `json:"quota"` + Usage int64 `json:"usage"` + ReferralCount int64 `json:"referral_count"` + TTL string `json:"ttl"` +} + +type IdentityConfigPeerEndpoint struct { + V4 string `json:"v4"` + V6 string `json:"v6"` + Host string `json:"host"` + Ports []uint16 `json:"ports"` +} + +type IdentityConfigPeer struct { + PublicKey string `json:"public_key"` + Endpoint IdentityConfigPeerEndpoint `json:"endpoint"` +} + +type IdentityConfigInterfaceAddresses struct { + V4 string `json:"v4"` + V6 string `json:"v6"` +} + +type IdentityConfigInterface struct { + Addresses IdentityConfigInterfaceAddresses `json:"addresses"` +} +type IdentityConfigServices struct { + HTTPProxy string `json:"http_proxy"` +} + +type IdentityConfig struct { + Peers []IdentityConfigPeer `json:"peers"` + Interface IdentityConfigInterface `json:"interface"` + Services IdentityConfigServices `json:"services"` + ClientID string `json:"client_id"` +} + +type Identity struct { + PrivateKey string `json:"private_key"` + Key string `json:"key"` + Account IdentityAccount `json:"account"` + Place int64 `json:"place"` + FCMToken string `json:"fcm_token"` + Name string `json:"name"` + TOS string `json:"tos"` + Locale string `json:"locale"` + InstallID string `json:"install_id"` + WarpEnabled bool `json:"warp_enabled"` + Type string `json:"type"` + Model string `json:"model"` + Config IdentityConfig `json:"config"` + Token string `json:"token"` + Enabled bool `json:"enabled"` + ID string `json:"id"` + Created string `json:"created"` + Updated string `json:"updated"` + WaitlistEnabled bool `json:"waitlist_enabled"` + WgConf string `json:"wgconf"` +} + +type IdentityDevice struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Model string `json:"model"` + Created string `json:"created"` + Activated string `json:"updated"` + Active bool `json:"active"` + Role string `json:"role"` +} + +type License struct { + License string `json:"license"` +} + +type bytewriter struct { + b []byte +} + +var _ io.WriteCloser = (*bytewriter)(nil) + +func (w *bytewriter) Write(p []byte) (n int, err error) { + w.b = append(w.b, p...) + return len(p), nil +} + +func (w *bytewriter) Close() error { + w.b = nil + return nil +} + +func (w *bytewriter) Bytes() []byte { + return w.b +} + +func (id *Identity) Json() ([]byte, error) { + var w bytewriter + if err := id.writeJson(&w); err != nil { + return nil, err + } + return w.Bytes(), nil +} + +func (id *Identity) writeJson(w io.Writer) error { + if len(id.ID) <= 0 { + return errZeroIdentity + } + id.genWgConf() + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + return enc.Encode(id) +} + +func (id *Identity) genWgConf() { + if len(id.Config.Peers) < 1 { + return + } + id.WgConf = fmt.Sprintf(`[Interface] +PublicKey = %s +Address = %s +Address = %s +DNS = %s +DNS = %s +[Peer] +PublicKey = %s +Endpoint = %s +Endpoint = %s +Endpoint = %s +AllowedIPs = %s +AllowedIPs = %s`, + id.Key, + id.Config.Interface.Addresses.V4, + id.Config.Interface.Addresses.V6, + // developers.cloudflare.com/1.1.1.1/ip-addresses/ + "1.1.1.1", + "2606:4700:4700::1001", + id.Config.Peers[0].PublicKey, + id.Config.Peers[0].Endpoint.V4, + id.Config.Peers[0].Endpoint.V6, + id.Config.Peers[0].Endpoint.Host, + "0.0.0.0/0", + "::/0", + ) +} + +func Load(b []byte) (Identity, error) { + var id Identity + err := json.Unmarshal(b, &id) + if err != nil { + return Identity{}, err + } + + p := len(id.Config.Peers) + if p < 1 { + return Identity{}, errZeroPeers + } + log.I("warp: loaded %s (peers: %d)", id.Key, p) + return id, nil +}