Compare commits

..

28 Commits

Author SHA1 Message Date
Nuno Diegues 88ce63e785 Release 2021.12.1 2021-12-16 00:50:28 +00:00
Nuno Diegues 2dc5f6ec8c TUN-5549: Revert "TUN-5277: Ensure cloudflared binary is FIPS compliant on linux amd64"
This reverts commit 157f5d1412.

FIPS compliant binaries (for linux/amd64) are causing HTTPS origins to not
be reachable by cloudflared in certain cases (e.g. with Let's Encrypt certificates).

Origins that are not HTTPS for cloudflared are not affected.
2021-12-16 00:29:01 +00:00
Nuno Diegues 8d41f99f2f Release 2021.12.0 2021-12-15 10:01:29 +00:00
Nuno Diegues 173190aa79 TUN-5544: Update CHANGES.md for next release 2021-12-15 09:15:53 +00:00
cthuang 9251b3aa1f TUN-5530: Get current time from ticker 2021-12-13 13:35:40 +00:00
Nuno Diegues b0e27d1eac TUN-5504: Fix upload of packages to public repo 2021-12-09 13:06:24 +00:00
cthuang 73a265f2fc TUN-5488: Close session after it's idle for a period defined by registerUdpSession RPC 2021-12-09 11:55:34 +00:00
Nuno Diegues 9bc59bc78c TUN-5519: Adjust URL for virtual_networks endpoint to match what we will publish 2021-12-07 13:06:46 +00:00
cthuang b73c588254 TUN-5422: Define RPC to unregister session 2021-12-06 16:37:09 +00:00
João Oliveirinha 7e47667b08 TUN-5481: Create abstraction for Origin UDP Connection
Creates an abstraction over UDP Conn for origin "connection" which can
be useful for future support of complex protocols that may require
changing ports during protocol negotiation (eg. SIP, TFTP)
In addition, it removes a dependency from ingress on connection package.
2021-12-06 16:37:09 +00:00
cthuang eea3d11e40 TUN-5301: Separate datagram multiplex and session management logic from quic connection logic 2021-12-06 16:37:09 +00:00
cthuang dd32dc1364 TUN-5299: Send/receive QUIC datagram from edge and proxy to origin as UDP 2021-12-06 16:37:09 +00:00
cthuang fc2333c934 TUN-5300: Define RPC to register UDP sessions 2021-12-06 16:37:09 +00:00
Nuno Diegues 571380b3f5 TUN-5362: Adjust route ip commands to be aware of virtual networks 2021-12-03 09:10:20 +00:00
Nuno Diegues eec6b87eea TUN-5361: Commands for managing virtual networks 2021-12-03 08:48:52 +00:00
Nuno Diegues 6cc7d99e32 TUN-5441: Fix message about available protocols 2021-11-18 14:30:22 +00:00
Nuno Diegues 59bbd51065 TUN-5129: Remove -dev suffix when computing version and Git has uncommitted changes
When building the docker image, this `-dev` suffix is being added to the
cloudflared binary version.
The reason must be that there's some file, which is tracked by git, and
that is modified during that build.

It's not clear which file is it. But, at the same time, it's not clear what
advantage this `-dev` suffix is bringing. So we're simply removing it so that
we generate proper versions (so that our tracking/observability can correctly
aggregate these values).
2021-11-17 12:28:05 +00:00
Nuno Diegues e35f744b36 TUN-5393: Content-length is no longer a control header for non-h2mux transports
- Refactors some h2mux specific logic from connection/header.go to connection/h2mux_header.go
 - Do the same for the unit tests
 - Add a non-h2mux "is control response header" function (we don't need one for the request flow)
 - In that new function, do not consider "content-length" as a control header
 - Use that function in the non-h2mux flow for response (and it will be used also in origintunneld)
2021-11-17 10:56:15 +00:00
Silver a96d4243ba Merge pull request #508 from bplessis/master
Fix for Issue #501: Unexpected User-agent insertion when tunneling http request
2021-11-15 10:08:56 -06:00
Silver d4733efb25 Merge pull request #507 from rex4539/typos
Fix typos
2021-11-15 10:04:13 -06:00
Benoit Plessis b88e0bc8f8 Fix for Issue #501: Unexpected User-agent insertion when tunneling http request
When forwarding an UA-less request to the origin server cloudflared insert the default golang http User-Agent, this is unexpected and can lead to issue.

This simple fix force setting the UA to the empty string when it isn't originaly provided.
2021-11-13 01:34:19 +01:00
Dimitris Apostolou 197a70c9c4 Fix typos 2021-11-12 17:38:06 +02:00
cthuang e71b88fcaa TUN-5408: Update quic package to v0.24.0 2021-11-10 22:10:38 +00:00
Nuno Diegues 157f5d1412 TUN-5277: Ensure cloudflared binary is FIPS compliant on linux amd64 2021-11-10 21:27:25 +00:00
cthuang 7024d193c9 TUN-5405: Update net package to v0.0.0-20211109214657-ef0fda0de508
This version contains fix to https://github.com/golang/go/issues/43989
2021-11-10 17:20:13 +00:00
Nuno Diegues 794635fb54 TUN-5397: Log cloudflared output when it fails to connect tunnel 2021-11-10 10:34:34 +00:00
Nuno Diegues 1ee540a166 TUN-5368: Log connection issues with LogLevel that depends on tunnel state
Connections from cloudflared to Cloudflare edge are long lived and may
break over time. That is expected for many reasons (ranging from network
conditions to operations within Cloudflare edge). Hence, logging that as
Error feels too strong and leads to users being concerned that something
is failing when it is actually expected.

With this change, we wrap logging about connection issues to be aware
of the tunnel state:
 - if the tunnel has no connections active, we log as error
 - otherwise we log as warning
2021-11-10 09:00:05 +00:00
Sudarsan Reddy 6bcc9a76e9 TUN-5387: Updated CHANGES.md for 2021.11.0 2021-11-04 14:55:18 +00:00
113 changed files with 5950 additions and 2292 deletions
+20
View File
@@ -1,5 +1,25 @@
**Experimental**: This is a new format for release notes. The format and availability is subject to change.
## 2021.12.1
### Bug Fixe
- Fixes Github issue #530 where cloudflared 2021.12.0 could not reach origins that were HTTPS and using certain encryption methods forbidden by FIPS compliance (such as Let's Encrypt certificates). To address this fix we have temporarily reverted FIPS compliance from amd64 linux binaries that was recently introduced (or fixed actually as it was never working before).
## 2021.12.0
### New Features
- Cloudflared binary released for amd64 linux is now FIPS compliant.
### Improvements
- Logging about connectivity to Cloudflare edge now only yields `ERR` level logging if there are no connections to
Cloudflare edge that are active. Otherwise it logs `WARN` level.
### Bug Fixes
- Fixes Github issue #501.
## 2021.11.0
### Improvements
- Fallback from `protocol:quic` to `protocol:http2` immediately if UDP connectivity isn't available. This could be because of a firewall or
egress rule.
## 2021.10.4
### Improvements
- Collect quic transport metrics on RTT, packets and bytes transferred.
+2 -2
View File
@@ -1,4 +1,4 @@
VERSION := $(shell git describe --tags --always --dirty="-dev" --match "[0-9][0-9][0-9][0-9].*.*")
VERSION := $(shell git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
MSI_VERSION := $(shell git tag -l --sort=v:refname | grep "w" | tail -1 | cut -c2-)
#MSI_VERSION expects the format of the tag to be: (wX.X.X). Starts with the w character to not break cfsetup.
#e.g. w3.0.1 or w4.2.10. It trims off the w character when creating the MSI.
@@ -114,7 +114,7 @@ test-ssh-server:
define publish_package
chmod 664 cloudflared*.$(1); \
for HOST in $(CF_PKG_HOSTS); do \
ssh-keyscan -t rsa $$HOST >> ~/.ssh/known_hosts; \
ssh-keyscan -t ecdsa $$HOST >> ~/.ssh/known_hosts; \
scp -p -4 cloudflared*.$(1) cfsync@$$HOST:/state/cf-pkg/staging/$(2)/$(TARGET_PUBLIC_REPO)/cloudflared/; \
done
endef
+28
View File
@@ -1,3 +1,31 @@
2021.12.1
- 2021-12-16 TUN-5549: Revert "TUN-5277: Ensure cloudflared binary is FIPS compliant on linux amd64"
2021.12.0
- 2021-12-13 TUN-5530: Get current time from ticker
- 2021-12-15 TUN-5544: Update CHANGES.md for next release
- 2021-12-07 TUN-5519: Adjust URL for virtual_networks endpoint to match what we will publish
- 2021-12-02 TUN-5488: Close session after it's idle for a period defined by registerUdpSession RPC
- 2021-12-09 TUN-5504: Fix upload of packages to public repo
- 2021-11-30 TUN-5481: Create abstraction for Origin UDP Connection
- 2021-11-30 TUN-5422: Define RPC to unregister session
- 2021-11-26 TUN-5361: Commands for managing virtual networks
- 2021-11-29 TUN-5362: Adjust route ip commands to be aware of virtual networks
- 2021-11-23 TUN-5301: Separate datagram multiplex and session management logic from quic connection logic
- 2021-11-10 TUN-5405: Update net package to v0.0.0-20211109214657-ef0fda0de508
- 2021-11-10 TUN-5408: Update quic package to v0.24.0
- 2021-11-12 Fix typos
- 2021-11-13 Fix for Issue #501: Unexpected User-agent insertion when tunneling http request
- 2021-11-16 TUN-5129: Remove `-dev` suffix when computing version and Git has uncommitted changes
- 2021-11-18 TUN-5441: Fix message about available protocols
- 2021-11-12 TUN-5300: Define RPC to register UDP sessions
- 2021-11-14 TUN-5299: Send/receive QUIC datagram from edge and proxy to origin as UDP
- 2021-11-04 TUN-5387: Updated CHANGES.md for 2021.11.0
- 2021-11-08 TUN-5368: Log connection issues with LogLevel that depends on tunnel state
- 2021-11-09 TUN-5397: Log cloudflared output when it fails to connect tunnel
- 2021-11-09 TUN-5277: Ensure cloudflared binary is FIPS compliant on linux amd64
- 2021-11-08 TUN-5393: Content-length is no longer a control header for non-h2mux transports
2021.11.0
- 2021-11-03 TUN-5285: Fallback to HTTP2 immediately if connection times out with no network activity
- 2021-09-29 Add flag to 'tunnel create' subcommand to specify a base64-encoded secret
+1 -1
View File
@@ -54,7 +54,7 @@ func (c *StdinoutStream) Write(p []byte) (int, error) {
return os.Stdout.Write(p)
}
// Helper to allow defering the response close with a check that the resp is not nil
// Helper to allow deferring the response close with a check that the resp is not nil
func closeRespBody(resp *http.Response) {
if resp != nil {
_ = resp.Body.Close()
+1 -1
View File
@@ -66,7 +66,7 @@ func DecodeOriginCert(blocks []byte) (*OriginCert, error) {
originCert.ServiceKey = ntt.ServiceKey
originCert.AccountID = ntt.AccountID
} else {
// Try the older format, where the zoneID and service key are seperated by
// Try the older format, where the zoneID and service key are separated by
// a new line character
token := string(block.Bytes)
s := strings.Split(token, "\n")
+1 -1
View File
@@ -155,7 +155,7 @@ stretch: &stretch
post-cache:
# Creates and routes a Named Tunnel for this build. Also constructs config file from env vars.
- python3 component-tests/setup.py --type create
- pytest component-tests
- pytest component-tests -o log_cli=true --log-cli-level=INFO
# The Named Tunnel is deleted and its route unprovisioned here.
- python3 component-tests/setup.py --type cleanup
update-homebrew:
+1 -1
View File
@@ -240,7 +240,7 @@ func login(c *cli.Context) error {
return nil
}
// ensureURLScheme prepends a URL with https:// if it doesnt have a scheme. http:// URLs will not be converted.
// ensureURLScheme prepends a URL with https:// if it doesn't have a scheme. http:// URLs will not be converted.
func ensureURLScheme(url string) string {
url = strings.Replace(strings.ToLower(url), "http://", "https://", 1)
if !strings.HasPrefix(url, "https://") {
+2
View File
@@ -102,6 +102,8 @@ func Commands() []*cli.Command {
buildLoginSubcommand(false),
buildCreateCommand(),
buildRouteCommand(),
// TODO TUN-5477 this should not be hidden
buildVirtualNetworkSubcommand(true),
buildRunCommand(),
buildListCommand(),
buildInfoCommand(),
@@ -1,8 +1,6 @@
package tunnel
import (
"net"
"github.com/pkg/errors"
"github.com/cloudflare/cloudflared/teamnet"
@@ -26,18 +24,18 @@ func (sc *subcommandContext) addRoute(newRoute teamnet.NewRoute) (teamnet.Route,
return client.AddRoute(newRoute)
}
func (sc *subcommandContext) deleteRoute(network net.IPNet) error {
func (sc *subcommandContext) deleteRoute(params teamnet.DeleteRouteParams) error {
client, err := sc.client()
if err != nil {
return errors.Wrap(err, noClientMsg)
}
return client.DeleteRoute(network)
return client.DeleteRoute(params)
}
func (sc *subcommandContext) getRouteByIP(ip net.IP) (teamnet.DetailedRoute, error) {
func (sc *subcommandContext) getRouteByIP(params teamnet.GetRouteByIpParams) (teamnet.DetailedRoute, error) {
client, err := sc.client()
if err != nil {
return teamnet.DetailedRoute{}, errors.Wrap(err, noClientMsg)
}
return client.GetByIP(ip)
return client.GetByIP(params)
}
@@ -0,0 +1,40 @@
package tunnel
import (
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/cloudflare/cloudflared/vnet"
)
func (sc *subcommandContext) addVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet.VirtualNetwork, error) {
client, err := sc.client()
if err != nil {
return vnet.VirtualNetwork{}, errors.Wrap(err, noClientMsg)
}
return client.CreateVirtualNetwork(newVnet)
}
func (sc *subcommandContext) listVirtualNetworks(filter *vnet.Filter) ([]*vnet.VirtualNetwork, error) {
client, err := sc.client()
if err != nil {
return nil, errors.Wrap(err, noClientMsg)
}
return client.ListVirtualNetworks(filter)
}
func (sc *subcommandContext) deleteVirtualNetwork(vnetId uuid.UUID) error {
client, err := sc.client()
if err != nil {
return errors.Wrap(err, noClientMsg)
}
return client.DeleteVirtualNetwork(vnetId)
}
func (sc *subcommandContext) updateVirtualNetwork(vnetId uuid.UUID, updates vnet.UpdateVirtualNetwork) error {
client, err := sc.client()
if err != nil {
return errors.Wrap(err, noClientMsg)
}
return client.UpdateVirtualNetwork(vnetId, updates)
}
+1 -1
View File
@@ -329,7 +329,7 @@ func listCommand(c *cli.Context) error {
if len(tunnels) > 0 {
formatAndPrintTunnelList(tunnels, c.Bool("show-recently-disconnected"))
} else {
fmt.Println("You have no tunnels, use 'cloudflared tunnel create' to define a new tunnel")
fmt.Println("No tunnels were found for the given filter flags. You can use 'cloudflared tunnel create' to create a tunnel.")
}
return nil
+89 -21
View File
@@ -6,6 +6,7 @@ import (
"os"
"text/tabwriter"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
@@ -15,26 +16,45 @@ import (
"github.com/urfave/cli/v2"
)
var (
vnetFlag = &cli.StringFlag{
Name: "virtual-network",
Aliases: []string{"vn"},
Usage: "The ID or name of the virtual network to which the route is associated to.",
}
)
func buildRouteIPSubcommand() *cli.Command {
return &cli.Command{
Name: "ip",
Usage: "Configure and query Cloudflare WARP routing to services or private networks available through this tunnel.",
Usage: "Configure and query Cloudflare WARP routing to private IP networks made available through Cloudflare Tunnels.",
UsageText: "cloudflared tunnel [--config FILEPATH] route COMMAND [arguments...]",
Description: `cloudflared can provision private routes from any IP space to origins in your corporate network.
Users enrolled in your Cloudflare for Teams organization can reach those routes through the
Cloudflare WARP client. You can also build rules to determine who can reach certain routes.`,
Description: `cloudflared can provision routes for any IP space in your corporate network. Users enrolled in
your Cloudflare for Teams organization can reach those IPs through the Cloudflare WARP
client. You can then configure L7/L4 filtering on https://dash.teams.cloudflare.com to
determine who can reach certain routes.
By default IP routes all exist within a single virtual network. If you use the same IP
space(s) in different physical private networks, all meant to be reachable via IP routes,
then you have to manage the ambiguous IP routes by associating them to virtual networks.
See "cloudflared tunnel network --help" for more information.`,
Subcommands: []*cli.Command{
{
Name: "add",
Action: cliutil.ConfiguredAction(addRouteCommand),
Usage: "Add any new network to the routing table reachable via the tunnel",
UsageText: "cloudflared tunnel [--config FILEPATH] route ip add [CIDR] [TUNNEL] [COMMENT?]",
Description: `Adds any network route space (represented as a CIDR) to your routing table.
That network space becomes reachable for requests egressing from a user's machine
Usage: "Add a new network to the routing table reachable via a Tunnel",
UsageText: "cloudflared tunnel [--config FILEPATH] route ip add [flags] [CIDR] [TUNNEL] [COMMENT?]",
Description: `Adds a network IP route space (represented as a CIDR) to your routing table.
That network IP space becomes reachable for requests egressing from a user's machine
as long as it is using Cloudflare WARP client and is enrolled in the same account
that is running the tunnel chosen here. Further, those requests will be proxied to
the specified tunnel, and reach an IP in the given CIDR, as long as that IP is
reachable from the tunnel.`,
that is running the Tunnel chosen here. Further, those requests will be proxied to
the specified Tunnel, and reach an IP in the given CIDR, as long as that IP is
reachable from cloudflared.
If the CIDR exists in more than one private network, to be connected with Cloudflare
Tunnels, then you have to manage those IP routes with virtual networks (see
"cloudflared tunnel network --help)". In those cases, you then have to tell
which virtual network's routing table you want to add the route to with:
"cloudflared tunnel route ip add --virtual-network [ID/name] [CIDR] [TUNNEL]".`,
Flags: []cli.Flag{vnetFlag},
},
{
Name: "show",
@@ -49,17 +69,22 @@ reachable from the tunnel.`,
Name: "delete",
Action: cliutil.ConfiguredAction(deleteRouteCommand),
Usage: "Delete a row from your organization's private routing table",
UsageText: "cloudflared tunnel [--config FILEPATH] route ip delete [CIDR]",
Description: `Deletes the row for a given CIDR from your routing table. That portion
of your network will no longer be reachable by the WARP clients.`,
UsageText: "cloudflared tunnel [--config FILEPATH] route ip delete [flags] [CIDR]",
Description: `Deletes the row for a given CIDR from your routing table. That portion of your network
will no longer be reachable by the WARP clients. Note that if you use virtual
networks, then you have to tell which virtual network whose routing table you
have a row deleted from.`,
Flags: []cli.Flag{vnetFlag},
},
{
Name: "get",
Action: cliutil.ConfiguredAction(getRouteByIPCommand),
Usage: "Check which row of the routing table matches a given IP.",
UsageText: "cloudflared tunnel [--config FILEPATH] route ip get [IP]",
Description: `Checks which row of the routing table will be used to proxy a given IP.
This helps check and validate your config.`,
UsageText: "cloudflared tunnel [--config FILEPATH] route ip get [flags] [IP]",
Description: `Checks which row of the routing table will be used to proxy a given IP. This helps check
and validate your config. Note that if you use virtual networks, then you have
to tell which virtual network whose routing table you want to use.`,
Flags: []cli.Flag{vnetFlag},
},
},
}
@@ -98,7 +123,7 @@ func showRoutesCommand(c *cli.Context) error {
if len(routes) > 0 {
formatAndPrintRouteList(routes)
} else {
fmt.Println("You have no routes, use 'cloudflared tunnel route ip add' to add a route")
fmt.Println("No routes were found for the given filter flags. You can use 'cloudflared tunnel route ip add' to add a route.")
}
return nil
@@ -112,7 +137,9 @@ func addRouteCommand(c *cli.Context) error {
if c.NArg() < 2 {
return errors.New("You must supply at least 2 arguments, first the network you wish to route (in CIDR form e.g. 1.2.3.4/32) and then the tunnel ID to proxy with")
}
args := c.Args()
_, network, err := net.ParseCIDR(args.Get(0))
if err != nil {
return errors.Wrap(err, "Invalid network CIDR")
@@ -120,19 +147,32 @@ func addRouteCommand(c *cli.Context) error {
if network == nil {
return errors.New("Invalid network CIDR")
}
tunnelRef := args.Get(1)
tunnelID, err := sc.findID(tunnelRef)
if err != nil {
return errors.Wrap(err, "Invalid tunnel")
}
comment := ""
if c.NArg() >= 3 {
comment = args.Get(2)
}
var vnetId *uuid.UUID
if c.IsSet(vnetFlag.Name) {
id, err := getVnetId(sc, c.String(vnetFlag.Name))
if err != nil {
return err
}
vnetId = &id
}
_, err = sc.addRoute(teamnet.NewRoute{
Comment: comment,
Network: *network,
TunnelID: tunnelID,
VNetID: vnetId,
})
if err != nil {
return errors.Wrap(err, "API error")
@@ -146,9 +186,11 @@ func deleteRouteCommand(c *cli.Context) error {
if err != nil {
return err
}
if c.NArg() != 1 {
return errors.New("You must supply exactly one argument, the network whose route you want to delete (in CIDR form e.g. 1.2.3.4/32)")
}
_, network, err := net.ParseCIDR(c.Args().First())
if err != nil {
return errors.Wrap(err, "Invalid network CIDR")
@@ -156,7 +198,20 @@ func deleteRouteCommand(c *cli.Context) error {
if network == nil {
return errors.New("Invalid network CIDR")
}
if err := sc.deleteRoute(*network); err != nil {
params := teamnet.DeleteRouteParams{
Network: *network,
}
if c.IsSet(vnetFlag.Name) {
vnetId, err := getVnetId(sc, c.String(vnetFlag.Name))
if err != nil {
return err
}
params.VNetID = &vnetId
}
if err := sc.deleteRoute(params); err != nil {
return errors.Wrap(err, "API error")
}
fmt.Printf("Successfully deleted route for %s\n", network)
@@ -177,7 +232,20 @@ func getRouteByIPCommand(c *cli.Context) error {
if ip == nil {
return fmt.Errorf("Invalid IP %s", ipInput)
}
route, err := sc.getRouteByIP(ip)
params := teamnet.GetRouteByIpParams{
Ip: ip,
}
if c.IsSet(vnetFlag.Name) {
vnetId, err := getVnetId(sc, c.String(vnetFlag.Name))
if err != nil {
return err
}
params.VNetID = &vnetId
}
route, err := sc.getRouteByIP(params)
if err != nil {
return errors.Wrap(err, "API error")
}
@@ -202,7 +270,7 @@ func formatAndPrintRouteList(routes []*teamnet.DetailedRoute) {
defer writer.Flush()
// Print column headers with tabbed columns
_, _ = fmt.Fprintln(writer, "NETWORK\tCOMMENT\tTUNNEL ID\tTUNNEL NAME\tCREATED\tDELETED\t")
_, _ = fmt.Fprintln(writer, "NETWORK\tVIRTUAL NET ID\tCOMMENT\tTUNNEL ID\tTUNNEL NAME\tCREATED\tDELETED\t")
// Loop through routes, create formatted string for each, and print using tabwriter
for _, route := range routes {
+285
View File
@@ -0,0 +1,285 @@
package tunnel
import (
"fmt"
"os"
"text/tabwriter"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
"github.com/cloudflare/cloudflared/vnet"
)
var (
makeDefaultFlag = &cli.BoolFlag{
Name: "default",
Aliases: []string{"d"},
Usage: "The virtual network becomes the default one for the account. This means that all operations that " +
"omit a virtual network will now implicitly be using this virtual network (i.e., the default one) such " +
"as new IP routes that are created. When this flag is not set, the virtual network will not become the " +
"default one in the account.",
}
newNameFlag = &cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Usage: "The new name for the virtual network.",
}
newCommentFlag = &cli.StringFlag{
Name: "comment",
Aliases: []string{"c"},
Usage: "A new comment describing the purpose of the virtual network.",
}
)
func buildVirtualNetworkSubcommand(hidden bool) *cli.Command {
return &cli.Command{
Name: "network",
Usage: "Configure and query virtual networks to manage private IP routes with overlapping IPs.",
UsageText: "cloudflared tunnel [--config FILEPATH] network COMMAND [arguments...]",
Description: `cloudflared allows to manage IP routes that expose origins in your private network space via their IP directly
to clients outside (e.g. using WARP client) --- those are configurable via "cloudflared tunnel route ip" commands.
By default, all those IP routes live in the same virtual network. Managing virtual networks (e.g. by creating a
new one) becomes relevant when you have different private networks that have overlapping IPs. E.g.: if you have
a private network A running Tunnel 1, and private network B running Tunnel 2, it is possible that both Tunnels
expose the same IP space (say 10.0.0.0/8); to handle that, you have to add each IP Route (one that points to
Tunnel 1 and another that points to Tunnel 2) in different Virtual Networks. That way, if your clients are on
Virtual Network X, they will see Tunnel 1 (via Route A) and not see Tunnel 2 (since its Route B is associated
to another Virtual Network Y).`,
Hidden: hidden,
Subcommands: []*cli.Command{
{
Name: "add",
Action: cliutil.ConfiguredAction(addVirtualNetworkCommand),
Usage: "Add a new virtual network to which IP routes can be attached",
UsageText: "cloudflared tunnel [--config FILEPATH] network add [flags] NAME [\"comment\"]",
Description: `Adds a new virtual network. You can then attach IP routes to this virtual network with "cloudflared tunnel route ip"
commands. By doing so, such route(s) become segregated from route(s) in another virtual networks. Note that all
routes exist within some virtual network. If you do not specify any, then the system pre-creates a default virtual
network to which all routes belong. That is fine if you do not have overlapping IPs within different physical
private networks in your infrastructure exposed via Cloudflare Tunnel. Note: if a virtual network is added as
the new default, then the previous existing default virtual network will be automatically modified to no longer
be the current default.`,
Flags: []cli.Flag{makeDefaultFlag},
Hidden: hidden,
},
{
Name: "list",
Action: cliutil.ConfiguredAction(listVirtualNetworksCommand),
Usage: "Lists the virtual networks",
UsageText: "cloudflared tunnel [--config FILEPATH] network list [flags]",
Description: "Lists the virtual networks based on the given filter flags.",
Flags: listVirtualNetworksFlags(),
Hidden: hidden,
},
{
Name: "delete",
Action: cliutil.ConfiguredAction(deleteVirtualNetworkCommand),
Usage: "Delete a virtual network",
UsageText: "cloudflared tunnel [--config FILEPATH] network delete VIRTUAL_NETWORK",
Description: `Deletes the virtual network (given its ID or name). This is only possible if that virtual network is unused.
A virtual network may be used by IP routes or by WARP devices.`,
Hidden: hidden,
},
{
Name: "update",
Action: cliutil.ConfiguredAction(updateVirtualNetworkCommand),
Usage: "Update a virtual network",
UsageText: "cloudflared tunnel [--config FILEPATH] network update [flags] VIRTUAL_NETWORK",
Description: `Updates the virtual network (given its ID or name). If this virtual network is updated to become the new
default, then the previously existing default virtual network will also be modified to no longer be the default.
You cannot update a virtual network to not be the default anymore directly. Instead, you should create a new
default or update an existing one to become the default.`,
Flags: []cli.Flag{newNameFlag, newCommentFlag, makeDefaultFlag},
Hidden: hidden,
},
},
}
}
func listVirtualNetworksFlags() []cli.Flag {
flags := make([]cli.Flag, 0)
flags = append(flags, vnet.FilterFlags...)
flags = append(flags, outputFormatFlag)
return flags
}
func addVirtualNetworkCommand(c *cli.Context) error {
sc, err := newSubcommandContext(c)
if err != nil {
return err
}
if c.NArg() < 1 {
return errors.New("You must supply at least 1 argument, the name of the virtual network you wish to add.")
}
warningChecker := updater.StartWarningCheck(c)
defer warningChecker.LogWarningIfAny(sc.log)
args := c.Args()
name := args.Get(0)
comment := ""
if c.NArg() >= 2 {
comment = args.Get(1)
}
newVnet := vnet.NewVirtualNetwork{
Name: name,
Comment: comment,
IsDefault: c.Bool(makeDefaultFlag.Name),
}
createdVnet, err := sc.addVirtualNetwork(newVnet)
if err != nil {
return errors.Wrap(err, "Could not add virtual network")
}
extraMsg := ""
if createdVnet.IsDefault {
extraMsg = " (as the new default for this account) "
}
fmt.Printf(
"Successfully added virtual 'network' %s with ID: %s%s\n"+
"You can now add IP routes attached to this virtual network. See `cloudflared tunnel route ip add -help`\n",
name, createdVnet.ID, extraMsg,
)
return nil
}
func listVirtualNetworksCommand(c *cli.Context) error {
sc, err := newSubcommandContext(c)
if err != nil {
return err
}
warningChecker := updater.StartWarningCheck(c)
defer warningChecker.LogWarningIfAny(sc.log)
filter, err := vnet.NewFromCLI(c)
if err != nil {
return errors.Wrap(err, "invalid flags for filtering virtual networks")
}
vnets, err := sc.listVirtualNetworks(filter)
if err != nil {
return err
}
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
return renderOutput(outputFormat, vnets)
}
if len(vnets) > 0 {
formatAndPrintVnetsList(vnets)
} else {
fmt.Println("No virtual networks were found for the given filter flags. You can use 'cloudflared tunnel network add' to add a virtual network.")
}
return nil
}
func deleteVirtualNetworkCommand(c *cli.Context) error {
sc, err := newSubcommandContext(c)
if err != nil {
return err
}
if c.NArg() != 1 {
return errors.New("You must supply exactly one argument, either the ID or name of the virtual network to delete")
}
input := c.Args().Get(0)
vnetId, err := getVnetId(sc, input)
if err != nil {
return err
}
if err := sc.deleteVirtualNetwork(vnetId); err != nil {
return errors.Wrap(err, "API error")
}
fmt.Printf("Successfully deleted virtual network '%s'\n", input)
return nil
}
func updateVirtualNetworkCommand(c *cli.Context) error {
sc, err := newSubcommandContext(c)
if err != nil {
return err
}
if c.NArg() != 1 {
return errors.New(" You must supply exactly one argument, either the ID or (current) name of the virtual network to update")
}
input := c.Args().Get(0)
vnetId, err := getVnetId(sc, input)
if err != nil {
return err
}
updates := vnet.UpdateVirtualNetwork{}
if c.IsSet(newNameFlag.Name) {
newName := c.String(newNameFlag.Name)
updates.Name = &newName
}
if c.IsSet(newCommentFlag.Name) {
newComment := c.String(newCommentFlag.Name)
updates.Comment = &newComment
}
if c.IsSet(makeDefaultFlag.Name) {
isDefault := c.Bool(makeDefaultFlag.Name)
updates.IsDefault = &isDefault
}
if err := sc.updateVirtualNetwork(vnetId, updates); err != nil {
return errors.Wrap(err, "API error")
}
fmt.Printf("Successfully updated virtual network '%s'\n", input)
return nil
}
func getVnetId(sc *subcommandContext, input string) (uuid.UUID, error) {
val, err := uuid.Parse(input)
if err == nil {
return val, nil
}
filter := vnet.NewFilter()
filter.WithDeleted(false)
filter.ByName(input)
vnets, err := sc.listVirtualNetworks(filter)
if err != nil {
return uuid.Nil, err
}
if len(vnets) != 1 {
return uuid.Nil, fmt.Errorf("there should only be 1 non-deleted virtual network named %s", input)
}
return vnets[0].ID, nil
}
func formatAndPrintVnetsList(vnets []*vnet.VirtualNetwork) {
const (
minWidth = 0
tabWidth = 8
padding = 1
padChar = ' '
flags = 0
)
writer := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, padChar, flags)
defer writer.Flush()
_, _ = fmt.Fprintln(writer, "ID\tNAME\tIS DEFAULT\tCOMMENT\tCREATED\tDELETED\t")
for _, virtualNetwork := range vnets {
formattedStr := virtualNetwork.TableString()
_, _ = fmt.Fprintln(writer, formattedStr)
}
}
+1 -1
View File
@@ -47,7 +47,7 @@ type batchData struct {
}
// WorkersVersion implements the Version interface.
// It contains everything needed to preform a version upgrade
// It contains everything needed to perform a version upgrade
type WorkersVersion struct {
downloadURL string
checksum string
+2 -2
View File
@@ -75,7 +75,7 @@ class TestLogging:
}
config = component_tests_config(extra_config)
with start_cloudflared(tmp_path, config, new_process=True, capture_output=False):
wait_tunnel_ready(tunnel_url=config.get_url())
wait_tunnel_ready(tunnel_url=config.get_url(), cfd_logs=str(log_file))
assert_log_in_file(log_file)
assert_json_log(log_file)
@@ -88,5 +88,5 @@ class TestLogging:
}
config = component_tests_config(extra_config)
with start_cloudflared(tmp_path, config, new_process=True, capture_output=False):
wait_tunnel_ready(tunnel_url=config.get_url())
wait_tunnel_ready(tunnel_url=config.get_url(), cfd_logs=str(log_dir))
assert_log_to_dir(config, log_dir)
+35 -9
View File
@@ -1,12 +1,13 @@
from contextlib import contextmanager
import logging
import requests
from retrying import retry
import os
import subprocess
import yaml
from contextlib import contextmanager
from time import sleep
import requests
import yaml
from retrying import retry
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS
LOGGER = logging.getLogger(__name__)
@@ -19,7 +20,8 @@ def write_config(directory, config):
return config_path
def start_cloudflared(directory, config, cfd_args=["run"], cfd_pre_args=["tunnel"], new_process=False, allow_input=False, capture_output=True, root=False):
def start_cloudflared(directory, config, cfd_args=["run"], cfd_pre_args=["tunnel"], new_process=False,
allow_input=False, capture_output=True, root=False):
config_path = write_config(directory, config.full_config)
cmd = cloudflared_cmd(config, config_path, cfd_args, cfd_pre_args, root)
if new_process:
@@ -53,18 +55,42 @@ def run_cloudflared_background(cmd, allow_input, capture_output):
LOGGER.info(f"cloudflared log: {cfd.stderr.read()}")
def wait_tunnel_ready(tunnel_url=None, require_min_connections=1, cfd_logs=None):
try:
inner_wait_tunnel_ready(tunnel_url, require_min_connections)
except Exception as e:
if cfd_logs is not None:
_log_cloudflared_logs(cfd_logs)
raise e
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
def wait_tunnel_ready(tunnel_url=None, require_min_connections=1):
def inner_wait_tunnel_ready(tunnel_url=None, require_min_connections=1):
metrics_url = f'http://localhost:{METRICS_PORT}/ready'
with requests.Session() as s:
resp = send_request(s, metrics_url, True)
assert resp.json()[
"readyConnections"] >= require_min_connections, f"Ready endpoint returned {resp.json()} but we expect at least {require_min_connections} connections"
assert resp.json()["readyConnections"] >= require_min_connections, \
f"Ready endpoint returned {resp.json()} but we expect at least {require_min_connections} connections"
if tunnel_url is not None:
send_request(s, tunnel_url, True)
def _log_cloudflared_logs(cfd_logs):
log_file = cfd_logs
if os.path.isdir(cfd_logs):
files = os.listdir(cfd_logs)
if len(files) == 0:
return
log_file = os.path.join(cfd_logs, files[0])
with open(log_file, "r") as f:
LOGGER.warning("Cloudflared Tunnel was not ready:")
for line in f.readlines():
LOGGER.warning(line)
@retry(stop_max_attempt_number=MAX_RETRIES * BACKOFF_SECS, wait_fixed=1000)
def check_tunnel_not_connected():
url = f'http://localhost:{METRICS_PORT}/ready'
+128
View File
@@ -0,0 +1,128 @@
package connection
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/cloudflare/cloudflared/h2mux"
)
// H2RequestHeadersToH1Request converts the HTTP/2 headers coming from origintunneld
// to an HTTP/1 Request object destined for the local origin web service.
// This operation includes conversion of the pseudo-headers into their closest
// HTTP/1 equivalents. See https://tools.ietf.org/html/rfc7540#section-8.1.2.3
func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error {
for _, header := range h2 {
name := strings.ToLower(header.Name)
if !IsH2muxControlRequestHeader(name) {
continue
}
switch name {
case ":method":
h1.Method = header.Value
case ":scheme":
// noop - use the preexisting scheme from h1.URL
case ":authority":
// Otherwise the host header will be based on the origin URL
h1.Host = header.Value
case ":path":
// We don't want to be an "opinionated" proxy, so ideally we would use :path as-is.
// However, this HTTP/1 Request object belongs to the Go standard library,
// whose URL package makes some opinionated decisions about the encoding of
// URL characters: see the docs of https://godoc.org/net/url#URL,
// in particular the EscapedPath method https://godoc.org/net/url#URL.EscapedPath,
// which is always used when computing url.URL.String(), whether we'd like it or not.
//
// Well, not *always*. We could circumvent this by using url.URL.Opaque. But
// that would present unusual difficulties when using an HTTP proxy: url.URL.Opaque
// is treated differently when HTTP_PROXY is set!
// See https://github.com/golang/go/issues/5684#issuecomment-66080888
//
// This means we are subject to the behavior of net/url's function `shouldEscape`
// (as invoked with mode=encodePath): https://github.com/golang/go/blob/go1.12.7/src/net/url/url.go#L101
if header.Value == "*" {
h1.URL.Path = "*"
continue
}
// Due to the behavior of validation.ValidateUrl, h1.URL may
// already have a partial value, with or without a trailing slash.
base := h1.URL.String()
base = strings.TrimRight(base, "/")
// But we know :path begins with '/', because we handled '*' above - see RFC7540
requestURL, err := url.Parse(base + header.Value)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("invalid path '%v'", header.Value))
}
h1.URL = requestURL
case "content-length":
contentLength, err := strconv.ParseInt(header.Value, 10, 64)
if err != nil {
return fmt.Errorf("unparseable content length")
}
h1.ContentLength = contentLength
case RequestUserHeaders:
// Do not forward the serialized headers to the origin -- deserialize them, and ditch the serialized version
// Find and parse user headers serialized into a single one
userHeaders, err := DeserializeHeaders(header.Value)
if err != nil {
return errors.Wrap(err, "Unable to parse user headers")
}
for _, userHeader := range userHeaders {
h1.Header.Add(userHeader.Name, userHeader.Value)
}
default:
// All other control headers shall just be proxied transparently
h1.Header.Add(header.Name, header.Value)
}
}
return nil
}
func H1ResponseToH2ResponseHeaders(status int, h1 http.Header) (h2 []h2mux.Header) {
h2 = []h2mux.Header{
{Name: ":status", Value: strconv.Itoa(status)},
}
userHeaders := make(http.Header, len(h1))
for header, values := range h1 {
h2name := strings.ToLower(header)
if h2name == "content-length" {
// This header has meaning in HTTP/2 and will be used by the edge,
// so it should be sent as an HTTP/2 response header.
// Since these are http2 headers, they're required to be lowercase
h2 = append(h2, h2mux.Header{Name: "content-length", Value: values[0]})
} else if !IsH2muxControlResponseHeader(h2name) || IsWebsocketClientHeader(h2name) {
// User headers, on the other hand, must all be serialized so that
// HTTP/2 header validation won't be applied to HTTP/1 header values
userHeaders[header] = values
}
}
// Perform user header serialization and set them in the single header
h2 = append(h2, h2mux.Header{Name: ResponseUserHeaders, Value: SerializeHeaders(userHeaders)})
return h2
}
// IsH2muxControlRequestHeader is called in the direction of eyeball -> origin.
func IsH2muxControlRequestHeader(headerName string) bool {
return headerName == "content-length" ||
headerName == "connection" || headerName == "upgrade" || // Websocket request headers
strings.HasPrefix(headerName, ":") ||
strings.HasPrefix(headerName, "cf-")
}
// IsH2muxControlResponseHeader is called in the direction of eyeball <- origin.
func IsH2muxControlResponseHeader(headerName string) bool {
return headerName == "content-length" ||
strings.HasPrefix(headerName, ":") ||
strings.HasPrefix(headerName, "cf-int-") ||
strings.HasPrefix(headerName, "cf-cloudflared-")
}
+642
View File
@@ -0,0 +1,642 @@
package connection
import (
"fmt"
"math/rand"
"net/http"
"net/url"
"reflect"
"regexp"
"strings"
"testing"
"testing/quick"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/cloudflare/cloudflared/h2mux"
)
type ByName []h2mux.Header
func (a ByName) Len() int { return len(a) }
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByName) Less(i, j int) bool {
if a[i].Name == a[j].Name {
return a[i].Value < a[j].Value
}
return a[i].Name < a[j].Name
}
func TestH2RequestHeadersToH1Request_RegularHeaders(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err)
mockHeaders := http.Header{
"Mock header 1": {"Mock value 1"},
"Mock header 2": {"Mock value 2"},
}
headersConversionErr := H2RequestHeadersToH1Request(createSerializedHeaders(RequestUserHeaders, mockHeaders), request)
assert.True(t, reflect.DeepEqual(mockHeaders, request.Header))
assert.NoError(t, headersConversionErr)
}
func createSerializedHeaders(headersField string, headers http.Header) []h2mux.Header {
return []h2mux.Header{{
Name: headersField,
Value: SerializeHeaders(headers),
}}
}
func TestH2RequestHeadersToH1Request_NoHeaders(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err)
emptyHeaders := make(http.Header)
headersConversionErr := H2RequestHeadersToH1Request(
[]h2mux.Header{{
Name: RequestUserHeaders,
Value: SerializeHeaders(emptyHeaders),
}},
request,
)
assert.True(t, reflect.DeepEqual(emptyHeaders, request.Header))
assert.NoError(t, headersConversionErr)
}
func TestH2RequestHeadersToH1Request_InvalidHostPath(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err)
mockRequestHeaders := []h2mux.Header{
{Name: ":path", Value: "//bad_path/"},
{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
}
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
assert.Equal(t, http.Header{
"Mock header": []string{"Mock value"},
}, request.Header)
assert.Equal(t, "http://example.com//bad_path/", request.URL.String())
assert.NoError(t, headersConversionErr)
}
func TestH2RequestHeadersToH1Request_HostPathWithQuery(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
assert.NoError(t, err)
mockRequestHeaders := []h2mux.Header{
{Name: ":path", Value: "/?query=mock%20value"},
{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
}
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
assert.Equal(t, http.Header{
"Mock header": []string{"Mock value"},
}, request.Header)
assert.Equal(t, "http://example.com/?query=mock%20value", request.URL.String())
assert.NoError(t, headersConversionErr)
}
func TestH2RequestHeadersToH1Request_HostPathWithURLEncoding(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
assert.NoError(t, err)
mockRequestHeaders := []h2mux.Header{
{Name: ":path", Value: "/mock%20path"},
{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
}
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
assert.Equal(t, http.Header{
"Mock header": []string{"Mock value"},
}, request.Header)
assert.Equal(t, "http://example.com/mock%20path", request.URL.String())
assert.NoError(t, headersConversionErr)
}
func TestH2RequestHeadersToH1Request_WeirdURLs(t *testing.T) {
type testCase struct {
path string
want string
}
testCases := []testCase{
{
path: "",
want: "",
},
{
path: "/",
want: "/",
},
{
path: "//",
want: "//",
},
{
path: "/test",
want: "/test",
},
{
path: "//test",
want: "//test",
},
{
// https://github.com/cloudflare/cloudflared/issues/81
path: "//test/",
want: "//test/",
},
{
path: "/%2Ftest",
want: "/%2Ftest",
},
{
path: "//%20test",
want: "//%20test",
},
{
// https://github.com/cloudflare/cloudflared/issues/124
path: "/test?get=somthing%20a",
want: "/test?get=somthing%20a",
},
{
path: "/%20",
want: "/%20",
},
{
// stdlib's EscapedPath() will always percent-encode ' '
path: "/ ",
want: "/%20",
},
{
path: "/ a ",
want: "/%20a%20",
},
{
path: "/a%20b",
want: "/a%20b",
},
{
path: "/foo/bar;param?query#frag",
want: "/foo/bar;param?query#frag",
},
{
// stdlib's EscapedPath() will always percent-encode non-ASCII chars
path: "/a␠b",
want: "/a%E2%90%A0b",
},
{
path: "/a-umlaut-ä",
want: "/a-umlaut-%C3%A4",
},
{
path: "/a-umlaut-%C3%A4",
want: "/a-umlaut-%C3%A4",
},
{
path: "/a-umlaut-%c3%a4",
want: "/a-umlaut-%c3%a4",
},
{
// here the second '#' is treated as part of the fragment
path: "/a#b#c",
want: "/a#b%23c",
},
{
path: "/a#b␠c",
want: "/a#b%E2%90%A0c",
},
{
path: "/a#b%20c",
want: "/a#b%20c",
},
{
path: "/a#b c",
want: "/a#b%20c",
},
{
// stdlib's EscapedPath() will always percent-encode '\'
path: "/\\",
want: "/%5C",
},
{
path: "/a\\",
want: "/a%5C",
},
{
path: "/a,b.c.",
want: "/a,b.c.",
},
{
path: "/.",
want: "/.",
},
{
// stdlib's EscapedPath() will always percent-encode '`'
path: "/a`",
want: "/a%60",
},
{
path: "/a[0]",
want: "/a[0]",
},
{
path: "/?a[0]=5 &b[]=",
want: "/?a[0]=5 &b[]=",
},
{
path: "/?a=%22b%20%22",
want: "/?a=%22b%20%22",
},
}
for index, testCase := range testCases {
requestURL := "https://example.com"
request, err := http.NewRequest(http.MethodGet, requestURL, nil)
assert.NoError(t, err)
mockRequestHeaders := []h2mux.Header{
{Name: ":path", Value: testCase.path},
{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
}
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
assert.NoError(t, headersConversionErr)
assert.Equal(t,
http.Header{
"Mock header": []string{"Mock value"},
},
request.Header)
assert.Equal(t,
"https://example.com"+testCase.want,
request.URL.String(),
"Failed URL index: %v %#v", index, testCase)
}
}
func TestH2RequestHeadersToH1Request_QuickCheck(t *testing.T) {
config := &quick.Config{
Values: func(args []reflect.Value, rand *rand.Rand) {
args[0] = reflect.ValueOf(randomHTTP2Path(t, rand))
},
}
type testOrigin struct {
url string
expectedScheme string
expectedBasePath string
}
testOrigins := []testOrigin{
{
url: "http://origin.hostname.example.com:8080",
expectedScheme: "http",
expectedBasePath: "http://origin.hostname.example.com:8080",
},
{
url: "http://origin.hostname.example.com:8080/",
expectedScheme: "http",
expectedBasePath: "http://origin.hostname.example.com:8080",
},
{
url: "http://origin.hostname.example.com:8080/api",
expectedScheme: "http",
expectedBasePath: "http://origin.hostname.example.com:8080/api",
},
{
url: "http://origin.hostname.example.com:8080/api/",
expectedScheme: "http",
expectedBasePath: "http://origin.hostname.example.com:8080/api",
},
{
url: "https://origin.hostname.example.com:8080/api",
expectedScheme: "https",
expectedBasePath: "https://origin.hostname.example.com:8080/api",
},
}
// use multiple schemes to demonstrate that the URL is based on the
// origin's scheme, not the :scheme header
for _, testScheme := range []string{"http", "https"} {
for _, testOrigin := range testOrigins {
assertion := func(testPath string) bool {
const expectedMethod = "POST"
const expectedHostname = "request.hostname.example.com"
h2 := []h2mux.Header{
{Name: ":method", Value: expectedMethod},
{Name: ":scheme", Value: testScheme},
{Name: ":authority", Value: expectedHostname},
{Name: ":path", Value: testPath},
{Name: RequestUserHeaders, Value: ""},
}
h1, err := http.NewRequest("GET", testOrigin.url, nil)
require.NoError(t, err)
err = H2RequestHeadersToH1Request(h2, h1)
return assert.NoError(t, err) &&
assert.Equal(t, expectedMethod, h1.Method) &&
assert.Equal(t, expectedHostname, h1.Host) &&
assert.Equal(t, testOrigin.expectedScheme, h1.URL.Scheme) &&
assert.Equal(t, testOrigin.expectedBasePath+testPath, h1.URL.String())
}
err := quick.Check(assertion, config)
assert.NoError(t, err)
}
}
}
func randomASCIIPrintableChar(rand *rand.Rand) int {
// smallest printable ASCII char is 32, largest is 126
const startPrintable = 32
const endPrintable = 127
return startPrintable + rand.Intn(endPrintable-startPrintable)
}
// randomASCIIText generates an ASCII string, some of whose characters may be
// percent-encoded. Its "logical length" (ignoring percent-encoding) is
// between 1 and `maxLength`.
func randomASCIIText(rand *rand.Rand, minLength int, maxLength int) string {
length := minLength + rand.Intn(maxLength)
var result strings.Builder
for i := 0; i < length; i++ {
c := randomASCIIPrintableChar(rand)
// 1/4 chance of using percent encoding when not necessary
if c == '%' || rand.Intn(4) == 0 {
result.WriteString(fmt.Sprintf("%%%02X", c))
} else {
result.WriteByte(byte(c))
}
}
return result.String()
}
// Calls `randomASCIIText` and ensures the result is a valid URL path,
// i.e. one that can pass unchanged through url.URL.String()
func randomHTTP1Path(t *testing.T, rand *rand.Rand, minLength int, maxLength int) string {
text := randomASCIIText(rand, minLength, maxLength)
re, err := regexp.Compile("[^/;,]*")
require.NoError(t, err)
return "/" + re.ReplaceAllStringFunc(text, url.PathEscape)
}
// Calls `randomASCIIText` and ensures the result is a valid URL query,
// i.e. one that can pass unchanged through url.URL.String()
func randomHTTP1Query(rand *rand.Rand, minLength int, maxLength int) string {
text := randomASCIIText(rand, minLength, maxLength)
return "?" + strings.ReplaceAll(text, "#", "%23")
}
// Calls `randomASCIIText` and ensures the result is a valid URL fragment,
// i.e. one that can pass unchanged through url.URL.String()
func randomHTTP1Fragment(t *testing.T, rand *rand.Rand, minLength int, maxLength int) string {
text := randomASCIIText(rand, minLength, maxLength)
u, err := url.Parse("#" + text)
require.NoError(t, err)
return u.String()
}
// Assemble a random :path pseudoheader that is legal by Go stdlib standards
// (i.e. all characters will satisfy "net/url".shouldEscape for their respective locations)
func randomHTTP2Path(t *testing.T, rand *rand.Rand) string {
result := randomHTTP1Path(t, rand, 1, 64)
if rand.Intn(2) == 1 {
result += randomHTTP1Query(rand, 1, 32)
}
if rand.Intn(2) == 1 {
result += randomHTTP1Fragment(t, rand, 1, 16)
}
return result
}
func stdlibHeaderToH2muxHeader(headers http.Header) (h2muxHeaders []h2mux.Header) {
for name, values := range headers {
for _, value := range values {
h2muxHeaders = append(h2muxHeaders, h2mux.Header{Name: name, Value: value})
}
}
return h2muxHeaders
}
func TestParseRequestHeaders(t *testing.T) {
mockUserHeadersToSerialize := http.Header{
"Mock-Header-One": {"1", "1.5"},
"Mock-Header-Two": {"2"},
"Mock-Header-Three": {"3"},
}
mockHeaders := []h2mux.Header{
{Name: "One", Value: "1"}, // will be dropped
{Name: "Cf-Two", Value: "cf-value-1"},
{Name: "Cf-Two", Value: "cf-value-2"},
{Name: RequestUserHeaders, Value: SerializeHeaders(mockUserHeadersToSerialize)},
}
expectedHeaders := []h2mux.Header{
{Name: "Cf-Two", Value: "cf-value-1"},
{Name: "Cf-Two", Value: "cf-value-2"},
{Name: "Mock-Header-One", Value: "1"},
{Name: "Mock-Header-One", Value: "1.5"},
{Name: "Mock-Header-Two", Value: "2"},
{Name: "Mock-Header-Three", Value: "3"},
}
h1 := &http.Request{
Header: make(http.Header),
}
err := H2RequestHeadersToH1Request(mockHeaders, h1)
assert.NoError(t, err)
assert.ElementsMatch(t, expectedHeaders, stdlibHeaderToH2muxHeader(h1.Header))
}
func TestIsH2muxControlRequestHeader(t *testing.T) {
controlRequestHeaders := []string{
// Anything that begins with cf-
"cf-sample-header",
// Any http2 pseudoheader
":sample-pseudo-header",
// content-length is a special case, it has to be there
// for some requests to work (per the HTTP2 spec)
"content-length",
// Websocket request headers
"connection",
"upgrade",
}
for _, header := range controlRequestHeaders {
assert.True(t, IsH2muxControlRequestHeader(header))
}
}
func TestIsH2muxControlResponseHeader(t *testing.T) {
controlResponseHeaders := []string{
// Anything that begins with cf-int- or cf-cloudflared-
"cf-int-sample-header",
"cf-cloudflared-sample-header",
// Any http2 pseudoheader
":sample-pseudo-header",
// content-length is a special case, it has to be there
// for some requests to work (per the HTTP2 spec)
"content-length",
}
for _, header := range controlResponseHeaders {
assert.True(t, IsH2muxControlResponseHeader(header))
}
}
func TestIsNotH2muxControlRequestHeader(t *testing.T) {
notControlRequestHeaders := []string{
"mock-header",
"another-sample-header",
}
for _, header := range notControlRequestHeaders {
assert.False(t, IsH2muxControlRequestHeader(header))
}
}
func TestIsNotH2muxControlResponseHeader(t *testing.T) {
notControlResponseHeaders := []string{
"mock-header",
"another-sample-header",
"upgrade",
"connection",
"cf-whatever", // On the response path, we only want to filter cf-int- and cf-cloudflared-
}
for _, header := range notControlResponseHeaders {
assert.False(t, IsH2muxControlResponseHeader(header))
}
}
func TestH1ResponseToH2ResponseHeaders(t *testing.T) {
mockHeaders := http.Header{
"User-header-one": {""},
"User-header-two": {"1", "2"},
"cf-header": {"cf-value"},
"cf-int-header": {"cf-int-value"},
"cf-cloudflared-header": {"cf-cloudflared-value"},
"Content-Length": {"123"},
}
mockResponse := http.Response{
StatusCode: 200,
Header: mockHeaders,
}
headers := H1ResponseToH2ResponseHeaders(mockResponse.StatusCode, mockResponse.Header)
serializedHeadersIndex := -1
for i, header := range headers {
if header.Name == ResponseUserHeaders {
serializedHeadersIndex = i
break
}
}
assert.NotEqual(t, -1, serializedHeadersIndex)
actualControlHeaders := append(
headers[:serializedHeadersIndex],
headers[serializedHeadersIndex+1:]...,
)
expectedControlHeaders := []h2mux.Header{
{Name: ":status", Value: "200"},
{Name: "content-length", Value: "123"},
}
assert.ElementsMatch(t, expectedControlHeaders, actualControlHeaders)
actualUserHeaders, err := DeserializeHeaders(headers[serializedHeadersIndex].Value)
expectedUserHeaders := []h2mux.Header{
{Name: "User-header-one", Value: ""},
{Name: "User-header-two", Value: "1"},
{Name: "User-header-two", Value: "2"},
{Name: "cf-header", Value: "cf-value"},
}
assert.NoError(t, err)
assert.ElementsMatch(t, expectedUserHeaders, actualUserHeaders)
}
// The purpose of this test is to check that our code and the http.Header
// implementation don't throw validation errors about header size
func TestHeaderSize(t *testing.T) {
largeValue := randSeq(5 * 1024 * 1024) // 5Mb
largeHeaders := http.Header{
"User-header": {largeValue},
}
mockResponse := http.Response{
StatusCode: 200,
Header: largeHeaders,
}
serializedHeaders := H1ResponseToH2ResponseHeaders(mockResponse.StatusCode, mockResponse.Header)
request, err := http.NewRequest(http.MethodGet, "https://example.com/", nil)
assert.NoError(t, err)
for _, header := range serializedHeaders {
request.Header.Set(header.Name, header.Value)
}
for _, header := range serializedHeaders {
if header.Name != ResponseUserHeaders {
continue
}
deserializedHeaders, err := DeserializeHeaders(header.Value)
assert.NoError(t, err)
assert.Equal(t, largeValue, deserializedHeaders[0].Value)
}
}
func randSeq(n int) string {
randomizer := rand.New(rand.NewSource(17))
var letters = []rune(":;,+/=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
b := make([]rune, n)
for i := range b {
b[i] = letters[randomizer.Intn(len(letters))]
}
return string(b)
}
func BenchmarkH1ResponseToH2ResponseHeaders(b *testing.B) {
ser := "eC1mb3J3YXJkZWQtcHJvdG8:aHR0cHM;dXBncmFkZS1pbnNlY3VyZS1yZXF1ZXN0cw:MQ;YWNjZXB0LWxhbmd1YWdl:ZW4tVVMsZW47cT0wLjkscnU7cT0wLjg;YWNjZXB0LWVuY29kaW5n:Z3ppcA;eC1mb3J3YXJkZWQtZm9y:MTczLjI0NS42MC42;dXNlci1hZ2VudA:TW96aWxsYS81LjAgKE1hY2ludG9zaDsgSW50ZWwgTWFjIE9TIFggMTBfMTRfNikgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzg0LjAuNDE0Ny44OSBTYWZhcmkvNTM3LjM2;c2VjLWZldGNoLW1vZGU:bmF2aWdhdGU;Y2RuLWxvb3A:Y2xvdWRmbGFyZQ;c2VjLWZldGNoLWRlc3Q:ZG9jdW1lbnQ;c2VjLWZldGNoLXVzZXI:PzE;c2VjLWZldGNoLXNpdGU:bm9uZQ;Y29va2ll:X19jZmR1aWQ9ZGNkOWZjOGNjNWMxMzE0NTMyYTFkMjhlZDEyOWRhOTYwMTU2OTk1MTYzNDsgX19jZl9ibT1mYzY2MzMzYzAzZmM0MWFiZTZmOWEyYzI2ZDUwOTA0YzIxYzZhMTQ2LTE1OTU2MjIzNDEtMTgwMC1BZTVzS2pIU2NiWGVFM05mMUhrTlNQMG1tMHBLc2pQWkloVnM1Z2g1SkNHQkFhS1UxVDB2b003alBGN3FjMHVSR2NjZGcrWHdhL1EzbTJhQzdDVU4xZ2M9;YWNjZXB0:dGV4dC9odG1sLGFwcGxpY2F0aW9uL3hodG1sK3htbCxhcHBsaWNhdGlvbi94bWw7cT0wLjksaW1hZ2Uvd2VicCxpbWFnZS9hcG5nLCovKjtxPTAuOCxhcHBsaWNhdGlvbi9zaWduZWQtZXhjaGFuZ2U7dj1iMztxPTAuOQ"
h2, _ := DeserializeHeaders(ser)
h1 := make(http.Header)
for _, header := range h2 {
h1.Add(header.Name, header.Value)
}
h1.Add("Content-Length", "200")
h1.Add("Cf-Something", "Else")
h1.Add("Upgrade", "websocket")
h1resp := &http.Response{
StatusCode: 200,
Header: h1,
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = H1ResponseToH2ResponseHeaders(h1resp.StatusCode, h1resp.Header)
}
}
+2 -113
View File
@@ -4,8 +4,6 @@ import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/pkg/errors"
@@ -44,93 +42,9 @@ func mustInitRespMetaHeader(src string) string {
var headerEncoding = base64.RawStdEncoding
// note: all h2mux headers should be lower-case (http/2 style)
const ()
// H2RequestHeadersToH1Request converts the HTTP/2 headers coming from origintunneld
// to an HTTP/1 Request object destined for the local origin web service.
// This operation includes conversion of the pseudo-headers into their closest
// HTTP/1 equivalents. See https://tools.ietf.org/html/rfc7540#section-8.1.2.3
func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error {
for _, header := range h2 {
name := strings.ToLower(header.Name)
if !IsControlRequestHeader(name) {
continue
}
switch name {
case ":method":
h1.Method = header.Value
case ":scheme":
// noop - use the preexisting scheme from h1.URL
case ":authority":
// Otherwise the host header will be based on the origin URL
h1.Host = header.Value
case ":path":
// We don't want to be an "opinionated" proxy, so ideally we would use :path as-is.
// However, this HTTP/1 Request object belongs to the Go standard library,
// whose URL package makes some opinionated decisions about the encoding of
// URL characters: see the docs of https://godoc.org/net/url#URL,
// in particular the EscapedPath method https://godoc.org/net/url#URL.EscapedPath,
// which is always used when computing url.URL.String(), whether we'd like it or not.
//
// Well, not *always*. We could circumvent this by using url.URL.Opaque. But
// that would present unusual difficulties when using an HTTP proxy: url.URL.Opaque
// is treated differently when HTTP_PROXY is set!
// See https://github.com/golang/go/issues/5684#issuecomment-66080888
//
// This means we are subject to the behavior of net/url's function `shouldEscape`
// (as invoked with mode=encodePath): https://github.com/golang/go/blob/go1.12.7/src/net/url/url.go#L101
if header.Value == "*" {
h1.URL.Path = "*"
continue
}
// Due to the behavior of validation.ValidateUrl, h1.URL may
// already have a partial value, with or without a trailing slash.
base := h1.URL.String()
base = strings.TrimRight(base, "/")
// But we know :path begins with '/', because we handled '*' above - see RFC7540
requestURL, err := url.Parse(base + header.Value)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("invalid path '%v'", header.Value))
}
h1.URL = requestURL
case "content-length":
contentLength, err := strconv.ParseInt(header.Value, 10, 64)
if err != nil {
return fmt.Errorf("unparseable content length")
}
h1.ContentLength = contentLength
case RequestUserHeaders:
// Do not forward the serialized headers to the origin -- deserialize them, and ditch the serialized version
// Find and parse user headers serialized into a single one
userHeaders, err := DeserializeHeaders(header.Value)
if err != nil {
return errors.Wrap(err, "Unable to parse user headers")
}
for _, userHeader := range userHeaders {
h1.Header.Add(userHeader.Name, userHeader.Value)
}
default:
// All other control headers shall just be proxied transparently
h1.Header.Add(header.Name, header.Value)
}
}
return nil
}
func IsControlRequestHeader(headerName string) bool {
return headerName == "content-length" ||
headerName == "connection" || headerName == "upgrade" || // Websocket request headers
strings.HasPrefix(headerName, ":") ||
strings.HasPrefix(headerName, "cf-")
}
// IsControlResponseHeader is called in the direction of eyeball <- origin.
func IsControlResponseHeader(headerName string) bool {
return headerName == "content-length" ||
strings.HasPrefix(headerName, ":") ||
return strings.HasPrefix(headerName, ":") ||
strings.HasPrefix(headerName, "cf-int-") ||
strings.HasPrefix(headerName, "cf-cloudflared-")
}
@@ -142,31 +56,6 @@ func IsWebsocketClientHeader(headerName string) bool {
headerName == "upgrade"
}
func H1ResponseToH2ResponseHeaders(status int, h1 http.Header) (h2 []h2mux.Header) {
h2 = []h2mux.Header{
{Name: ":status", Value: strconv.Itoa(status)},
}
userHeaders := make(http.Header, len(h1))
for header, values := range h1 {
h2name := strings.ToLower(header)
if h2name == "content-length" {
// This header has meaning in HTTP/2 and will be used by the edge,
// so it should be sent as an HTTP/2 response header.
// Since these are http2 headers, they're required to be lowercase
h2 = append(h2, h2mux.Header{Name: "content-length", Value: values[0]})
} else if !IsControlResponseHeader(h2name) || IsWebsocketClientHeader(h2name) {
// User headers, on the other hand, must all be serialized so that
// HTTP/2 header validation won't be applied to HTTP/1 header values
userHeaders[header] = values
}
}
// Perform user header serialization and set them in the single header
h2 = append(h2, h2mux.Header{Name: ResponseUserHeaders, Value: SerializeHeaders(userHeaders)})
return h2
}
// Serialize HTTP1.x headers by base64-encoding each header name and value,
// and then joining them in the format of [key:value;]
func SerializeHeaders(h1Headers http.Header) string {
-604
View File
@@ -2,441 +2,14 @@ package connection
import (
"fmt"
"math/rand"
"net/http"
"net/url"
"reflect"
"regexp"
"sort"
"strings"
"testing"
"testing/quick"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/cloudflare/cloudflared/h2mux"
)
type ByName []h2mux.Header
func (a ByName) Len() int { return len(a) }
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByName) Less(i, j int) bool {
if a[i].Name == a[j].Name {
return a[i].Value < a[j].Value
}
return a[i].Name < a[j].Name
}
func TestH2RequestHeadersToH1Request_RegularHeaders(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err)
mockHeaders := http.Header{
"Mock header 1": {"Mock value 1"},
"Mock header 2": {"Mock value 2"},
}
headersConversionErr := H2RequestHeadersToH1Request(createSerializedHeaders(RequestUserHeaders, mockHeaders), request)
assert.True(t, reflect.DeepEqual(mockHeaders, request.Header))
assert.NoError(t, headersConversionErr)
}
func createSerializedHeaders(headersField string, headers http.Header) []h2mux.Header {
return []h2mux.Header{{
Name: headersField,
Value: SerializeHeaders(headers),
}}
}
func TestH2RequestHeadersToH1Request_NoHeaders(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err)
emptyHeaders := make(http.Header)
headersConversionErr := H2RequestHeadersToH1Request(
[]h2mux.Header{{
Name: RequestUserHeaders,
Value: SerializeHeaders(emptyHeaders),
}},
request,
)
assert.True(t, reflect.DeepEqual(emptyHeaders, request.Header))
assert.NoError(t, headersConversionErr)
}
func TestH2RequestHeadersToH1Request_InvalidHostPath(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err)
mockRequestHeaders := []h2mux.Header{
{Name: ":path", Value: "//bad_path/"},
{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
}
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
assert.Equal(t, http.Header{
"Mock header": []string{"Mock value"},
}, request.Header)
assert.Equal(t, "http://example.com//bad_path/", request.URL.String())
assert.NoError(t, headersConversionErr)
}
func TestH2RequestHeadersToH1Request_HostPathWithQuery(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
assert.NoError(t, err)
mockRequestHeaders := []h2mux.Header{
{Name: ":path", Value: "/?query=mock%20value"},
{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
}
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
assert.Equal(t, http.Header{
"Mock header": []string{"Mock value"},
}, request.Header)
assert.Equal(t, "http://example.com/?query=mock%20value", request.URL.String())
assert.NoError(t, headersConversionErr)
}
func TestH2RequestHeadersToH1Request_HostPathWithURLEncoding(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
assert.NoError(t, err)
mockRequestHeaders := []h2mux.Header{
{Name: ":path", Value: "/mock%20path"},
{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
}
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
assert.Equal(t, http.Header{
"Mock header": []string{"Mock value"},
}, request.Header)
assert.Equal(t, "http://example.com/mock%20path", request.URL.String())
assert.NoError(t, headersConversionErr)
}
func TestH2RequestHeadersToH1Request_WeirdURLs(t *testing.T) {
type testCase struct {
path string
want string
}
testCases := []testCase{
{
path: "",
want: "",
},
{
path: "/",
want: "/",
},
{
path: "//",
want: "//",
},
{
path: "/test",
want: "/test",
},
{
path: "//test",
want: "//test",
},
{
// https://github.com/cloudflare/cloudflared/issues/81
path: "//test/",
want: "//test/",
},
{
path: "/%2Ftest",
want: "/%2Ftest",
},
{
path: "//%20test",
want: "//%20test",
},
{
// https://github.com/cloudflare/cloudflared/issues/124
path: "/test?get=somthing%20a",
want: "/test?get=somthing%20a",
},
{
path: "/%20",
want: "/%20",
},
{
// stdlib's EscapedPath() will always percent-encode ' '
path: "/ ",
want: "/%20",
},
{
path: "/ a ",
want: "/%20a%20",
},
{
path: "/a%20b",
want: "/a%20b",
},
{
path: "/foo/bar;param?query#frag",
want: "/foo/bar;param?query#frag",
},
{
// stdlib's EscapedPath() will always percent-encode non-ASCII chars
path: "/a␠b",
want: "/a%E2%90%A0b",
},
{
path: "/a-umlaut-ä",
want: "/a-umlaut-%C3%A4",
},
{
path: "/a-umlaut-%C3%A4",
want: "/a-umlaut-%C3%A4",
},
{
path: "/a-umlaut-%c3%a4",
want: "/a-umlaut-%c3%a4",
},
{
// here the second '#' is treated as part of the fragment
path: "/a#b#c",
want: "/a#b%23c",
},
{
path: "/a#b␠c",
want: "/a#b%E2%90%A0c",
},
{
path: "/a#b%20c",
want: "/a#b%20c",
},
{
path: "/a#b c",
want: "/a#b%20c",
},
{
// stdlib's EscapedPath() will always percent-encode '\'
path: "/\\",
want: "/%5C",
},
{
path: "/a\\",
want: "/a%5C",
},
{
path: "/a,b.c.",
want: "/a,b.c.",
},
{
path: "/.",
want: "/.",
},
{
// stdlib's EscapedPath() will always percent-encode '`'
path: "/a`",
want: "/a%60",
},
{
path: "/a[0]",
want: "/a[0]",
},
{
path: "/?a[0]=5 &b[]=",
want: "/?a[0]=5 &b[]=",
},
{
path: "/?a=%22b%20%22",
want: "/?a=%22b%20%22",
},
}
for index, testCase := range testCases {
requestURL := "https://example.com"
request, err := http.NewRequest(http.MethodGet, requestURL, nil)
assert.NoError(t, err)
mockRequestHeaders := []h2mux.Header{
{Name: ":path", Value: testCase.path},
{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
}
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
assert.NoError(t, headersConversionErr)
assert.Equal(t,
http.Header{
"Mock header": []string{"Mock value"},
},
request.Header)
assert.Equal(t,
"https://example.com"+testCase.want,
request.URL.String(),
"Failed URL index: %v %#v", index, testCase)
}
}
func TestH2RequestHeadersToH1Request_QuickCheck(t *testing.T) {
config := &quick.Config{
Values: func(args []reflect.Value, rand *rand.Rand) {
args[0] = reflect.ValueOf(randomHTTP2Path(t, rand))
},
}
type testOrigin struct {
url string
expectedScheme string
expectedBasePath string
}
testOrigins := []testOrigin{
{
url: "http://origin.hostname.example.com:8080",
expectedScheme: "http",
expectedBasePath: "http://origin.hostname.example.com:8080",
},
{
url: "http://origin.hostname.example.com:8080/",
expectedScheme: "http",
expectedBasePath: "http://origin.hostname.example.com:8080",
},
{
url: "http://origin.hostname.example.com:8080/api",
expectedScheme: "http",
expectedBasePath: "http://origin.hostname.example.com:8080/api",
},
{
url: "http://origin.hostname.example.com:8080/api/",
expectedScheme: "http",
expectedBasePath: "http://origin.hostname.example.com:8080/api",
},
{
url: "https://origin.hostname.example.com:8080/api",
expectedScheme: "https",
expectedBasePath: "https://origin.hostname.example.com:8080/api",
},
}
// use multiple schemes to demonstrate that the URL is based on the
// origin's scheme, not the :scheme header
for _, testScheme := range []string{"http", "https"} {
for _, testOrigin := range testOrigins {
assertion := func(testPath string) bool {
const expectedMethod = "POST"
const expectedHostname = "request.hostname.example.com"
h2 := []h2mux.Header{
{Name: ":method", Value: expectedMethod},
{Name: ":scheme", Value: testScheme},
{Name: ":authority", Value: expectedHostname},
{Name: ":path", Value: testPath},
{Name: RequestUserHeaders, Value: ""},
}
h1, err := http.NewRequest("GET", testOrigin.url, nil)
require.NoError(t, err)
err = H2RequestHeadersToH1Request(h2, h1)
return assert.NoError(t, err) &&
assert.Equal(t, expectedMethod, h1.Method) &&
assert.Equal(t, expectedHostname, h1.Host) &&
assert.Equal(t, testOrigin.expectedScheme, h1.URL.Scheme) &&
assert.Equal(t, testOrigin.expectedBasePath+testPath, h1.URL.String())
}
err := quick.Check(assertion, config)
assert.NoError(t, err)
}
}
}
func randomASCIIPrintableChar(rand *rand.Rand) int {
// smallest printable ASCII char is 32, largest is 126
const startPrintable = 32
const endPrintable = 127
return startPrintable + rand.Intn(endPrintable-startPrintable)
}
// randomASCIIText generates an ASCII string, some of whose characters may be
// percent-encoded. Its "logical length" (ignoring percent-encoding) is
// between 1 and `maxLength`.
func randomASCIIText(rand *rand.Rand, minLength int, maxLength int) string {
length := minLength + rand.Intn(maxLength)
var result strings.Builder
for i := 0; i < length; i++ {
c := randomASCIIPrintableChar(rand)
// 1/4 chance of using percent encoding when not necessary
if c == '%' || rand.Intn(4) == 0 {
result.WriteString(fmt.Sprintf("%%%02X", c))
} else {
result.WriteByte(byte(c))
}
}
return result.String()
}
// Calls `randomASCIIText` and ensures the result is a valid URL path,
// i.e. one that can pass unchanged through url.URL.String()
func randomHTTP1Path(t *testing.T, rand *rand.Rand, minLength int, maxLength int) string {
text := randomASCIIText(rand, minLength, maxLength)
re, err := regexp.Compile("[^/;,]*")
require.NoError(t, err)
return "/" + re.ReplaceAllStringFunc(text, url.PathEscape)
}
// Calls `randomASCIIText` and ensures the result is a valid URL query,
// i.e. one that can pass unchanged through url.URL.String()
func randomHTTP1Query(rand *rand.Rand, minLength int, maxLength int) string {
text := randomASCIIText(rand, minLength, maxLength)
return "?" + strings.ReplaceAll(text, "#", "%23")
}
// Calls `randomASCIIText` and ensures the result is a valid URL fragment,
// i.e. one that can pass unchanged through url.URL.String()
func randomHTTP1Fragment(t *testing.T, rand *rand.Rand, minLength int, maxLength int) string {
text := randomASCIIText(rand, minLength, maxLength)
u, err := url.Parse("#" + text)
require.NoError(t, err)
return u.String()
}
// Assemble a random :path pseudoheader that is legal by Go stdlib standards
// (i.e. all characters will satisfy "net/url".shouldEscape for their respective locations)
func randomHTTP2Path(t *testing.T, rand *rand.Rand) string {
result := randomHTTP1Path(t, rand, 1, 64)
if rand.Intn(2) == 1 {
result += randomHTTP1Query(rand, 1, 32)
}
if rand.Intn(2) == 1 {
result += randomHTTP1Fragment(t, rand, 1, 16)
}
return result
}
func stdlibHeaderToH2muxHeader(headers http.Header) (h2muxHeaders []h2mux.Header) {
for name, values := range headers {
for _, value := range values {
h2muxHeaders = append(h2muxHeaders, h2mux.Header{Name: name, Value: value})
}
}
return h2muxHeaders
}
func TestSerializeHeaders(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err)
@@ -511,70 +84,13 @@ func TestDeserializeMalformed(t *testing.T) {
}
}
func TestParseRequestHeaders(t *testing.T) {
mockUserHeadersToSerialize := http.Header{
"Mock-Header-One": {"1", "1.5"},
"Mock-Header-Two": {"2"},
"Mock-Header-Three": {"3"},
}
mockHeaders := []h2mux.Header{
{Name: "One", Value: "1"}, // will be dropped
{Name: "Cf-Two", Value: "cf-value-1"},
{Name: "Cf-Two", Value: "cf-value-2"},
{Name: RequestUserHeaders, Value: SerializeHeaders(mockUserHeadersToSerialize)},
}
expectedHeaders := []h2mux.Header{
{Name: "Cf-Two", Value: "cf-value-1"},
{Name: "Cf-Two", Value: "cf-value-2"},
{Name: "Mock-Header-One", Value: "1"},
{Name: "Mock-Header-One", Value: "1.5"},
{Name: "Mock-Header-Two", Value: "2"},
{Name: "Mock-Header-Three", Value: "3"},
}
h1 := &http.Request{
Header: make(http.Header),
}
err := H2RequestHeadersToH1Request(mockHeaders, h1)
assert.NoError(t, err)
assert.ElementsMatch(t, expectedHeaders, stdlibHeaderToH2muxHeader(h1.Header))
}
func TestIsControlRequestHeader(t *testing.T) {
controlRequestHeaders := []string{
// Anything that begins with cf-
"cf-sample-header",
// Any http2 pseudoheader
":sample-pseudo-header",
// content-length is a special case, it has to be there
// for some requests to work (per the HTTP2 spec)
"content-length",
// Websocket request headers
"connection",
"upgrade",
}
for _, header := range controlRequestHeaders {
assert.True(t, IsControlRequestHeader(header))
}
}
func TestIsControlResponseHeader(t *testing.T) {
controlResponseHeaders := []string{
// Anything that begins with cf-int- or cf-cloudflared-
"cf-int-sample-header",
"cf-cloudflared-sample-header",
// Any http2 pseudoheader
":sample-pseudo-header",
// content-length is a special case, it has to be there
// for some requests to work (per the HTTP2 spec)
"content-length",
}
for _, header := range controlResponseHeaders {
@@ -582,17 +98,6 @@ func TestIsControlResponseHeader(t *testing.T) {
}
}
func TestIsNotControlRequestHeader(t *testing.T) {
notControlRequestHeaders := []string{
"mock-header",
"another-sample-header",
}
for _, header := range notControlRequestHeaders {
assert.False(t, IsControlRequestHeader(header))
}
}
func TestIsNotControlResponseHeader(t *testing.T) {
notControlResponseHeaders := []string{
"mock-header",
@@ -606,112 +111,3 @@ func TestIsNotControlResponseHeader(t *testing.T) {
assert.False(t, IsControlResponseHeader(header))
}
}
func TestH1ResponseToH2ResponseHeaders(t *testing.T) {
mockHeaders := http.Header{
"User-header-one": {""},
"User-header-two": {"1", "2"},
"cf-header": {"cf-value"},
"cf-int-header": {"cf-int-value"},
"cf-cloudflared-header": {"cf-cloudflared-value"},
"Content-Length": {"123"},
}
mockResponse := http.Response{
StatusCode: 200,
Header: mockHeaders,
}
headers := H1ResponseToH2ResponseHeaders(mockResponse.StatusCode, mockResponse.Header)
serializedHeadersIndex := -1
for i, header := range headers {
if header.Name == ResponseUserHeaders {
serializedHeadersIndex = i
break
}
}
assert.NotEqual(t, -1, serializedHeadersIndex)
actualControlHeaders := append(
headers[:serializedHeadersIndex],
headers[serializedHeadersIndex+1:]...,
)
expectedControlHeaders := []h2mux.Header{
{Name: ":status", Value: "200"},
{Name: "content-length", Value: "123"},
}
assert.ElementsMatch(t, expectedControlHeaders, actualControlHeaders)
actualUserHeaders, err := DeserializeHeaders(headers[serializedHeadersIndex].Value)
expectedUserHeaders := []h2mux.Header{
{Name: "User-header-one", Value: ""},
{Name: "User-header-two", Value: "1"},
{Name: "User-header-two", Value: "2"},
{Name: "cf-header", Value: "cf-value"},
}
assert.NoError(t, err)
assert.ElementsMatch(t, expectedUserHeaders, actualUserHeaders)
}
// The purpose of this test is to check that our code and the http.Header
// implementation don't throw validation errors about header size
func TestHeaderSize(t *testing.T) {
largeValue := randSeq(5 * 1024 * 1024) // 5Mb
largeHeaders := http.Header{
"User-header": {largeValue},
}
mockResponse := http.Response{
StatusCode: 200,
Header: largeHeaders,
}
serializedHeaders := H1ResponseToH2ResponseHeaders(mockResponse.StatusCode, mockResponse.Header)
request, err := http.NewRequest(http.MethodGet, "https://example.com/", nil)
assert.NoError(t, err)
for _, header := range serializedHeaders {
request.Header.Set(header.Name, header.Value)
}
for _, header := range serializedHeaders {
if header.Name != ResponseUserHeaders {
continue
}
deserializedHeaders, err := DeserializeHeaders(header.Value)
assert.NoError(t, err)
assert.Equal(t, largeValue, deserializedHeaders[0].Value)
}
}
func randSeq(n int) string {
randomizer := rand.New(rand.NewSource(17))
var letters = []rune(":;,+/=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
b := make([]rune, n)
for i := range b {
b[i] = letters[randomizer.Intn(len(letters))]
}
return string(b)
}
func BenchmarkH1ResponseToH2ResponseHeaders(b *testing.B) {
ser := "eC1mb3J3YXJkZWQtcHJvdG8:aHR0cHM;dXBncmFkZS1pbnNlY3VyZS1yZXF1ZXN0cw:MQ;YWNjZXB0LWxhbmd1YWdl:ZW4tVVMsZW47cT0wLjkscnU7cT0wLjg;YWNjZXB0LWVuY29kaW5n:Z3ppcA;eC1mb3J3YXJkZWQtZm9y:MTczLjI0NS42MC42;dXNlci1hZ2VudA:TW96aWxsYS81LjAgKE1hY2ludG9zaDsgSW50ZWwgTWFjIE9TIFggMTBfMTRfNikgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzg0LjAuNDE0Ny44OSBTYWZhcmkvNTM3LjM2;c2VjLWZldGNoLW1vZGU:bmF2aWdhdGU;Y2RuLWxvb3A:Y2xvdWRmbGFyZQ;c2VjLWZldGNoLWRlc3Q:ZG9jdW1lbnQ;c2VjLWZldGNoLXVzZXI:PzE;c2VjLWZldGNoLXNpdGU:bm9uZQ;Y29va2ll:X19jZmR1aWQ9ZGNkOWZjOGNjNWMxMzE0NTMyYTFkMjhlZDEyOWRhOTYwMTU2OTk1MTYzNDsgX19jZl9ibT1mYzY2MzMzYzAzZmM0MWFiZTZmOWEyYzI2ZDUwOTA0YzIxYzZhMTQ2LTE1OTU2MjIzNDEtMTgwMC1BZTVzS2pIU2NiWGVFM05mMUhrTlNQMG1tMHBLc2pQWkloVnM1Z2g1SkNHQkFhS1UxVDB2b003alBGN3FjMHVSR2NjZGcrWHdhL1EzbTJhQzdDVU4xZ2M9;YWNjZXB0:dGV4dC9odG1sLGFwcGxpY2F0aW9uL3hodG1sK3htbCxhcHBsaWNhdGlvbi94bWw7cT0wLjksaW1hZ2Uvd2VicCxpbWFnZS9hcG5nLCovKjtxPTAuOCxhcHBsaWNhdGlvbi9zaWduZWQtZXhjaGFuZ2U7dj1iMztxPTAuOQ"
h2, _ := DeserializeHeaders(ser)
h1 := make(http.Header)
for _, header := range h2 {
h1.Add(header.Name, header.Value)
}
h1.Add("Content-Length", "200")
h1.Add("Cf-Something", "Else")
h1.Add("Upgrade", "websocket")
h1resp := &http.Response{
StatusCode: 200,
Header: h1,
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = H1ResponseToH2ResponseHeaders(h1resp.StatusCode, h1resp.Header)
}
}
+6 -4
View File
@@ -125,7 +125,7 @@ func (c *HTTP2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case TypeTCP:
host, err := getRequestHost(r)
if err != nil {
err := fmt.Errorf(`cloudflared recieved a warp-routing request with an empty host value: %w`, err)
err := fmt.Errorf(`cloudflared received a warp-routing request with an empty host value: %w`, err)
c.log.Error().Err(err)
respWriter.WriteErrorResponse()
}
@@ -184,12 +184,14 @@ func (rp *http2RespWriter) WriteRespHeaders(status int, header http.Header) erro
for name, values := range header {
// Since these are http2 headers, they're required to be lowercase
h2name := strings.ToLower(name)
if h2name == "content-length" {
// This header has meaning in HTTP/2 and will be used by the edge,
// so it should be sent as an HTTP/2 response header.
// so it should be sent *also* as an HTTP/2 response header.
dest[name] = values
// Since these are http2 headers, they're required to be lowercase
} else if !IsControlResponseHeader(h2name) || IsWebsocketClientHeader(h2name) {
}
if !IsControlResponseHeader(h2name) || IsWebsocketClientHeader(h2name) {
// User headers, on the other hand, must all be serialized so that
// HTTP/2 header validation won't be applied to HTTP/1 header values
userHeaders[name] = values
+2 -2
View File
@@ -12,7 +12,7 @@ import (
)
const (
AvailableProtocolFlagMessage = "Available protocols: http2 - Go's implementation, h2mux - Cloudflare's implementation of HTTP/2, and auto - automatically select between http2 and h2mux"
AvailableProtocolFlagMessage = "Available protocols: 'auto' - automatically chooses the best protocol over time (the default; and also the recommended one); 'quic' - based on QUIC, relying on UDP egress to Cloudflare edge; 'http2' - using Go's HTTP2 library, relying on TCP egress to Cloudflare edge; 'h2mux' - Cloudflare's implementation of HTTP/2, deprecated"
// edgeH2muxTLSServerName is the server name to establish h2mux connection with edge
edgeH2muxTLSServerName = "cftunnel.com"
// edgeH2TLSServerName is the server name to establish http2 connection with edge
@@ -42,7 +42,7 @@ const (
// H2mux on HTTP2 failure to connect.
HTTP2Warp
//QUICWarp is used only with named tunnels. It's useful for warp-routing where we want to fallback to HTTP2 but
// dont' want HTTP2 to fallback to H2mux
// don't want HTTP2 to fallback to H2mux
QUICWarp
)
+1 -1
View File
@@ -26,7 +26,7 @@ var (
func mockFetcher(getError bool, protocolPercent ...edgediscovery.ProtocolPercent) PercentageFetcher {
return func() (edgediscovery.ProtocolPercents, error) {
if getError {
return nil, fmt.Errorf("failed to fetch precentage")
return nil, fmt.Errorf("failed to fetch percentage")
}
return protocolPercent, nil
}
+97 -23
View File
@@ -9,11 +9,16 @@ import (
"net/http"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"github.com/lucas-clemente/quic-go"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"golang.org/x/sync/errgroup"
"github.com/cloudflare/cloudflared/datagramsession"
"github.com/cloudflare/cloudflared/ingress"
quicpogs "github.com/cloudflare/cloudflared/quic"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
@@ -24,16 +29,16 @@ const (
// HTTPMethodKey is used to get or set http method in QUIC ALPN if the underlying proxy connection type is HTTP.
HTTPMethodKey = "HttpMethod"
// HTTPHostKey is used to get or set http Method in QUIC ALPN if the underlying proxy connection type is HTTP.
HTTPHostKey = "HttpHost"
HTTPHostKey = "HttpHost"
MaxDatagramFrameSize = 1220
)
// QUICConnection represents the type that facilitates Proxying via QUIC streams.
type QUICConnection struct {
session quic.Session
logger *zerolog.Logger
httpProxy OriginProxy
gracefulShutdownC <-chan struct{}
stoppedGracefully bool
session quic.Session
logger *zerolog.Logger
httpProxy OriginProxy
sessionManager datagramsession.Manager
}
// NewQUICConnection returns a new instance of QUICConnection.
@@ -63,18 +68,34 @@ func NewQUICConnection(
return nil, err
}
datagramMuxer, err := quicpogs.NewDatagramMuxer(session)
if err != nil {
return nil, err
}
sessionManager := datagramsession.NewManager(datagramMuxer, observer.log)
return &QUICConnection{
session: session,
httpProxy: httpProxy,
logger: observer.log,
session: session,
httpProxy: httpProxy,
logger: observer.log,
sessionManager: sessionManager,
}, nil
}
// Serve starts a QUIC session that begins accepting streams.
func (q *QUICConnection) Serve(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
errGroup, ctx := errgroup.WithContext(ctx)
errGroup.Go(func() error {
return q.acceptStream(ctx)
})
errGroup.Go(func() error {
return q.sessionManager.Serve(ctx)
})
return errGroup.Wait()
}
func (q *QUICConnection) acceptStream(ctx context.Context) error {
for {
stream, err := q.session.AcceptStream(ctx)
if err != nil {
@@ -99,7 +120,30 @@ func (q *QUICConnection) Close() {
}
func (q *QUICConnection) handleStream(stream quic.Stream) error {
connectRequest, err := quicpogs.ReadConnectRequestData(stream)
signature, err := quicpogs.DetermineProtocol(stream)
if err != nil {
return err
}
switch signature {
case quicpogs.DataStreamProtocolSignature:
reqServerStream, err := quicpogs.NewRequestServerStream(stream, signature)
if err != nil {
return nil
}
return q.handleDataStream(reqServerStream)
case quicpogs.RPCStreamProtocolSignature:
rpcStream, err := quicpogs.NewRPCServerStream(stream, signature)
if err != nil {
return err
}
return q.handleRPCStream(rpcStream)
default:
return fmt.Errorf("unknown protocol %v", signature)
}
}
func (q *QUICConnection) handleDataStream(stream *quicpogs.RequestServerStream) error {
connectRequest, err := stream.ReadConnectRequestData()
if err != nil {
return err
}
@@ -114,32 +158,62 @@ func (q *QUICConnection) handleStream(stream quic.Stream) error {
w := newHTTPResponseAdapter(stream)
return q.httpProxy.ProxyHTTP(w, req, connectRequest.Type == quicpogs.ConnectionTypeWebsocket)
case quicpogs.ConnectionTypeTCP:
rwa := &streamReadWriteAcker{
ReadWriter: stream,
}
rwa := &streamReadWriteAcker{stream}
return q.httpProxy.ProxyTCP(context.Background(), rwa, &TCPRequest{Dest: connectRequest.Dest})
}
return nil
}
func (q *QUICConnection) handleRPCStream(rpcStream *quicpogs.RPCServerStream) error {
return rpcStream.Serve(q, q.logger)
}
func (q *QUICConnection) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration) error {
// Each session is a series of datagram from an eyeball to a dstIP:dstPort.
// (src port, dst IP, dst port) uniquely identifies a session, so it needs a dedicated connected socket.
originProxy, err := ingress.DialUDP(dstIP, dstPort)
if err != nil {
q.logger.Err(err).Msgf("Failed to create udp proxy to %s:%d", dstIP, dstPort)
return err
}
session, err := q.sessionManager.RegisterSession(ctx, sessionID, originProxy)
if err != nil {
q.logger.Err(err).Msgf("Failed to register udp session %s", sessionID)
return err
}
go func() {
defer q.sessionManager.UnregisterSession(q.session.Context(), sessionID)
if err := session.Serve(q.session.Context(), closeAfterIdleHint); err != nil {
q.logger.Debug().Err(err).Str("sessionID", sessionID.String()).Msg("session terminated")
}
}()
q.logger.Debug().Msgf("Registered session %v, %v, %v", sessionID, dstIP, dstPort)
return nil
}
func (q *QUICConnection) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID) error {
q.sessionManager.UnregisterSession(ctx, sessionID)
return nil
}
// streamReadWriteAcker is a light wrapper over QUIC streams with a callback to send response back to
// the client.
type streamReadWriteAcker struct {
io.ReadWriter
*quicpogs.RequestServerStream
}
// AckConnection acks response back to the proxy.
func (s *streamReadWriteAcker) AckConnection() error {
return quicpogs.WriteConnectResponseData(s, nil)
return s.WriteConnectResponseData(nil)
}
// httpResponseAdapter translates responses written by the HTTP Proxy into ones that can be used in QUIC.
type httpResponseAdapter struct {
io.Writer
*quicpogs.RequestServerStream
}
func newHTTPResponseAdapter(w io.Writer) httpResponseAdapter {
return httpResponseAdapter{w}
func newHTTPResponseAdapter(s *quicpogs.RequestServerStream) httpResponseAdapter {
return httpResponseAdapter{s}
}
func (hrw httpResponseAdapter) WriteRespHeaders(status int, header http.Header) error {
@@ -151,11 +225,11 @@ func (hrw httpResponseAdapter) WriteRespHeaders(status int, header http.Header)
metadata = append(metadata, quicpogs.Metadata{Key: httpHeaderKey, Val: v})
}
}
return quicpogs.WriteConnectResponseData(hrw, nil, metadata...)
return hrw.WriteConnectResponseData(nil, metadata...)
}
func (hrw httpResponseAdapter) WriteErrorResponse(err error) {
quicpogs.WriteConnectResponseData(hrw, err, quicpogs.Metadata{Key: "HttpStatus", Val: strconv.Itoa(http.StatusBadGateway)})
hrw.WriteConnectResponseData(err, quicpogs.Metadata{Key: "HttpStatus", Val: strconv.Itoa(http.StatusBadGateway)})
}
func buildHTTPRequest(connectRequest *quicpogs.ConnectRequest, body io.ReadCloser) (*http.Request, error) {
@@ -176,7 +250,7 @@ func buildHTTPRequest(connectRequest *quicpogs.ConnectRequest, body io.ReadClose
// metadata.Key is off the format httpHeaderKey:<HTTPHeader>
httpHeaderKey := strings.Split(metadata.Key, ":")
if len(httpHeaderKey) != 2 {
return nil, fmt.Errorf("Header Key: %s malformed", metadata.Key)
return nil, fmt.Errorf("header Key: %s malformed", metadata.Key)
}
req.Header.Add(httpHeaderKey[1], metadata.Val)
}
+37 -36
View File
@@ -26,7 +26,6 @@ import (
"github.com/stretchr/testify/require"
quicpogs "github.com/cloudflare/cloudflared/quic"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
@@ -34,7 +33,8 @@ import (
// It also serves as a demonstration for communication with the QUIC connection started by a cloudflared.
func TestQUICServer(t *testing.T) {
quicConfig := &quic.Config{
KeepAlive: true,
KeepAlive: true,
EnableDatagrams: true,
}
// Setup test.
@@ -76,15 +76,15 @@ func TestQUICServer(t *testing.T) {
dest: "/ok",
connectionType: quicpogs.ConnectionTypeHTTP,
metadata: []quicpogs.Metadata{
quicpogs.Metadata{
{
Key: "HttpHeader:Cf-Ray",
Val: "123123123",
},
quicpogs.Metadata{
{
Key: "HttpHost",
Val: "cf.host",
},
quicpogs.Metadata{
{
Key: "HttpMethod",
Val: "GET",
},
@@ -96,19 +96,19 @@ func TestQUICServer(t *testing.T) {
dest: "/echo_body",
connectionType: quicpogs.ConnectionTypeHTTP,
metadata: []quicpogs.Metadata{
quicpogs.Metadata{
{
Key: "HttpHeader:Cf-Ray",
Val: "123123123",
},
quicpogs.Metadata{
{
Key: "HttpHost",
Val: "cf.host",
},
quicpogs.Metadata{
{
Key: "HttpMethod",
Val: "POST",
},
quicpogs.Metadata{
{
Key: "HttpHeader:Content-Length",
Val: "24",
},
@@ -121,19 +121,19 @@ func TestQUICServer(t *testing.T) {
dest: "/ok",
connectionType: quicpogs.ConnectionTypeWebsocket,
metadata: []quicpogs.Metadata{
quicpogs.Metadata{
{
Key: "HttpHeader:Cf-Cloudflared-Proxy-Connection-Upgrade",
Val: "Websocket",
},
quicpogs.Metadata{
{
Key: "HttpHeader:Another-Header",
Val: "Misc",
},
quicpogs.Metadata{
{
Key: "HttpHost",
Val: "cf.host",
},
quicpogs.Metadata{
{
Key: "HttpMethod",
Val: "get",
},
@@ -171,7 +171,7 @@ func TestQUICServer(t *testing.T) {
udpListener.LocalAddr(),
tlsClientConfig,
originProxy,
&pogs.ConnectionOptions{},
&tunnelpogs.ConnectionOptions{},
controlStream,
NewObserver(&log, &log, false),
)
@@ -218,10 +218,11 @@ func quicServer(
stream, err := session.OpenStreamSync(context.Background())
require.NoError(t, err)
err = quicpogs.WriteConnectRequestData(stream, dest, connectionType, metadata...)
reqClientStream := quicpogs.RequestClientStream{ReadWriteCloser: stream}
err = reqClientStream.WriteConnectRequestData(dest, connectionType, metadata...)
require.NoError(t, err)
_, err = quicpogs.ReadConnectResponseData(stream)
_, err = reqClientStream.ReadConnectResponseData()
require.NoError(t, err)
if message != nil {
@@ -309,23 +310,23 @@ func TestBuildHTTPRequest(t *testing.T) {
connectRequest: &quicpogs.ConnectRequest{
Dest: "http://test.com",
Metadata: []quicpogs.Metadata{
quicpogs.Metadata{
{
Key: "HttpHeader:Cf-Cloudflared-Proxy-Connection-Upgrade",
Val: "Websocket",
},
quicpogs.Metadata{
{
Key: "HttpHeader:Content-Length",
Val: "514",
},
quicpogs.Metadata{
{
Key: "HttpHeader:Another-Header",
Val: "Misc",
},
quicpogs.Metadata{
{
Key: "HttpHost",
Val: "cf.host",
},
quicpogs.Metadata{
{
Key: "HttpMethod",
Val: "get",
},
@@ -355,19 +356,19 @@ func TestBuildHTTPRequest(t *testing.T) {
connectRequest: &quicpogs.ConnectRequest{
Dest: "http://test.com",
Metadata: []quicpogs.Metadata{
quicpogs.Metadata{
{
Key: "HttpHeader:Cf-Cloudflared-Proxy-Connection-Upgrade",
Val: "Websocket",
},
quicpogs.Metadata{
{
Key: "HttpHeader:Another-Header",
Val: "Misc",
},
quicpogs.Metadata{
{
Key: "HttpHost",
Val: "cf.host",
},
quicpogs.Metadata{
{
Key: "HttpMethod",
Val: "get",
},
@@ -396,19 +397,19 @@ func TestBuildHTTPRequest(t *testing.T) {
connectRequest: &quicpogs.ConnectRequest{
Dest: "http://test.com",
Metadata: []quicpogs.Metadata{
quicpogs.Metadata{
{
Key: "HttpHeader:Another-Header",
Val: "Misc",
},
quicpogs.Metadata{
{
Key: "HttpHeader:Transfer-Encoding",
Val: "chunked",
},
quicpogs.Metadata{
{
Key: "HttpHost",
Val: "cf.host",
},
quicpogs.Metadata{
{
Key: "HttpMethod",
Val: "get",
},
@@ -438,19 +439,19 @@ func TestBuildHTTPRequest(t *testing.T) {
connectRequest: &quicpogs.ConnectRequest{
Dest: "http://test.com",
Metadata: []quicpogs.Metadata{
quicpogs.Metadata{
{
Key: "HttpHeader:Another-Header",
Val: "Misc",
},
quicpogs.Metadata{
{
Key: "HttpHeader:Transfer-Encoding",
Val: "gzip,chunked",
},
quicpogs.Metadata{
{
Key: "HttpHost",
Val: "cf.host",
},
quicpogs.Metadata{
{
Key: "HttpMethod",
Val: "get",
},
@@ -481,15 +482,15 @@ func TestBuildHTTPRequest(t *testing.T) {
Type: quicpogs.ConnectionTypeWebsocket,
Dest: "http://test.com",
Metadata: []quicpogs.Metadata{
quicpogs.Metadata{
{
Key: "HttpHeader:Another-Header",
Val: "Misc",
},
quicpogs.Metadata{
{
Key: "HttpHost",
Val: "cf.host",
},
quicpogs.Metadata{
{
Key: "HttpMethod",
Val: "get",
},
+33
View File
@@ -0,0 +1,33 @@
package datagramsession
import (
"io"
"github.com/google/uuid"
)
// registerSessionEvent is an event to start tracking a new session
type registerSessionEvent struct {
sessionID uuid.UUID
originProxy io.ReadWriteCloser
resultChan chan *Session
}
func newRegisterSessionEvent(sessionID uuid.UUID, originProxy io.ReadWriteCloser) *registerSessionEvent {
return &registerSessionEvent{
sessionID: sessionID,
originProxy: originProxy,
resultChan: make(chan *Session, 1),
}
}
// unregisterSessionEvent is an event to stop tracking and terminate the session.
type unregisterSessionEvent struct {
sessionID uuid.UUID
}
// newDatagram is an event when transport receives new datagram
type newDatagram struct {
sessionID uuid.UUID
payload []byte
}
+134
View File
@@ -0,0 +1,134 @@
package datagramsession
import (
"context"
"io"
"github.com/google/uuid"
"github.com/rs/zerolog"
"golang.org/x/sync/errgroup"
)
const (
requestChanCapacity = 16
)
// Manager defines the APIs to manage sessions from the same transport.
type Manager interface {
// Serve starts the event loop
Serve(ctx context.Context) error
// RegisterSession starts tracking a session. Caller is responsible for starting the session
RegisterSession(ctx context.Context, sessionID uuid.UUID, dstConn io.ReadWriteCloser) (*Session, error)
// UnregisterSession stops tracking the session and terminates it
UnregisterSession(ctx context.Context, sessionID uuid.UUID) error
}
type manager struct {
registrationChan chan *registerSessionEvent
unregistrationChan chan *unregisterSessionEvent
datagramChan chan *newDatagram
transport transport
sessions map[uuid.UUID]*Session
log *zerolog.Logger
}
func NewManager(transport transport, log *zerolog.Logger) Manager {
return &manager{
registrationChan: make(chan *registerSessionEvent),
unregistrationChan: make(chan *unregisterSessionEvent),
// datagramChan is buffered, so it can read more datagrams from transport while the event loop is processing other events
datagramChan: make(chan *newDatagram, requestChanCapacity),
transport: transport,
sessions: make(map[uuid.UUID]*Session),
log: log,
}
}
func (m *manager) Serve(ctx context.Context) error {
errGroup, ctx := errgroup.WithContext(ctx)
errGroup.Go(func() error {
for {
sessionID, payload, err := m.transport.ReceiveFrom()
if err != nil {
m.log.Err(err).Msg("Failed to receive datagram from transport, closing session manager")
return err
}
datagram := &newDatagram{
sessionID: sessionID,
payload: payload,
}
select {
case <-ctx.Done():
return ctx.Err()
// Only the event loop routine can update/lookup the sessions map to avoid concurrent access
// Send the datagram to the event loop. It will find the session to send to
case m.datagramChan <- datagram:
}
}
})
errGroup.Go(func() error {
for {
select {
case <-ctx.Done():
return ctx.Err()
case datagram := <-m.datagramChan:
m.sendToSession(datagram)
case registration := <-m.registrationChan:
m.registerSession(ctx, registration)
// TODO: TUN-5422: Unregister inactive session upon timeout
case unregistration := <-m.unregistrationChan:
m.unregisterSession(unregistration)
}
}
})
return errGroup.Wait()
}
func (m *manager) RegisterSession(ctx context.Context, sessionID uuid.UUID, originProxy io.ReadWriteCloser) (*Session, error) {
event := newRegisterSessionEvent(sessionID, originProxy)
select {
case <-ctx.Done():
return nil, ctx.Err()
case m.registrationChan <- event:
session := <-event.resultChan
return session, nil
}
}
func (m *manager) registerSession(ctx context.Context, registration *registerSessionEvent) {
session := newSession(registration.sessionID, m.transport, registration.originProxy)
m.sessions[registration.sessionID] = session
registration.resultChan <- session
}
func (m *manager) UnregisterSession(ctx context.Context, sessionID uuid.UUID) error {
event := &unregisterSessionEvent{sessionID: sessionID}
select {
case <-ctx.Done():
return ctx.Err()
case m.unregistrationChan <- event:
return nil
}
}
func (m *manager) unregisterSession(unregistration *unregisterSessionEvent) {
session, ok := m.sessions[unregistration.sessionID]
if ok {
delete(m.sessions, unregistration.sessionID)
session.close()
}
}
func (m *manager) sendToSession(datagram *newDatagram) {
session, ok := m.sessions[datagram.sessionID]
if !ok {
m.log.Error().Str("sessionID", datagram.sessionID.String()).Msg("session not found")
return
}
// session writes to destination over a connected UDP socket, which should not be blocking, so this call doesn't
// need to run in another go routine
_, err := session.transportToDst(datagram.payload)
if err != nil {
m.log.Err(err).Str("sessionID", datagram.sessionID.String()).Msg("Failed to write payload to session")
}
}
+215
View File
@@ -0,0 +1,215 @@
package datagramsession
import (
"bytes"
"context"
"fmt"
"io"
"net"
"testing"
"time"
"github.com/google/uuid"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
)
func TestManagerServe(t *testing.T) {
const (
sessions = 20
msgs = 50
)
log := zerolog.Nop()
transport := &mockQUICTransport{
reqChan: newDatagramChannel(1),
respChan: newDatagramChannel(1),
}
mg := NewManager(transport, &log)
eyeballTracker := make(map[uuid.UUID]*datagramChannel)
for i := 0; i < sessions; i++ {
sessionID := uuid.New()
eyeballTracker[sessionID] = newDatagramChannel(1)
}
ctx, cancel := context.WithCancel(context.Background())
serveDone := make(chan struct{})
go func(ctx context.Context) {
mg.Serve(ctx)
close(serveDone)
}(ctx)
go func(ctx context.Context) {
for {
sessionID, payload, err := transport.respChan.Receive(ctx)
if err != nil {
require.Equal(t, context.Canceled, err)
return
}
respChan := eyeballTracker[sessionID]
require.NoError(t, respChan.Send(ctx, sessionID, payload))
}
}(ctx)
errGroup, ctx := errgroup.WithContext(ctx)
for sID, receiver := range eyeballTracker {
// Assign loop variables to local variables
sessionID := sID
eyeballRespReceiver := receiver
errGroup.Go(func() error {
payload := testPayload(sessionID)
expectResp := testResponse(payload)
cfdConn, originConn := net.Pipe()
origin := mockOrigin{
expectMsgCount: msgs,
expectedMsg: payload,
expectedResp: expectResp,
conn: originConn,
}
eyeball := mockEyeball{
expectMsgCount: msgs,
expectedMsg: expectResp,
expectSessionID: sessionID,
respReceiver: eyeballRespReceiver,
}
reqErrGroup, reqCtx := errgroup.WithContext(ctx)
reqErrGroup.Go(func() error {
return origin.serve()
})
reqErrGroup.Go(func() error {
return eyeball.serve(reqCtx)
})
session, err := mg.RegisterSession(ctx, sessionID, cfdConn)
require.NoError(t, err)
sessionDone := make(chan struct{})
go func() {
session.Serve(ctx, time.Minute*2)
close(sessionDone)
}()
for i := 0; i < msgs; i++ {
require.NoError(t, transport.newRequest(ctx, sessionID, testPayload(sessionID)))
}
// Make sure eyeball and origin have received all messages before unregistering the session
require.NoError(t, reqErrGroup.Wait())
require.NoError(t, mg.UnregisterSession(ctx, sessionID))
<-sessionDone
return nil
})
}
require.NoError(t, errGroup.Wait())
cancel()
transport.close()
<-serveDone
}
type mockOrigin struct {
expectMsgCount int
expectedMsg []byte
expectedResp []byte
conn io.ReadWriteCloser
}
func (mo *mockOrigin) serve() error {
expectedMsgLen := len(mo.expectedMsg)
readBuffer := make([]byte, expectedMsgLen+1)
for i := 0; i < mo.expectMsgCount; i++ {
n, err := mo.conn.Read(readBuffer)
if err != nil {
return err
}
if n != expectedMsgLen {
return fmt.Errorf("Expect to read %d bytes, read %d", expectedMsgLen, n)
}
if !bytes.Equal(readBuffer[:n], mo.expectedMsg) {
return fmt.Errorf("Expect %v, read %v", mo.expectedMsg, readBuffer[:n])
}
_, err = mo.conn.Write(mo.expectedResp)
if err != nil {
return err
}
}
return nil
}
func testPayload(sessionID uuid.UUID) []byte {
return []byte(fmt.Sprintf("Message from %s", sessionID))
}
func testResponse(msg []byte) []byte {
return []byte(fmt.Sprintf("Response to %v", msg))
}
type mockEyeball struct {
expectMsgCount int
expectedMsg []byte
expectSessionID uuid.UUID
respReceiver *datagramChannel
}
func (me *mockEyeball) serve(ctx context.Context) error {
for i := 0; i < me.expectMsgCount; i++ {
sessionID, msg, err := me.respReceiver.Receive(ctx)
if err != nil {
return err
}
if sessionID != me.expectSessionID {
return fmt.Errorf("Expect session %s, got %s", me.expectSessionID, sessionID)
}
if !bytes.Equal(msg, me.expectedMsg) {
return fmt.Errorf("Expect %v, read %v", me.expectedMsg, msg)
}
}
return nil
}
// datagramChannel is a channel for Datagram with wrapper to send/receive with context
type datagramChannel struct {
datagramChan chan *newDatagram
closedChan chan struct{}
}
func newDatagramChannel(capacity uint) *datagramChannel {
return &datagramChannel{
datagramChan: make(chan *newDatagram, capacity),
closedChan: make(chan struct{}),
}
}
func (rc *datagramChannel) Send(ctx context.Context, sessionID uuid.UUID, payload []byte) error {
select {
case <-ctx.Done():
return ctx.Err()
case <-rc.closedChan:
return fmt.Errorf("datagram channel closed")
case rc.datagramChan <- &newDatagram{sessionID: sessionID, payload: payload}:
return nil
}
}
func (rc *datagramChannel) Receive(ctx context.Context) (uuid.UUID, []byte, error) {
select {
case <-ctx.Done():
return uuid.Nil, nil, ctx.Err()
case <-rc.closedChan:
return uuid.Nil, nil, fmt.Errorf("datagram channel closed")
case msg := <-rc.datagramChan:
return msg.sessionID, msg.payload, nil
}
}
func (rc *datagramChannel) Close() {
// No need to close msgChan, it will be garbage collect once there is no reference to it
close(rc.closedChan)
}
+118
View File
@@ -0,0 +1,118 @@
package datagramsession
import (
"context"
"io"
"time"
"github.com/google/uuid"
)
const (
defaultCloseIdleAfter = time.Second * 210
)
// Each Session is a bidirectional pipe of datagrams between transport and dstConn
// Currently the only implementation of transport is quic DatagramMuxer
// Destination can be a connection with origin or with eyeball
// When the destination is origin:
// - Datagrams from edge are read by Manager from the transport. Manager finds the corresponding Session and calls the
// write method of the Session to send to origin
// - Datagrams from origin are read from conn and SentTo transport. Transport will return them to eyeball
// When the destination is eyeball:
// - Datagrams from eyeball are read from conn and SentTo transport. Transport will send them to cloudflared
// - Datagrams from cloudflared are read by Manager from the transport. Manager finds the corresponding Session and calls the
// write method of the Session to send to eyeball
type Session struct {
id uuid.UUID
transport transport
dstConn io.ReadWriteCloser
// activeAtChan is used to communicate the last read/write time
activeAtChan chan time.Time
doneChan chan struct{}
}
func newSession(id uuid.UUID, transport transport, dstConn io.ReadWriteCloser) *Session {
return &Session{
id: id,
transport: transport,
dstConn: dstConn,
// activeAtChan has low capacity. It can be full when there are many concurrent read/write. markActive() will
// drop instead of blocking because last active time only needs to be an approximation
activeAtChan: make(chan time.Time, 2),
doneChan: make(chan struct{}),
}
}
func (s *Session) Serve(ctx context.Context, closeAfterIdle time.Duration) error {
serveCtx, cancel := context.WithCancel(ctx)
defer cancel()
go s.waitForCloseCondition(serveCtx, closeAfterIdle)
// QUIC implementation copies data to another buffer before returning https://github.com/lucas-clemente/quic-go/blob/v0.24.0/session.go#L1967-L1975
// This makes it safe to share readBuffer between iterations
readBuffer := make([]byte, s.transport.MTU())
for {
if err := s.dstToTransport(readBuffer); err != nil {
return err
}
}
}
func (s *Session) waitForCloseCondition(ctx context.Context, closeAfterIdle time.Duration) {
if closeAfterIdle == 0 {
// provide deafult is caller doesn't specify one
closeAfterIdle = defaultCloseIdleAfter
}
// Closing dstConn cancels read so Serve function can return
defer s.dstConn.Close()
checkIdleFreq := closeAfterIdle / 8
checkIdleTicker := time.NewTicker(checkIdleFreq)
defer checkIdleTicker.Stop()
activeAt := time.Now()
for {
select {
case <-ctx.Done():
return
case <-s.doneChan:
return
// TODO: TUN-5423 evaluate if using atomic is more efficient
case now := <-checkIdleTicker.C:
// The session is considered inactive if current time is after (last active time + allowed idle time)
if now.After(activeAt.Add(closeAfterIdle)) {
return
}
case activeAt = <-s.activeAtChan: // Update last active time
}
}
}
func (s *Session) dstToTransport(buffer []byte) error {
n, err := s.dstConn.Read(buffer)
s.markActive()
if n > 0 {
if err := s.transport.SendTo(s.id, buffer[:n]); err != nil {
return err
}
}
return err
}
func (s *Session) transportToDst(payload []byte) (int, error) {
s.markActive()
return s.dstConn.Write(payload)
}
// Sends the last active time to the idle checker loop without blocking. activeAtChan will only be full when there
// are many concurrent read/write. It is fine to lose some precision
func (s *Session) markActive() {
select {
case s.activeAtChan <- time.Now():
default:
}
}
func (s *Session) close() {
close(s.doneChan)
}
+177
View File
@@ -0,0 +1,177 @@
package datagramsession
import (
"bytes"
"context"
"fmt"
"io"
"net"
"sync"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
)
// TestCloseSession makes sure a session will stop after context is done
func TestSessionCtxDone(t *testing.T) {
testSessionReturns(t, closeByContext, time.Minute*2)
}
// TestCloseSession makes sure a session will stop after close method is called
func TestCloseSession(t *testing.T) {
testSessionReturns(t, closeByCallingClose, time.Minute*2)
}
// TestCloseIdle makess sure a session will stop after there is no read/write for a period defined by closeAfterIdle
func TestCloseIdle(t *testing.T) {
testSessionReturns(t, closeByTimeout, time.Millisecond*100)
}
func testSessionReturns(t *testing.T, closeBy closeMethod, closeAfterIdle time.Duration) {
sessionID := uuid.New()
cfdConn, originConn := net.Pipe()
payload := testPayload(sessionID)
transport := &mockQUICTransport{
reqChan: newDatagramChannel(1),
respChan: newDatagramChannel(1),
}
session := newSession(sessionID, transport, cfdConn)
ctx, cancel := context.WithCancel(context.Background())
sessionDone := make(chan struct{})
go func() {
session.Serve(ctx, closeAfterIdle)
close(sessionDone)
}()
go func() {
n, err := session.transportToDst(payload)
require.NoError(t, err)
require.Equal(t, len(payload), n)
}()
readBuffer := make([]byte, len(payload)+1)
n, err := originConn.Read(readBuffer)
require.NoError(t, err)
require.Equal(t, len(payload), n)
lastRead := time.Now()
switch closeBy {
case closeByContext:
cancel()
case closeByCallingClose:
session.close()
}
<-sessionDone
if closeBy == closeByTimeout {
require.True(t, time.Now().After(lastRead.Add(closeAfterIdle)))
}
// call cancelled again otherwise the linter will warn about possible context leak
cancel()
}
type closeMethod int
const (
closeByContext closeMethod = iota
closeByCallingClose
closeByTimeout
)
func TestWriteToDstSessionPreventClosed(t *testing.T) {
testActiveSessionNotClosed(t, false, true)
}
func TestReadFromDstSessionPreventClosed(t *testing.T) {
testActiveSessionNotClosed(t, true, false)
}
func testActiveSessionNotClosed(t *testing.T, readFromDst bool, writeToDst bool) {
const closeAfterIdle = time.Millisecond * 100
const activeTime = time.Millisecond * 500
sessionID := uuid.New()
cfdConn, originConn := net.Pipe()
payload := testPayload(sessionID)
transport := &mockQUICTransport{
reqChan: newDatagramChannel(100),
respChan: newDatagramChannel(100),
}
session := newSession(sessionID, transport, cfdConn)
startTime := time.Now()
activeUntil := startTime.Add(activeTime)
ctx, cancel := context.WithCancel(context.Background())
errGroup, ctx := errgroup.WithContext(ctx)
errGroup.Go(func() error {
session.Serve(ctx, closeAfterIdle)
if time.Now().Before(startTime.Add(activeTime)) {
return fmt.Errorf("session closed while it's still active")
}
return nil
})
if readFromDst {
errGroup.Go(func() error {
for {
if time.Now().After(activeUntil) {
return nil
}
if _, err := originConn.Write(payload); err != nil {
return err
}
time.Sleep(closeAfterIdle / 2)
}
})
}
if writeToDst {
errGroup.Go(func() error {
readBuffer := make([]byte, len(payload))
for {
n, err := originConn.Read(readBuffer)
if err != nil {
if err == io.EOF || err == io.ErrClosedPipe {
return nil
}
return err
}
if !bytes.Equal(payload, readBuffer[:n]) {
return fmt.Errorf("payload %v is not equal to %v", readBuffer[:n], payload)
}
}
})
errGroup.Go(func() error {
for {
if time.Now().After(activeUntil) {
return nil
}
if _, err := session.transportToDst(payload); err != nil {
return err
}
time.Sleep(closeAfterIdle / 2)
}
})
}
require.NoError(t, errGroup.Wait())
cancel()
}
func TestMarkActiveNotBlocking(t *testing.T) {
const concurrentCalls = 50
session := newSession(uuid.New(), nil, nil)
var wg sync.WaitGroup
wg.Add(concurrentCalls)
for i := 0; i < concurrentCalls; i++ {
go func() {
session.markActive()
wg.Done()
}()
}
wg.Wait()
}
+13
View File
@@ -0,0 +1,13 @@
package datagramsession
import "github.com/google/uuid"
// Transport is a connection between cloudflared and edge that can multiplex datagrams from multiple sessions
type transport interface {
// SendTo writes payload for a session to the transport
SendTo(sessionID uuid.UUID, payload []byte) error
// ReceiveFrom reads the next datagram from the transport
ReceiveFrom() (uuid.UUID, []byte, error)
// Max transmission unit of the transport
MTU() uint
}
+36
View File
@@ -0,0 +1,36 @@
package datagramsession
import (
"context"
"github.com/google/uuid"
)
type mockQUICTransport struct {
reqChan *datagramChannel
respChan *datagramChannel
}
func (mt *mockQUICTransport) SendTo(sessionID uuid.UUID, payload []byte) error {
buf := make([]byte, len(payload))
// The QUIC implementation copies data to another buffer before returning https://github.com/lucas-clemente/quic-go/blob/v0.24.0/session.go#L1967-L1975
copy(buf, payload)
return mt.respChan.Send(context.Background(), sessionID, buf)
}
func (mt *mockQUICTransport) ReceiveFrom() (uuid.UUID, []byte, error) {
return mt.reqChan.Receive(context.Background())
}
func (mt *mockQUICTransport) MTU() uint {
return 1220
}
func (mt *mockQUICTransport) newRequest(ctx context.Context, sessionID uuid.UUID, payload []byte) error {
return mt.reqChan.Send(ctx, sessionID, payload)
}
func (mt *mockQUICTransport) close() {
mt.reqChan.Close()
mt.respChan.Close()
}
+2 -2
View File
@@ -72,7 +72,7 @@ def get_or_create_release(repo, version, dry_run=False):
except UnknownObjectException:
logging.info("Release %s not found", version)
# We dont want to create a new release tag if one doesnt already exist
# We don't want to create a new release tag if one doesn't already exist
assert_tag_exists(repo, version)
if dry_run:
@@ -198,7 +198,7 @@ def upload_asset(release, filepath, filename, release_version, kv_account_id, na
pass # the macOS release copy fails with being the same file (already in the artifacts directory)
def main():
""" Attempts to upload Asset to Github Release. Creates Release if it doesnt exist """
""" Attempts to upload Asset to Github Release. Creates Release if it doesn't exist """
try:
args = parse_args()
client = Github(args.api_key)
+2 -2
View File
@@ -28,7 +28,7 @@ require (
github.com/json-iterator/go v1.1.10
github.com/kr/text v0.2.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lucas-clemente/quic-go v0.23.0
github.com/lucas-clemente/quic-go v0.24.0
github.com/mattn/go-colorable v0.1.8
github.com/miekg/dns v1.1.31
github.com/mitchellh/go-homedir v1.1.0
@@ -46,7 +46,7 @@ require (
github.com/urfave/cli/v2 v2.2.0
go.uber.org/automaxprocs v1.4.0
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781
golang.org/x/net v0.0.0-20211109214657-ef0fda0de508
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1
+4
View File
@@ -422,6 +422,8 @@ github.com/liquidweb/liquidweb-go v1.6.0/go.mod h1:UDcVnAMDkZxpw4Y7NOHkqoeiGacVL
github.com/lucas-clemente/quic-go v0.13.1/go.mod h1:Vn3/Fb0/77b02SGhQk36KzOUmXgVpFfizUfW5WMaqyU=
github.com/lucas-clemente/quic-go v0.23.0 h1:5vFnKtZ6nHDFsc/F3uuiF4T3y/AXaQdxjUqiVw26GZE=
github.com/lucas-clemente/quic-go v0.23.0/go.mod h1:paZuzjXCE5mj6sikVLMvqXk8lJV2AsqtJ6bDhjEfxx0=
github.com/lucas-clemente/quic-go v0.24.0 h1:ToR7SIIEdrgOhgVTHvPgdVRJfgVy+N0wQAagH7L4d5g=
github.com/lucas-clemente/quic-go v0.24.0/go.mod h1:paZuzjXCE5mj6sikVLMvqXk8lJV2AsqtJ6bDhjEfxx0=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
@@ -818,6 +820,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20211109214657-ef0fda0de508 h1:v3NKo+t/Kc3EASxaKZ82lwK6mCf4ZeObQBduYFZHo7c=
golang.org/x/net v0.0.0-20211109214657-ef0fda0de508/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+1 -1
View File
@@ -93,7 +93,7 @@ func (m *activeStreamMap) Set(newStream *MuxedStream) bool {
return true
}
// Delete stops tracking the stream. It should be called only after it is closed and resetted.
// Delete stops tracking the stream. It should be called only after it is closed and reset.
func (m *activeStreamMap) Delete(streamID uint32) {
m.Lock()
defer m.Unlock()
+4 -4
View File
@@ -12,7 +12,7 @@ import (
/* This is an implementation of https://github.com/vkrasnov/h2-compression-dictionaries
but modified for tunnels in a few key ways:
Since tunnels is a server-to-server service, some aspects of the spec would cause
unnessasary head-of-line blocking on the CPU and on the network, hence this implementation
unnecessary head-of-line blocking on the CPU and on the network, hence this implementation
allows for parallel compression on the "client", and buffering on the "server" to solve
this problem. */
@@ -67,7 +67,7 @@ var compressionPresets = map[CompressionSetting]CompressionPreset{
}
func compressionSettingVal(version, fmt, sz, nd uint8) uint32 {
// Currently the compression settings are inlcude:
// Currently the compression settings are include:
// * version: only 1 is supported
// * fmt: only 2 for brotli is supported
// * sz: log2 of the maximal allowed dictionary size
@@ -438,7 +438,7 @@ func assignDictToStream(s *MuxedStream, p []byte) bool {
h2d.dictLock.Lock()
if w.comp != nil {
// Check again with lock, in therory the inteface allows for unordered writes
// Check again with lock, in therory the interface allows for unordered writes
h2d.dictLock.Unlock()
return false
}
@@ -468,7 +468,7 @@ func assignDictToStream(s *MuxedStream, p []byte) bool {
}
} else {
// Use the overflow dictionary as last resort
// If slots are availabe generate new dictioanries for path and content-type
// If slots are available generate new dictionaries for path and content-type
useID, _ = h2d.getGenericDictID()
pathID, pathFound = h2d.getNextDictID()
if pathFound {
+2 -2
View File
@@ -174,7 +174,7 @@ func Handshake(
pingTimestamp := NewPingTimestamp()
connActive := NewSignal()
idleDuration := config.HeartbeatInterval
// Sanity check to enusre idelDuration is sane
// Sanity check to ensure idelDuration is sane
if idleDuration == 0 || idleDuration < defaultTimeout {
idleDuration = defaultTimeout
config.Log.Info().Msgf("muxer: Minimum idle time has been adjusted to %d", defaultTimeout)
@@ -274,7 +274,7 @@ func (m *Muxer) readPeerSettings(magic uint32) error {
m.compressionQuality = compressionPresets[CompressionNone]
return nil
}
// Values used for compression are the mimimum between the two peers
// Values used for compression are the minimum between the two peers
if sz < m.compressionQuality.dictSize {
m.compressionQuality.dictSize = sz
}
+1 -1
View File
@@ -130,7 +130,7 @@ func TestMuxMetricsUpdater(t *testing.T) {
m.updateReceiveWindow(uint32(j))
m.updateSendWindow(uint32(j))
// should always be disgarded since the send time is before readerSend
// should always be discarded since the send time is before readerSend
rm := &roundTripMeasurement{receiveTime: readerStart, sendTime: readerStart.Add(-time.Duration(j*dataPoints) * time.Millisecond)}
m.updateRTT(rm)
+19 -4
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
@@ -19,7 +20,6 @@ import (
"golang.org/x/net/proxy"
"golang.org/x/sync/errgroup"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/socks"
"github.com/cloudflare/cloudflared/websocket"
@@ -97,7 +97,7 @@ func TestDefaultStreamWSOverTCPConnection(t *testing.T) {
}
// TestSocksStreamWSOverTCPConnection simulates proxying in socks mode.
// Eyeball side runs cloudflared accesss tcp with --url flag to start a websocket forwarder which
// Eyeball side runs cloudflared access tcp with --url flag to start a websocket forwarder which
// wraps SOCKS5 traffic in websocket
// Origin side runs a tcpOverWSConnection with socks.StreamHandler
func TestSocksStreamWSOverTCPConnection(t *testing.T) {
@@ -192,8 +192,10 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
func TestWsConnReturnsBeforeStreamReturns(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
eyeballConn, err := connection.NewHTTP2RespWriter(r, w, connection.TypeWebsocket)
assert.NoError(t, err)
eyeballConn := &readWriter{
w: w,
r: r.Body,
}
cfdConn, originConn := net.Pipe()
tcpOverWSConn := tcpOverWSConnection{
@@ -319,3 +321,16 @@ func echoTCPOrigin(t *testing.T, conn net.Conn) {
_, err = conn.Write(testResponse)
assert.NoError(t, err)
}
type readWriter struct {
w io.Writer
r io.Reader
}
func (r *readWriter) Read(p []byte) (n int, err error) {
return r.r.Read(p)
}
func (r *readWriter) Write(p []byte) (n int, err error) {
return r.w.Write(p)
}
+27
View File
@@ -0,0 +1,27 @@
package ingress
import (
"fmt"
"io"
"net"
)
type UDPProxy struct {
io.ReadWriteCloser
}
func DialUDP(dstIP net.IP, dstPort uint16) (*UDPProxy, error) {
dstAddr := &net.UDPAddr{
IP: dstIP,
Port: int(dstPort),
}
// We use nil as local addr to force runtime to find the best suitable local address IP given the destination
// address as context.
udpConn, err := net.DialUDP("udp", nil, dstAddr)
if err != nil {
return nil, fmt.Errorf("unable to create UDP proxy to origin (%v:%v): %w", dstIP, dstPort, err)
}
return &UDPProxy{udpConn}, nil
}
+12 -30
View File
@@ -4,46 +4,32 @@ import (
"encoding/json"
"fmt"
"net/http"
"sync"
conn "github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/tunnelstate"
"github.com/rs/zerolog"
)
// ReadyServer serves HTTP 200 if the tunnel can serve traffic. Intended for k8s readiness checks.
type ReadyServer struct {
sync.RWMutex
isConnected map[int]bool
log *zerolog.Logger
tracker *tunnelstate.ConnTracker
}
// NewReadyServer initializes a ReadyServer and starts listening for dis/connection events.
func NewReadyServer(log *zerolog.Logger) *ReadyServer {
return &ReadyServer{
isConnected: make(map[int]bool, 0),
log: log,
tracker: tunnelstate.NewConnTracker(log),
}
}
func (rs *ReadyServer) OnTunnelEvent(c conn.Event) {
switch c.EventType {
case conn.Connected:
rs.Lock()
rs.isConnected[int(c.Index)] = true
rs.Unlock()
case conn.Disconnected, conn.Reconnecting, conn.RegisteringTunnel, conn.Unregistering:
rs.Lock()
rs.isConnected[int(c.Index)] = false
rs.Unlock()
default:
rs.log.Error().Msgf("Unknown connection event case %v", c)
}
rs.tracker.OnTunnelEvent(c)
}
type body struct {
Status int `json:"status"`
ReadyConnections int `json:"readyConnections"`
Status int `json:"status"`
ReadyConnections uint `json:"readyConnections"`
}
// ServeHTTP responds with HTTP 200 if the tunnel is connected to the edge.
@@ -63,15 +49,11 @@ func (rs *ReadyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// This is the bulk of the logic for ServeHTTP, broken into its own pure function
// to make unit testing easy.
func (rs *ReadyServer) makeResponse() (statusCode, readyConnections int) {
statusCode = http.StatusServiceUnavailable
rs.RLock()
defer rs.RUnlock()
for _, connected := range rs.isConnected {
if connected {
statusCode = http.StatusOK
readyConnections++
}
func (rs *ReadyServer) makeResponse() (statusCode int, readyConnections uint) {
readyConnections = rs.tracker.CountActiveConns()
if readyConnections > 0 {
return http.StatusOK, readyConnections
} else {
return http.StatusServiceUnavailable, readyConnections
}
return statusCode, readyConnections
}
+4 -2
View File
@@ -7,6 +7,8 @@ import (
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/cloudflare/cloudflared/tunnelstate"
"github.com/cloudflare/cloudflared/connection"
)
@@ -18,7 +20,7 @@ func TestReadyServer_makeResponse(t *testing.T) {
name string
fields fields
wantOK bool
wantReadyConnections int
wantReadyConnections uint
}{
{
name: "One connection online => HTTP 200",
@@ -49,7 +51,7 @@ func TestReadyServer_makeResponse(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rs := &ReadyServer{
isConnected: tt.fields.isConnected,
tracker: tunnelstate.MockedConnTracker(tt.fields.isConnected),
}
gotStatusCode, gotReadyConnections := rs.makeResponse()
if tt.wantOK && gotStatusCode != http.StatusOK {
+42
View File
@@ -0,0 +1,42 @@
package origin
import (
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/tunnelstate"
)
type ConnAwareLogger struct {
tracker *tunnelstate.ConnTracker
logger *zerolog.Logger
}
func NewConnAwareLogger(logger *zerolog.Logger, observer *connection.Observer) *ConnAwareLogger {
connAwareLogger := &ConnAwareLogger{
tracker: tunnelstate.NewConnTracker(logger),
logger: logger,
}
observer.RegisterSink(connAwareLogger.tracker)
return connAwareLogger
}
func (c *ConnAwareLogger) ReplaceLogger(logger *zerolog.Logger) *ConnAwareLogger {
return &ConnAwareLogger{
tracker: c.tracker,
logger: logger,
}
}
func (c *ConnAwareLogger) ConnAwareLogger() *zerolog.Event {
if c.tracker.CountActiveConns() == 0 {
return c.logger.Error()
}
return c.logger.Warn()
}
func (c *ConnAwareLogger) Logger() *zerolog.Logger {
return c.logger
}
+5
View File
@@ -177,6 +177,11 @@ func (p *Proxy) proxyHTTPRequest(
roundTripReq.Header.Set("Connection", "keep-alive")
}
// Set the User-Agent as an empty string if not provided to avoid inserting golang default UA
if roundTripReq.Header.Get("User-Agent") == "" {
roundTripReq.Header.Set("User-Agent", "")
}
resp, err := httpService.RoundTrip(roundTripReq)
if err != nil {
return errors.Wrap(err, "Unable to reach the origin service. The service may be down or it may not be responding to traffic from cloudflared")
+1 -1
View File
@@ -767,7 +767,7 @@ func newWSRespWriter(w io.Writer) *wsRespWriter {
func (w *wsRespWriter) Write(p []byte) (int, error) {
returnedMsg, err := wsutil.ReadServerBinary(bytes.NewBuffer(p))
if err != nil {
// The data was not returned by a websocket connecton.
// The data was not returned by a websocket connection.
if err != io.ErrUnexpectedEOF {
return w.w.Write(p)
}
+1 -1
View File
@@ -91,7 +91,7 @@ func (cm *reconnectCredentialManager) ConnDigest(connID uint8) ([]byte, error) {
defer cm.mu.RUnlock()
digest, ok := cm.connDigest[connID]
if !ok {
return nil, fmt.Errorf("no conneciton digest for connection %v", connID)
return nil, fmt.Errorf("no connection digest for connection %v", connID)
}
return digest, nil
}
+10 -7
View File
@@ -46,7 +46,7 @@ type Supervisor struct {
nextConnectedIndex int
nextConnectedSignal chan struct{}
log *zerolog.Logger
log *ConnAwareLogger
logTransport *zerolog.Logger
reconnectCredentialManager *reconnectCredentialManager
@@ -91,7 +91,7 @@ func NewSupervisor(config *TunnelConfig, reconnectCh chan ReconnectSignal, grace
edgeIPs: edgeIPs,
tunnelErrors: make(chan tunnelError),
tunnelsConnecting: map[int]chan struct{}{},
log: config.Log,
log: NewConnAwareLogger(config.Log, config.Observer),
logTransport: config.LogTransport,
reconnectCredentialManager: newReconnectCredentialManager(connection.MetricsNamespace, connection.TunnelSubsystem, config.HAConnections),
useReconnectToken: useReconnectToken,
@@ -123,7 +123,7 @@ func (s *Supervisor) Run(
if timer, err := s.reconnectCredentialManager.RefreshAuth(ctx, refreshAuthBackoff, s.authenticate); err == nil {
refreshAuthBackoffTimer = timer
} else {
s.log.Err(err).
s.log.Logger().Err(err).
Dur("refreshAuthRetryDuration", refreshAuthRetryDuration).
Msgf("supervisor: initial refreshAuth failed, retrying in %v", refreshAuthRetryDuration)
refreshAuthBackoffTimer = time.After(refreshAuthRetryDuration)
@@ -145,7 +145,7 @@ func (s *Supervisor) Run(
case tunnelError := <-s.tunnelErrors:
tunnelsActive--
if tunnelError.err != nil && !shuttingDown {
s.log.Err(tunnelError.err).Int(connection.LogFieldConnIndex, tunnelError.index).Msg("Connection terminated")
s.log.ConnAwareLogger().Err(tunnelError.err).Int(connection.LogFieldConnIndex, tunnelError.index).Msg("Connection terminated")
tunnelsWaiting = append(tunnelsWaiting, tunnelError.index)
s.waitForNextTunnel(tunnelError.index)
@@ -170,7 +170,7 @@ func (s *Supervisor) Run(
case <-refreshAuthBackoffTimer:
newTimer, err := s.reconnectCredentialManager.RefreshAuth(ctx, refreshAuthBackoff, s.authenticate)
if err != nil {
s.log.Err(err).Msg("supervisor: Authentication failed")
s.log.Logger().Err(err).Msg("supervisor: Authentication failed")
// Permanent failure. Leave the `select` without setting the
// channel to be non-null, so we'll never hit this case of the `select` again.
continue
@@ -195,7 +195,7 @@ func (s *Supervisor) initialize(
) error {
availableAddrs := s.edgeIPs.AvailableAddrs()
if s.config.HAConnections > availableAddrs {
s.log.Info().Msgf("You requested %d HA connections but I can give you at most %d.", s.config.HAConnections, availableAddrs)
s.log.Logger().Info().Msgf("You requested %d HA connections but I can give you at most %d.", s.config.HAConnections, availableAddrs)
s.config.HAConnections = availableAddrs
}
@@ -244,6 +244,7 @@ func (s *Supervisor) startFirstTunnel(
s.reconnectCredentialManager,
s.config,
addr,
s.log,
firstConnIndex,
connectedSignal,
s.cloudflaredUUID,
@@ -277,6 +278,7 @@ func (s *Supervisor) startFirstTunnel(
s.reconnectCredentialManager,
s.config,
addr,
s.log,
firstConnIndex,
connectedSignal,
s.cloudflaredUUID,
@@ -310,6 +312,7 @@ func (s *Supervisor) startTunnel(
s.reconnectCredentialManager,
s.config,
addr,
s.log,
uint8(index),
connectedSignal,
s.cloudflaredUUID,
@@ -373,7 +376,7 @@ func (s *Supervisor) authenticate(ctx context.Context, numPreviousAttempts int)
if err != nil {
return nil, err
}
rpcClient := connection.NewTunnelServerClient(ctx, stream, s.log)
rpcClient := connection.NewTunnelServerClient(ctx, stream, s.log.Logger())
defer rpcClient.Close()
const arbitraryConnectionID = uint8(0)
+26 -21
View File
@@ -131,6 +131,7 @@ func ServeTunnelLoop(
credentialManager *reconnectCredentialManager,
config *TunnelConfig,
addr *allregions.EdgeAddr,
connAwareLogger *ConnAwareLogger,
connIndex uint8,
connectedSignal *signal.Signal,
cloudflaredUUID uuid.UUID,
@@ -140,7 +141,8 @@ func ServeTunnelLoop(
haConnections.Inc()
defer haConnections.Dec()
connLog := config.Log.With().Uint8(connection.LogFieldConnIndex, connIndex).Logger()
logger := config.Log.With().Uint8(connection.LogFieldConnIndex, connIndex).Logger()
connLog := connAwareLogger.ReplaceLogger(&logger)
protocolFallback := &protocolFallback{
retry.BackoffHandler{MaxRetries: config.Retries},
@@ -160,7 +162,7 @@ func ServeTunnelLoop(
for {
err, recoverable := ServeTunnel(
ctx,
&connLog,
connLog,
credentialManager,
config,
addr,
@@ -182,7 +184,7 @@ func ServeTunnelLoop(
if !ok {
return err
}
connLog.Info().Msgf("Retrying connection in up to %s seconds", duration)
connLog.Logger().Info().Msgf("Retrying connection in up to %s seconds", duration)
select {
case <-ctx.Done():
@@ -192,7 +194,7 @@ func ServeTunnelLoop(
case <-protocolFallback.BackoffTimer():
var idleTimeoutError *quic.IdleTimeoutError
if !selectNextProtocol(
&connLog,
connLog.Logger(),
protocolFallback,
config.ProtocolSelector,
errors.As(err, &idleTimeoutError),
@@ -255,7 +257,7 @@ func selectNextProtocol(
// on error returns a flag indicating if error can be retried
func ServeTunnel(
ctx context.Context,
connLog *zerolog.Logger,
connLog *ConnAwareLogger,
credentialManager *reconnectCredentialManager,
config *TunnelConfig,
addr *allregions.EdgeAddr,
@@ -299,29 +301,29 @@ func ServeTunnel(
if err != nil {
switch err := err.(type) {
case connection.DupConnRegisterTunnelError:
connLog.Err(err).Msg("Unable to establish connection.")
connLog.ConnAwareLogger().Err(err).Msg("Unable to establish connection.")
// don't retry this connection anymore, let supervisor pick a new address
return err, false
case connection.ServerRegisterTunnelError:
connLog.Err(err).Msg("Register tunnel error from server side")
connLog.ConnAwareLogger().Err(err).Msg("Register tunnel error from server side")
// Don't send registration error return from server to Sentry. They are
// logged on server side
if incidents := config.IncidentLookup.ActiveIncidents(); len(incidents) > 0 {
connLog.Error().Msg(activeIncidentsMsg(incidents))
connLog.ConnAwareLogger().Msg(activeIncidentsMsg(incidents))
}
return err.Cause, !err.Permanent
case ReconnectSignal:
connLog.Info().
connLog.Logger().Info().
Uint8(connection.LogFieldConnIndex, connIndex).
Msgf("Restarting connection due to reconnect signal in %s", err.Delay)
err.DelayBeforeReconnect()
return err, true
default:
if err == context.Canceled {
connLog.Debug().Err(err).Msgf("Serve tunnel error")
connLog.Logger().Debug().Err(err).Msgf("Serve tunnel error")
return err, false
}
connLog.Err(err).Msgf("Serve tunnel error")
connLog.ConnAwareLogger().Err(err).Msgf("Serve tunnel error")
_, permanent := err.(unrecoverableError)
return err, !permanent
}
@@ -331,7 +333,7 @@ func ServeTunnel(
func serveTunnel(
ctx context.Context,
connLog *zerolog.Logger,
connLog *ConnAwareLogger,
credentialManager *reconnectCredentialManager,
config *TunnelConfig,
addr *allregions.EdgeAddr,
@@ -364,6 +366,7 @@ func serveTunnel(
return ServeQUIC(ctx,
addr.UDP,
config,
connLog,
connOptions,
controlStream,
connIndex,
@@ -373,7 +376,7 @@ func serveTunnel(
case connection.HTTP2, connection.HTTP2Warp:
edgeConn, err := edgediscovery.DialEdge(ctx, dialTimeout, config.EdgeTLSConfigs[protocol], addr.TCP)
if err != nil {
connLog.Err(err).Msg("Unable to establish connection with Cloudflare edge")
connLog.ConnAwareLogger().Err(err).Msg("Unable to establish connection with Cloudflare edge")
return err, true
}
@@ -395,7 +398,7 @@ func serveTunnel(
default:
edgeConn, err := edgediscovery.DialEdge(ctx, dialTimeout, config.EdgeTLSConfigs[protocol], addr.TCP)
if err != nil {
connLog.Err(err).Msg("Unable to establish connection with Cloudflare edge")
connLog.ConnAwareLogger().Err(err).Msg("Unable to establish connection with Cloudflare edge")
return err, true
}
@@ -427,7 +430,7 @@ func (r unrecoverableError) Error() string {
func ServeH2mux(
ctx context.Context,
connLog *zerolog.Logger,
connLog *ConnAwareLogger,
credentialManager *reconnectCredentialManager,
config *TunnelConfig,
edgeConn net.Conn,
@@ -437,7 +440,7 @@ func ServeH2mux(
reconnectCh chan ReconnectSignal,
gracefulShutdownC <-chan struct{},
) error {
connLog.Debug().Msgf("Connecting via h2mux")
connLog.Logger().Debug().Msgf("Connecting via h2mux")
// Returns error from parsing the origin URL or handshake errors
handler, err, recoverable := connection.NewH2muxConnection(
config.ConnectionConfig,
@@ -474,7 +477,7 @@ func ServeH2mux(
func ServeHTTP2(
ctx context.Context,
connLog *zerolog.Logger,
connLog *ConnAwareLogger,
config *TunnelConfig,
tlsServerConn net.Conn,
connOptions *tunnelpogs.ConnectionOptions,
@@ -483,7 +486,7 @@ func ServeHTTP2(
gracefulShutdownC <-chan struct{},
reconnectCh chan ReconnectSignal,
) error {
connLog.Debug().Msgf("Connecting via http2")
connLog.Logger().Debug().Msgf("Connecting via http2")
h2conn := connection.NewHTTP2Connection(
tlsServerConn,
config.ConnectionConfig,
@@ -515,6 +518,7 @@ func ServeQUIC(
ctx context.Context,
edgeAddr *net.UDPAddr,
config *TunnelConfig,
connLogger *ConnAwareLogger,
connOptions *tunnelpogs.ConnectionOptions,
controlStreamHandler connection.ControlStreamHandler,
connIndex uint8,
@@ -528,7 +532,8 @@ func ServeQUIC(
MaxIncomingStreams: connection.MaxConcurrentStreams,
MaxIncomingUniStreams: connection.MaxConcurrentStreams,
KeepAlive: true,
Tracer: quicpogs.NewClientTracer(config.Log, connIndex),
EnableDatagrams: true,
Tracer: quicpogs.NewClientTracer(connLogger.Logger(), connIndex),
}
for {
select {
@@ -545,7 +550,7 @@ func ServeQUIC(
controlStreamHandler,
config.Observer)
if err != nil {
config.Log.Error().Msgf("Failed to create new quic connection, err: %v", err)
connLogger.ConnAwareLogger().Err(err).Msgf("Failed to create new quic connection")
return err, true
}
@@ -553,7 +558,7 @@ func ServeQUIC(
errGroup.Go(func() error {
err := quicConn.Serve(ctx)
if err != nil {
config.Log.Error().Msgf("Failed to serve quic connection, err: %v", err)
connLogger.ConnAwareLogger().Err(err).Msg("Failed to serve quic connection")
}
return fmt.Errorf("Connection with edge closed")
})
+88
View File
@@ -0,0 +1,88 @@
package quic
import (
"fmt"
"github.com/google/uuid"
"github.com/lucas-clemente/quic-go"
"github.com/pkg/errors"
)
const (
MaxDatagramFrameSize = 1220
sessionIDLen = len(uuid.UUID{})
)
type DatagramMuxer struct {
ID uuid.UUID
session quic.Session
}
func NewDatagramMuxer(quicSession quic.Session) (*DatagramMuxer, error) {
muxerID, err := uuid.NewRandom()
if err != nil {
return nil, err
}
return &DatagramMuxer{
ID: muxerID,
session: quicSession,
}, nil
}
// SendTo suffix the session ID to the payload so the other end of the QUIC session can demultiplex
// the payload from multiple datagram sessions
func (dm *DatagramMuxer) SendTo(sessionID uuid.UUID, payload []byte) error {
if len(payload) > MaxDatagramFrameSize-sessionIDLen {
// TODO: TUN-5302 return ICMP packet too big message
return fmt.Errorf("origin UDP payload has %d bytes, which exceeds transport MTU %d", len(payload), MaxDatagramFrameSize-sessionIDLen)
}
msgWithID, err := SuffixSessionID(sessionID, payload)
if err != nil {
return errors.Wrap(err, "Failed to suffix session ID to datagram, it will be dropped")
}
if err := dm.session.SendMessage(msgWithID); err != nil {
return errors.Wrap(err, "Failed to send datagram back to edge")
}
return nil
}
// ReceiveFrom extracts datagram session ID, then sends the session ID and payload to session manager
// which determines how to proxy to the origin. It assumes the datagram session has already been
// registered with session manager through other side channel
func (dm *DatagramMuxer) ReceiveFrom() (uuid.UUID, []byte, error) {
msg, err := dm.session.ReceiveMessage()
if err != nil {
return uuid.Nil, nil, err
}
return ExtractSessionID(msg)
}
func (dm *DatagramMuxer) MTU() uint {
return MaxDatagramFrameSize
}
// Each QUIC datagram should be suffixed with session ID.
// ExtractSessionID extracts the session ID and a slice with only the payload
func ExtractSessionID(b []byte) (uuid.UUID, []byte, error) {
msgLen := len(b)
if msgLen < sessionIDLen {
return uuid.Nil, nil, fmt.Errorf("session ID has %d bytes, but data only has %d", sessionIDLen, len(b))
}
// Parse last 16 bytess as UUID and remove it from slice
sessionID, err := uuid.FromBytes(b[len(b)-sessionIDLen:])
if err != nil {
return uuid.Nil, nil, err
}
b = b[:len(b)-sessionIDLen]
return sessionID, b, nil
}
// SuffixSessionID appends the session ID at the end of the payload. Suffix is more performant than prefix because
// the payload slice might already have enough capacity to append the session ID at the end
func SuffixSessionID(sessionID uuid.UUID, b []byte) ([]byte, error) {
if len(b)+len(sessionID) > MaxDatagramFrameSize {
return nil, fmt.Errorf("datagram size exceed %d", MaxDatagramFrameSize)
}
b = append(b, sessionID[:]...)
return b, nil
}
+41
View File
@@ -0,0 +1,41 @@
package quic
import (
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
var (
testSessionID = uuid.New()
)
func TestSuffixThenRemoveSessionID(t *testing.T) {
msg := []byte(t.Name())
msgWithID, err := SuffixSessionID(testSessionID, msg)
require.NoError(t, err)
require.Len(t, msgWithID, len(msg)+sessionIDLen)
sessionID, msgWithoutID, err := ExtractSessionID(msgWithID)
require.NoError(t, err)
require.Equal(t, msg, msgWithoutID)
require.Equal(t, testSessionID, sessionID)
}
func TestRemoveSessionIDError(t *testing.T) {
// message is too short to contain session ID
msg := []byte("test")
_, _, err := ExtractSessionID(msg)
require.Error(t, err)
}
func TestSuffixSessionIDError(t *testing.T) {
msg := make([]byte, MaxDatagramFrameSize-sessionIDLen)
_, err := SuffixSessionID(testSessionID, msg)
require.NoError(t, err)
msg = make([]byte, MaxDatagramFrameSize-sessionIDLen+1)
_, err = SuffixSessionID(testSessionID, msg)
require.Error(t, err)
}
+1 -1
View File
@@ -366,7 +366,7 @@ func (sc *serverCollector) lostPackets(reason logging.PacketLossReason) {
func (sc *serverCollector) updatedRTT(rtt *logging.RTTStats) {
latestRTT := rtt.LatestRTT()
// May return 0 if no valid updates have occured
// May return 0 if no valid updates have occurred
if latestRTT > 0 {
serverMetrics.rtt.Observe(durationToPromGauge(latestRTT))
}
+180 -69
View File
@@ -1,16 +1,33 @@
package quic
import (
"bytes"
"context"
"fmt"
"io"
"net"
"time"
capnp "zombiezen.com/go/capnproto2"
"zombiezen.com/go/capnproto2/rpc"
"github.com/google/uuid"
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/tunnelrpc"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
// protocolSignature is a custom protocol signature to ensure that whoever performs a handshake does not write data
// before writing the metadata.
var protocolSignature = []byte{0x0A, 0x36, 0xCD, 0x12, 0xA1, 0x3E}
// The first 6 bytes of the stream is used to distinguish the type of stream. It ensures whoever performs a handshake does
// not write data before writing the metadata.
type ProtocolSignature [6]byte
var (
// DataStreamProtocolSignature is a custom protocol signature for data stream
DataStreamProtocolSignature = ProtocolSignature{0x0A, 0x36, 0xCD, 0x12, 0xA1, 0x3E}
// RPCStreamProtocolSignature is a custom protocol signature for RPC stream
RPCStreamProtocolSignature = ProtocolSignature{0x52, 0xBB, 0x82, 0x5C, 0xDB, 0x65}
)
const protocolVersionLength = 2
@@ -20,18 +37,26 @@ const (
protocolV1 protocolVersion = "01"
)
// RequestServerStream is a stream to serve requests
type RequestServerStream struct {
io.ReadWriteCloser
}
func NewRequestServerStream(stream io.ReadWriteCloser, signature ProtocolSignature) (*RequestServerStream, error) {
if signature != DataStreamProtocolSignature {
return nil, fmt.Errorf("RequestClientStream can only be created from data stream")
}
return &RequestServerStream{stream}, nil
}
// ReadConnectRequestData reads the handshake data from a QUIC stream.
func ReadConnectRequestData(stream io.Reader) (*ConnectRequest, error) {
if err := verifySignature(stream); err != nil {
return nil, err
}
func (rss *RequestServerStream) ReadConnectRequestData() (*ConnectRequest, error) {
// This is a NO-OP for now. We could cause a branching if we wanted to use multiple versions.
if _, err := readVersion(stream); err != nil {
if _, err := readVersion(rss); err != nil {
return nil, err
}
msg, err := capnp.NewDecoder(stream).Decode()
msg, err := capnp.NewDecoder(rss).Decode()
if err != nil {
return nil, err
}
@@ -43,50 +68,8 @@ func ReadConnectRequestData(stream io.Reader) (*ConnectRequest, error) {
return r, nil
}
// WriteConnectRequestData writes requestMeta to a stream.
func WriteConnectRequestData(stream io.Writer, dest string, connectionType ConnectionType, metadata ...Metadata) error {
connectRequest := &ConnectRequest{
Dest: dest,
Type: connectionType,
Metadata: metadata,
}
msg, err := connectRequest.toPogs()
if err != nil {
return err
}
if err := writePreamble(stream); err != nil {
return err
}
return capnp.NewEncoder(stream).Encode(msg)
}
// ReadConnectResponseData reads the response to a RequestMeta in a stream.
func ReadConnectResponseData(stream io.Reader) (*ConnectResponse, error) {
if err := verifySignature(stream); err != nil {
return nil, err
}
// This is a NO-OP for now. We could cause a branching if we wanted to use multiple versions.
if _, err := readVersion(stream); err != nil {
return nil, err
}
msg, err := capnp.NewDecoder(stream).Decode()
if err != nil {
return nil, err
}
r := &ConnectResponse{}
if err := r.fromPogs(msg); err != nil {
return nil, err
}
return r, nil
}
// WriteConnectResponseData writes response to a QUIC stream.
func WriteConnectResponseData(stream io.Writer, respErr error, metadata ...Metadata) error {
func (rss *RequestServerStream) WriteConnectResponseData(respErr error, metadata ...Metadata) error {
var connectResponse *ConnectResponse
if respErr != nil {
connectResponse = &ConnectResponse{
@@ -103,14 +86,105 @@ func WriteConnectResponseData(stream io.Writer, respErr error, metadata ...Metad
return err
}
if err := writePreamble(stream); err != nil {
if err := writeDataStreamPreamble(rss); err != nil {
return err
}
return capnp.NewEncoder(stream).Encode(msg)
return capnp.NewEncoder(rss).Encode(msg)
}
func writePreamble(stream io.Writer) error {
if err := writeSignature(stream); err != nil {
type RequestClientStream struct {
io.ReadWriteCloser
}
// WriteConnectRequestData writes requestMeta to a stream.
func (rcs *RequestClientStream) WriteConnectRequestData(dest string, connectionType ConnectionType, metadata ...Metadata) error {
connectRequest := &ConnectRequest{
Dest: dest,
Type: connectionType,
Metadata: metadata,
}
msg, err := connectRequest.toPogs()
if err != nil {
return err
}
if err := writeDataStreamPreamble(rcs); err != nil {
return err
}
return capnp.NewEncoder(rcs).Encode(msg)
}
// ReadConnectResponseData reads the response to a RequestMeta in a stream.
func (rcs *RequestClientStream) ReadConnectResponseData() (*ConnectResponse, error) {
signature, err := DetermineProtocol(rcs)
if err != nil {
return nil, err
}
if signature != DataStreamProtocolSignature {
return nil, fmt.Errorf("Wrong protocol signature %v", signature)
}
// This is a NO-OP for now. We could cause a branching if we wanted to use multiple versions.
if _, err := readVersion(rcs); err != nil {
return nil, err
}
msg, err := capnp.NewDecoder(rcs).Decode()
if err != nil {
return nil, err
}
r := &ConnectResponse{}
if err := r.fromPogs(msg); err != nil {
return nil, err
}
return r, nil
}
// RPCServerStream is a stream to serve RPCs. It is closed when the RPC client is done
type RPCServerStream struct {
io.ReadWriteCloser
}
func NewRPCServerStream(stream io.ReadWriteCloser, protocol ProtocolSignature) (*RPCServerStream, error) {
if protocol != RPCStreamProtocolSignature {
return nil, fmt.Errorf("RPCStream can only be created from rpc stream")
}
return &RPCServerStream{stream}, nil
}
func (s *RPCServerStream) Serve(sessionManager tunnelpogs.SessionManager, logger *zerolog.Logger) error {
rpcTransport := tunnelrpc.NewTransportLogger(logger, rpc.StreamTransport(s))
defer rpcTransport.Close()
main := tunnelpogs.SessionManager_ServerToClient(sessionManager)
rpcConn := rpc.NewConn(
rpcTransport,
rpc.MainInterface(main.Client),
)
defer rpcConn.Close()
return rpcConn.Wait()
}
func DetermineProtocol(stream io.Reader) (ProtocolSignature, error) {
signature, err := readSignature(stream)
if err != nil {
return ProtocolSignature{}, err
}
switch signature {
case DataStreamProtocolSignature:
return DataStreamProtocolSignature, nil
case RPCStreamProtocolSignature:
return RPCStreamProtocolSignature, nil
default:
return ProtocolSignature{}, fmt.Errorf("Unknown signature %v", signature)
}
}
func writeDataStreamPreamble(stream io.Writer) error {
if err := writeSignature(stream, DataStreamProtocolSignature); err != nil {
return err
}
@@ -128,20 +202,57 @@ func readVersion(stream io.Reader) (string, error) {
return string(version), err
}
func writeSignature(stream io.Writer) error {
_, err := stream.Write(protocolSignature)
func readSignature(stream io.Reader) (ProtocolSignature, error) {
var signature ProtocolSignature
if _, err := io.ReadFull(stream, signature[:]); err != nil {
return ProtocolSignature{}, err
}
return signature, nil
}
func writeSignature(stream io.Writer, signature ProtocolSignature) error {
_, err := stream.Write(signature[:])
return err
}
func verifySignature(stream io.Reader) error {
signature := make([]byte, len(protocolSignature))
if _, err := io.ReadFull(stream, signature); err != nil {
// RPCClientStream is a stream to call methods of SessionManager
type RPCClientStream struct {
client tunnelpogs.SessionManager_PogsClient
transport rpc.Transport
}
func NewRPCClientStream(ctx context.Context, stream io.ReadWriteCloser, logger *zerolog.Logger) (*RPCClientStream, error) {
n, err := stream.Write(RPCStreamProtocolSignature[:])
if err != nil {
return nil, err
}
if n != len(RPCStreamProtocolSignature) {
return nil, fmt.Errorf("expect to write %d bytes for RPC stream protocol signature, wrote %d", len(RPCStreamProtocolSignature), n)
}
transport := tunnelrpc.NewTransportLogger(logger, rpc.StreamTransport(stream))
conn := rpc.NewConn(
transport,
tunnelrpc.ConnLog(logger),
)
return &RPCClientStream{
client: tunnelpogs.SessionManager_PogsClient{Client: conn.Bootstrap(ctx), Conn: conn},
transport: transport,
}, nil
}
func (rcs *RPCClientStream) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeIdleAfterHint time.Duration) error {
resp, err := rcs.client.RegisterUdpSession(ctx, sessionID, dstIP, dstPort, closeIdleAfterHint)
if err != nil {
return err
}
if !bytes.Equal(signature[0:], protocolSignature) {
return fmt.Errorf("Wrong signature: %v", signature)
}
return nil
return resp.Err
}
func (rcs *RPCClientStream) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID) error {
return rcs.client.UnregisterUdpSession(ctx, sessionID)
}
func (rcs *RPCClientStream) Close() {
_ = rcs.client.Close()
_ = rcs.transport.Close()
}
+120 -7
View File
@@ -2,13 +2,24 @@ package quic
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net"
"testing"
"time"
"github.com/google/uuid"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
testCloseIdleAfterHint = time.Minute * 2
)
func TestConnectRequestData(t *testing.T) {
var tests = []struct {
name string
@@ -21,7 +32,7 @@ func TestConnectRequestData(t *testing.T) {
hostname: "tunnel.com",
connectionType: ConnectionTypeHTTP,
metadata: []Metadata{
Metadata{
{
Key: "key",
Val: "1234",
},
@@ -31,9 +42,15 @@ func TestConnectRequestData(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
b := &bytes.Buffer{}
err := WriteConnectRequestData(b, test.hostname, test.connectionType, test.metadata...)
reqClientStream := RequestClientStream{noopCloser{b}}
err := reqClientStream.WriteConnectRequestData(test.hostname, test.connectionType, test.metadata...)
require.NoError(t, err)
reqMeta, err := ReadConnectRequestData(b)
protocol, err := DetermineProtocol(b)
require.NoError(t, err)
reqServerStream, err := NewRequestServerStream(noopCloser{b}, protocol)
require.NoError(t, err)
reqMeta, err := reqServerStream.ReadConnectRequestData()
require.NoError(t, err)
assert.Equal(t, test.metadata, reqMeta.Metadata)
@@ -52,7 +69,7 @@ func TestConnectResponseMeta(t *testing.T) {
{
name: "Signature verified and response metadata is unmarshaled and read correctly",
metadata: []Metadata{
Metadata{
{
Key: "key",
Val: "1234",
},
@@ -62,7 +79,7 @@ func TestConnectResponseMeta(t *testing.T) {
name: "If error is not empty, other fields should be blank",
err: errors.New("something happened"),
metadata: []Metadata{
Metadata{
{
Key: "key",
Val: "1234",
},
@@ -73,9 +90,12 @@ func TestConnectResponseMeta(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
b := &bytes.Buffer{}
err := WriteConnectResponseData(b, test.err, test.metadata...)
reqServerStream := RequestServerStream{noopCloser{b}}
err := reqServerStream.WriteConnectResponseData(test.err, test.metadata...)
require.NoError(t, err)
respMeta, err := ReadConnectResponseData(b)
reqClientStream := RequestClientStream{noopCloser{b}}
respMeta, err := reqClientStream.ReadConnectResponseData()
require.NoError(t, err)
if respMeta.Error == "" {
@@ -86,3 +106,96 @@ func TestConnectResponseMeta(t *testing.T) {
})
}
}
func TestRegisterUdpSession(t *testing.T) {
clientReader, serverWriter := io.Pipe()
serverReader, clientWriter := io.Pipe()
clientStream := mockRPCStream{clientReader, clientWriter}
serverStream := mockRPCStream{serverReader, serverWriter}
rpcServer := mockRPCServer{
sessionID: uuid.New(),
dstIP: net.IP{172, 16, 0, 1},
dstPort: 8000,
closeIdleAfter: testCloseIdleAfterHint,
}
logger := zerolog.Nop()
sessionRegisteredChan := make(chan struct{})
go func() {
protocol, err := DetermineProtocol(serverStream)
assert.NoError(t, err)
rpcServerStream, err := NewRPCServerStream(serverStream, protocol)
assert.NoError(t, err)
err = rpcServerStream.Serve(rpcServer, &logger)
assert.NoError(t, err)
serverStream.Close()
close(sessionRegisteredChan)
}()
rpcClientStream, err := NewRPCClientStream(context.Background(), clientStream, &logger)
assert.NoError(t, err)
assert.NoError(t, rpcClientStream.RegisterUdpSession(context.Background(), rpcServer.sessionID, rpcServer.dstIP, rpcServer.dstPort, testCloseIdleAfterHint))
// Different sessionID, the RPC server should reject the registraion
assert.Error(t, rpcClientStream.RegisterUdpSession(context.Background(), uuid.New(), rpcServer.dstIP, rpcServer.dstPort, testCloseIdleAfterHint))
assert.NoError(t, rpcClientStream.UnregisterUdpSession(context.Background(), rpcServer.sessionID))
// Different sessionID, the RPC server should reject the unregistraion
assert.Error(t, rpcClientStream.UnregisterUdpSession(context.Background(), uuid.New()))
rpcClientStream.Close()
<-sessionRegisteredChan
}
type mockRPCServer struct {
sessionID uuid.UUID
dstIP net.IP
dstPort uint16
closeIdleAfter time.Duration
}
func (s mockRPCServer) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeIdleAfter time.Duration) error {
if s.sessionID != sessionID {
return fmt.Errorf("expect session ID %s, got %s", s.sessionID, sessionID)
}
if !s.dstIP.Equal(dstIP) {
return fmt.Errorf("expect destination IP %s, got %s", s.dstIP, dstIP)
}
if s.dstPort != dstPort {
return fmt.Errorf("expect destination port %d, got %d", s.dstPort, dstPort)
}
if s.closeIdleAfter != closeIdleAfter {
return fmt.Errorf("expect closeIdleAfter %d, got %d", s.closeIdleAfter, closeIdleAfter)
}
return nil
}
func (s mockRPCServer) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID) error {
if s.sessionID != sessionID {
return fmt.Errorf("expect session ID %s, got %s", s.sessionID, sessionID)
}
return nil
}
type mockRPCStream struct {
io.ReadCloser
io.WriteCloser
}
func (s mockRPCStream) Close() error {
_ = s.ReadCloser.Close()
_ = s.WriteCloser.Close()
return nil
}
type noopCloser struct {
io.ReadWriter
}
func (noopCloser) Close() error {
return nil
}
+2 -2
View File
@@ -18,7 +18,7 @@ const (
authFailure = uint8(1)
)
// AuthHandler handles socks authenication requests
// AuthHandler handles socks authentication requests
type AuthHandler interface {
Handle(io.Reader, io.Writer) error
Register(uint8, Authenticator)
@@ -43,7 +43,7 @@ func (h *StandardAuthHandler) Register(method uint8, a Authenticator) {
h.authenticators[method] = a
}
// Handle gets the methods from the SOCKS5 client and authenicates with the first supported method
// Handle gets the methods from the SOCKS5 client and authenticates with the first supported method
func (h *StandardAuthHandler) Handle(bufConn io.Reader, conn io.Writer) error {
methods, err := readMethods(bufConn)
if err != nil {
+1 -1
View File
@@ -55,7 +55,7 @@ func (h *StandardRequestHandler) handleConnect(conn io.ReadWriter, req *Request)
addr, err := net.ResolveIPAddr("ip", req.DestAddr.FQDN)
if err != nil {
_ = sendReply(conn, ruleFailure, req.DestAddr)
return fmt.Errorf("unable to resolve host to confirm acceess")
return fmt.Errorf("unable to resolve host to confirm access")
}
req.DestAddr.IP = addr.IP
+1 -1
View File
@@ -178,7 +178,7 @@ class TestSSHShell(TestSSHBase):
self.assertIn(username, pwd)
# Ensure shell launched with correct user's permissions and privs.
# Cant read root owned 0700 files.
# Can't read root owned 0700 files.
output = self.get_command_output(
session, f"cat {self.ROOT_ONLY_TEST_FILE_PATH}"
)
+40 -14
View File
@@ -16,11 +16,13 @@ import (
// network, and says that eyeballs can reach that route using the corresponding
// tunnel.
type Route struct {
Network CIDR `json:"network"`
TunnelID uuid.UUID `json:"tunnel_id"`
Comment string `json:"comment"`
CreatedAt time.Time `json:"created_at"`
DeletedAt time.Time `json:"deleted_at"`
Network CIDR `json:"network"`
TunnelID uuid.UUID `json:"tunnel_id"`
// Optional field. When unset, it means the Route belongs to the default virtual network.
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
Comment string `json:"comment"`
CreatedAt time.Time `json:"created_at"`
DeletedAt time.Time `json:"deleted_at"`
}
// CIDR is just a newtype wrapper around net.IPNet. It adds JSON unmarshalling.
@@ -62,27 +64,33 @@ type NewRoute struct {
Network net.IPNet
TunnelID uuid.UUID
Comment string
// Optional field. If unset, backend will assume the default vnet for the account.
VNetID *uuid.UUID
}
// MarshalJSON handles fields with non-JSON types (e.g. net.IPNet).
func (r NewRoute) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
TunnelID uuid.UUID `json:"tunnel_id"`
Comment string `json:"comment"`
TunnelID uuid.UUID `json:"tunnel_id"`
Comment string `json:"comment"`
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
}{
TunnelID: r.TunnelID,
Comment: r.Comment,
VNetID: r.VNetID,
})
}
// DetailedRoute is just a Route with some extra fields, e.g. TunnelName.
type DetailedRoute struct {
Network CIDR `json:"network"`
TunnelID uuid.UUID `json:"tunnel_id"`
Comment string `json:"comment"`
CreatedAt time.Time `json:"created_at"`
DeletedAt time.Time `json:"deleted_at"`
TunnelName string `json:"tunnel_name"`
Network CIDR `json:"network"`
TunnelID uuid.UUID `json:"tunnel_id"`
// Optional field. When unset, it means the DetailedRoute belongs to the default virtual network.
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
Comment string `json:"comment"`
CreatedAt time.Time `json:"created_at"`
DeletedAt time.Time `json:"deleted_at"`
TunnelName string `json:"tunnel_name"`
}
// IsZero checks if DetailedRoute is the zero value.
@@ -97,9 +105,15 @@ func (r DetailedRoute) TableString() string {
if !r.DeletedAt.IsZero() {
deletedColumn = r.DeletedAt.Format(time.RFC3339)
}
vnetColumn := "default"
if r.VNetID != nil {
vnetColumn = r.VNetID.String()
}
return fmt.Sprintf(
"%s\t%s\t%s\t%s\t%s\t%s\t",
"%s\t%s\t%s\t%s\t%s\t%s\t%s\t",
r.Network.String(),
vnetColumn,
r.Comment,
r.TunnelID,
r.TunnelName,
@@ -107,3 +121,15 @@ func (r DetailedRoute) TableString() string {
deletedColumn,
)
}
type DeleteRouteParams struct {
Network net.IPNet
// Optional field. If unset, backend will assume the default vnet for the account.
VNetID *uuid.UUID
}
type GetRouteByIpParams struct {
Ip net.IP
// Optional field. If unset, backend will assume the default vnet for the account.
VNetID *uuid.UUID
}
+133 -56
View File
@@ -12,77 +12,154 @@ import (
)
func TestUnmarshalRoute(t *testing.T) {
// Response from the teamnet route backend
data := `{
"network":"10.1.2.40/29",
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
"comment":"test",
"created_at":"2020-12-22T02:00:15.587008Z",
"deleted_at":null
}`
var r Route
err := json.Unmarshal([]byte(data), &r)
testCases := []struct {
Json string
HasVnet bool
}{
{
`{
"network":"10.1.2.40/29",
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
"comment":"test",
"created_at":"2020-12-22T02:00:15.587008Z",
"deleted_at":null
}`,
false,
},
{
`{
"network":"10.1.2.40/29",
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
"comment":"test",
"created_at":"2020-12-22T02:00:15.587008Z",
"deleted_at":null,
"virtual_network_id":"38c95083-8191-4110-8339-3f438d44fdb9"
}`,
true,
},
}
// Check everything worked
require.NoError(t, err)
require.Equal(t, uuid.MustParse("fba6ffea-807f-4e7a-a740-4184ee1b82c8"), r.TunnelID)
require.Equal(t, "test", r.Comment)
_, cidr, err := net.ParseCIDR("10.1.2.40/29")
require.NoError(t, err)
require.Equal(t, CIDR(*cidr), r.Network)
require.Equal(t, "test", r.Comment)
for _, testCase := range testCases {
data := testCase.Json
var r Route
err := json.Unmarshal([]byte(data), &r)
// Check everything worked
require.NoError(t, err)
require.Equal(t, uuid.MustParse("fba6ffea-807f-4e7a-a740-4184ee1b82c8"), r.TunnelID)
require.Equal(t, "test", r.Comment)
_, cidr, err := net.ParseCIDR("10.1.2.40/29")
require.NoError(t, err)
require.Equal(t, CIDR(*cidr), r.Network)
require.Equal(t, "test", r.Comment)
if testCase.HasVnet {
require.Equal(t, uuid.MustParse("38c95083-8191-4110-8339-3f438d44fdb9"), *r.VNetID)
} else {
require.Nil(t, r.VNetID)
}
}
}
func TestDetailedRouteJsonRoundtrip(t *testing.T) {
// Response from the teamnet route backend
data := `{
"network":"10.1.2.40/29",
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
"comment":"test",
"created_at":"2020-12-22T02:00:15.587008Z",
"deleted_at":"2021-01-14T05:01:42.183002Z",
"tunnel_name":"Mr. Tun"
}`
var r DetailedRoute
err := json.Unmarshal([]byte(data), &r)
testCases := []struct {
Json string
HasVnet bool
}{
{
`{
"network":"10.1.2.40/29",
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
"comment":"test",
"created_at":"2020-12-22T02:00:15.587008Z",
"deleted_at":"2021-01-14T05:01:42.183002Z",
"tunnel_name":"Mr. Tun"
}`,
false,
},
{
`{
"network":"10.1.2.40/29",
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
"virtual_network_id":"38c95083-8191-4110-8339-3f438d44fdb9",
"comment":"test",
"created_at":"2020-12-22T02:00:15.587008Z",
"deleted_at":"2021-01-14T05:01:42.183002Z",
"tunnel_name":"Mr. Tun"
}`,
true,
},
}
// Check everything worked
require.NoError(t, err)
require.Equal(t, uuid.MustParse("fba6ffea-807f-4e7a-a740-4184ee1b82c8"), r.TunnelID)
require.Equal(t, "test", r.Comment)
_, cidr, err := net.ParseCIDR("10.1.2.40/29")
require.NoError(t, err)
require.Equal(t, CIDR(*cidr), r.Network)
require.Equal(t, "test", r.Comment)
require.Equal(t, "Mr. Tun", r.TunnelName)
for _, testCase := range testCases {
data := testCase.Json
bytes, err := json.Marshal(r)
require.NoError(t, err)
obtainedJson := string(bytes)
data = strings.Replace(data, "\t", "", -1)
data = strings.Replace(data, "\n", "", -1)
require.Equal(t, data, obtainedJson)
var r DetailedRoute
err := json.Unmarshal([]byte(data), &r)
// Check everything worked
require.NoError(t, err)
require.Equal(t, uuid.MustParse("fba6ffea-807f-4e7a-a740-4184ee1b82c8"), r.TunnelID)
require.Equal(t, "test", r.Comment)
_, cidr, err := net.ParseCIDR("10.1.2.40/29")
require.NoError(t, err)
require.Equal(t, CIDR(*cidr), r.Network)
require.Equal(t, "test", r.Comment)
require.Equal(t, "Mr. Tun", r.TunnelName)
if testCase.HasVnet {
require.Equal(t, uuid.MustParse("38c95083-8191-4110-8339-3f438d44fdb9"), *r.VNetID)
} else {
require.Nil(t, r.VNetID)
}
bytes, err := json.Marshal(r)
require.NoError(t, err)
obtainedJson := string(bytes)
data = strings.Replace(data, "\t", "", -1)
data = strings.Replace(data, "\n", "", -1)
require.Equal(t, data, obtainedJson)
}
}
func TestMarshalNewRoute(t *testing.T) {
_, network, err := net.ParseCIDR("1.2.3.4/32")
require.NoError(t, err)
require.NotNil(t, network)
newRoute := NewRoute{
Network: *network,
TunnelID: uuid.New(),
Comment: "hi",
vnetId := uuid.New()
newRoutes := []NewRoute{
{
Network: *network,
TunnelID: uuid.New(),
Comment: "hi",
},
{
Network: *network,
TunnelID: uuid.New(),
Comment: "hi",
VNetID: &vnetId,
},
}
// Test where receiver is struct
serialized, err := json.Marshal(newRoute)
require.NoError(t, err)
require.True(t, strings.Contains(string(serialized), "tunnel_id"))
for _, newRoute := range newRoutes {
// Test where receiver is struct
serialized, err := json.Marshal(newRoute)
require.NoError(t, err)
require.True(t, strings.Contains(string(serialized), "tunnel_id"))
// Test where receiver is pointer to struct
serialized, err = json.Marshal(&newRoute)
require.NoError(t, err)
require.True(t, strings.Contains(string(serialized), "tunnel_id"))
// Test where receiver is pointer to struct
serialized, err = json.Marshal(&newRoute)
require.NoError(t, err)
require.True(t, strings.Contains(string(serialized), "tunnel_id"))
if newRoute.VNetID == nil {
require.False(t, strings.Contains(string(serialized), "virtual_network_id"))
} else {
require.True(t, strings.Contains(string(serialized), "virtual_network_id"))
}
}
}
func TestRouteTableString(t *testing.T) {
+28 -1
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"net"
"net/url"
"strconv"
"time"
"github.com/google/uuid"
@@ -34,6 +35,11 @@ var (
Name: "filter-comment-is",
Usage: "Show only routes with this comment.",
}
filterVnet = cli.StringFlag{
Name: "filter-virtual-network-id",
Usage: "Show only routes that are attached to the given virtual network ID.",
}
// Flags contains all filter flags.
FilterFlags = []cli.Flag{
&filterDeleted,
@@ -41,6 +47,7 @@ var (
&filterSubset,
&filterSuperset,
&filterComment,
&filterVnet,
}
)
@@ -81,11 +88,23 @@ func NewFromCLI(c *cli.Context) (*Filter, error) {
if tunnelID := c.String(filterTunnelID.Name); tunnelID != "" {
u, err := uuid.Parse(tunnelID)
if err != nil {
return nil, errors.Wrap(err, "Couldn't parse UUID from --filter-tunnel-id")
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterTunnelID.Name)
}
f.tunnelID(u)
}
if vnetId := c.String(filterVnet.Name); vnetId != "" {
u, err := uuid.Parse(vnetId)
if err != nil {
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterVnet.Name)
}
f.vnetID(u)
}
if maxFetch := c.Int("max-fetch-size"); maxFetch > 0 {
f.MaxFetchSize(uint(maxFetch))
}
return f, nil
}
@@ -133,6 +152,14 @@ func (f *Filter) tunnelID(id uuid.UUID) {
f.queryParams.Set("tunnel_id", id.String())
}
func (f *Filter) vnetID(id uuid.UUID) {
f.queryParams.Set("virtual_network_id", id.String())
}
func (f *Filter) MaxFetchSize(max uint) {
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
}
func (f Filter) Encode() string {
return f.queryParams.Encode()
}
+2 -2
View File
@@ -178,7 +178,7 @@ func getToken(appURL *url.URL, appInfo *AppInfo, useHostOnly bool, log *zerolog.
return token, nil
}
// If an app token couldnt be found on disk, check for an org token and attempt to exchange it for an app token.
// If an app token couldn't be found on disk, check for an org token and attempt to exchange it for an app token.
var orgTokenPath string
orgToken, err := GetOrgTokenIfExists(appInfo.AuthDomain)
if err != nil {
@@ -213,7 +213,7 @@ func getToken(appURL *url.URL, appInfo *AppInfo, useHostOnly bool, log *zerolog.
// getTokensFromEdge will attempt to use the transfer service to retrieve an app and org token, save them to disk,
// and return the app token.
func getTokensFromEdge(appURL *url.URL, appTokenPath, orgTokenPath string, useHostOnly bool, log *zerolog.Logger) (string, error) {
// If no org token exists or if it couldnt be exchanged for an app token, then run the transfer service flow.
// If no org token exists or if it couldn't be exchanged for an app token, then run the transfer service flow.
// this weird parameter is the resource name (token) and the key/value
// we want to send to the transfer service. the key is token and the value
+2 -2
View File
@@ -49,12 +49,12 @@ func (u *UpstreamHTTPS) Exchange(ctx context.Context, query *dns.Msg) (*dns.Msg,
for _, bootstrap := range u.bootstraps {
endpoint, client, err := configureBootstrap(bootstrap)
if err != nil {
u.log.Err(err).Msgf("failed to configure boostrap upstream %s", bootstrap)
u.log.Err(err).Msgf("failed to configure bootstrap upstream %s", bootstrap)
continue
}
msg, err := exchange(queryBuf, query.Id, endpoint, client, u.log)
if err != nil {
u.log.Err(err).Msgf("failed to connect to a boostrap upstream %s", bootstrap)
u.log.Err(err).Msgf("failed to connect to a bootstrap upstream %s", bootstrap)
continue
}
return msg, nil
+150
View File
@@ -0,0 +1,150 @@
package pogs
import (
"context"
"fmt"
"net"
"time"
"github.com/cloudflare/cloudflared/tunnelrpc"
"github.com/google/uuid"
capnp "zombiezen.com/go/capnproto2"
"zombiezen.com/go/capnproto2/rpc"
"zombiezen.com/go/capnproto2/server"
)
type SessionManager interface {
RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration) error
UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID) error
}
type SessionManager_PogsImpl struct {
impl SessionManager
}
func SessionManager_ServerToClient(s SessionManager) tunnelrpc.SessionManager {
return tunnelrpc.SessionManager_ServerToClient(SessionManager_PogsImpl{s})
}
func (i SessionManager_PogsImpl) RegisterUdpSession(p tunnelrpc.SessionManager_registerUdpSession) error {
server.Ack(p.Options)
sessionIDRaw, err := p.Params.SessionId()
if err != nil {
return err
}
sessionID, err := uuid.FromBytes(sessionIDRaw)
if err != nil {
return err
}
dstIPRaw, err := p.Params.DstIp()
if err != nil {
return err
}
dstIP := net.IP(dstIPRaw)
if dstIP == nil {
return fmt.Errorf("%v is not valid IP", dstIPRaw)
}
dstPort := p.Params.DstPort()
closeIdleAfterHint := time.Duration(p.Params.CloseAfterIdleHint())
resp := RegisterUdpSessionResponse{}
registrationErr := i.impl.RegisterUdpSession(p.Ctx, sessionID, dstIP, dstPort, closeIdleAfterHint)
if registrationErr != nil {
resp.Err = registrationErr
}
result, err := p.Results.NewResult()
if err != nil {
return err
}
return resp.Marshal(result)
}
func (i SessionManager_PogsImpl) UnregisterUdpSession(p tunnelrpc.SessionManager_unregisterUdpSession) error {
server.Ack(p.Options)
sessionIDRaw, err := p.Params.SessionId()
if err != nil {
return err
}
sessionID, err := uuid.FromBytes(sessionIDRaw)
if err != nil {
return err
}
return i.impl.UnregisterUdpSession(p.Ctx, sessionID)
}
type RegisterUdpSessionResponse struct {
Err error
}
func (p *RegisterUdpSessionResponse) Marshal(s tunnelrpc.RegisterUdpSessionResponse) error {
if p.Err != nil {
return s.SetErr(p.Err.Error())
}
return nil
}
func (p *RegisterUdpSessionResponse) Unmarshal(s tunnelrpc.RegisterUdpSessionResponse) error {
respErr, err := s.Err()
if err != nil {
return err
}
if respErr != "" {
p.Err = fmt.Errorf(respErr)
}
return nil
}
type SessionManager_PogsClient struct {
Client capnp.Client
Conn *rpc.Conn
}
func (c SessionManager_PogsClient) Close() error {
c.Client.Close()
return c.Conn.Close()
}
func (c SessionManager_PogsClient) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration) (*RegisterUdpSessionResponse, error) {
client := tunnelrpc.SessionManager{Client: c.Client}
promise := client.RegisterUdpSession(ctx, func(p tunnelrpc.SessionManager_registerUdpSession_Params) error {
if err := p.SetSessionId(sessionID[:]); err != nil {
return err
}
if err := p.SetDstIp(dstIP); err != nil {
return err
}
p.SetDstPort(dstPort)
p.SetCloseAfterIdleHint(int64(closeAfterIdleHint))
return nil
})
result, err := promise.Result().Struct()
if err != nil {
return nil, wrapRPCError(err)
}
response := new(RegisterUdpSessionResponse)
err = response.Unmarshal(result)
if err != nil {
return nil, err
}
return response, nil
}
func (c SessionManager_PogsClient) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID) error {
client := tunnelrpc.SessionManager{Client: c.Client}
promise := client.UnregisterUdpSession(ctx, func(p tunnelrpc.SessionManager_unregisterUdpSession_Params) error {
if err := p.SetSessionId(sessionID[:]); err != nil {
return err
}
return nil
})
_, err := promise.Struct()
return err
}
+10
View File
@@ -142,3 +142,13 @@ interface TunnelServer extends (RegistrationServer) {
authenticate @4 (originCert :Data, hostname :Text, options :RegistrationOptions) -> (result :AuthenticateResponse);
reconnectTunnel @5 (jwt :Data, eventDigest :Data, connDigest :Data, hostname :Text, options :RegistrationOptions) -> (result :TunnelRegistration);
}
struct RegisterUdpSessionResponse {
err @0 :Text;
}
interface SessionManager {
# Let the edge decide closeAfterIdle to make sure cloudflared doesn't close session before the edge closes its side
registerUdpSession @0 (sessionId :Data, dstIp :Data, dstPort: UInt16, closeAfterIdleHint: Int64) -> (result :RegisterUdpSessionResponse);
unregisterUdpSession @1 (sessionId :Data) -> ();
}
+693 -176
View File
@@ -3371,195 +3371,711 @@ func (p TunnelServer_reconnectTunnel_Results_Promise) Result() TunnelRegistratio
return TunnelRegistration_Promise{Pipeline: p.Pipeline.GetPipeline(0)}
}
const schema_db8274f9144abc7e = "x\xda\xccY{l\x1c\xd5\xd5?g\xee\xaeg\xed\xd8" +
"\xac\x87\xd9\xc4\x89\x05\x9f\xbf/\x0a\xe2\xc3`\xc0q\xa9" +
"\xd2\x94\xd6v\xb0Sl\xf2\xf0x\x93\x96G\x82\x18\xef" +
"\xde\xd8\xe3\xee\xce,3\xb3\xc6N\x13\x92\x98\xa4\x10\xc4" +
"+!\xe1\x91\x926\x04\xd1\xaa\x14ZR@m*\xaa" +
"B_\x80x\xa3P%\x10\xd4\x96\x90>\xa2PJ\xa0" +
"BT\x94\xa9\xce\xcc\xce\xc3kc;\xd0?\xfa\xdf\xea" +
"\xcc\xb9\xf7\x9es~\xe7\x9e\xf3\xbbg\xcf\xff\x9a\xd8&" +
"4\xc7\xc5\x19\x00\xca\xc6x\x85\xc3\x1b_Y\xbb\xe7\x8c" +
"_\x8d\x82R\x8f\xe8\\\xfbxw\xeaC{\xf4u\x88" +
"3\x11\xa0E\x8b\xafEy}\\\x04\x90G\xe2\x7f\x06" +
"t.\x91\xce\xbb\"\xf5\xd2\x0b\xd7\x81T\x1fU\x8e\x91" +
"\xf2\xea\x8aF\x94\xf3\x15\xa4\xacU\x90\xf2\x85\xf9\x17\xf7" +
"~~\xe7\xb3\x9bA\xaa\x17Be\xc0\x96\x95\xe2Z\x94" +
"5\x914\xb9\xb8\x1c\xd0yo\xc7\xec\x1f\xdc\xfb\xc2\xd3" +
"[@:\x13\xa1t\xf6z\xf15\x04\x94\xb7\x89?\x02" +
"tZ\x17?\xbf\xbf\xbe\xe5\x8e\x1de\xe7\x0a\xa4\xd8\x9c" +
"hD\xb9=A\xbb})q\x0d\xa03w\xe8\x8c\xab" +
"~\xf9\x9bG\xee\x04\xa5\x09\xd19\xdcw\xf6\xabl\xf7" +
"\x03\xaf\xc3J\x14Q\x00h\xb97\xb1\x976\xde\xe7\xea" +
"\xbex\xce\xe3?\xbb\xed\x91\xeb\xbf\x05\xca\x99\x88\x00\xae" +
"#3+\xffI\x0agU\xb6\x02:;\x0e\xfe|Y" +
"~\xdb\xae\xbd\x9ei\xeew\xa5R\x10 \xe6l\xee\xfa" +
" \xbf\xf2\xbe\xf4}%\xa3)J-\x9d\x95'\x10\xb0" +
"\xe5\xb2\xca\x06\x04t.x\xed\xe8\xf2\xa5?^\xf3\xbd" +
"\xc8\xdab\xd5ZZ\xfb\xd0\xff\\R9|t\xf1\xc3" +
" 5\xf9_\xae\xae\xea\xa5/\xb1\xd5\xecc\xf5\xae_" +
"<Z\x8e\x87\xebj\xbe\xaa\x0f\xe5MUnx\xaa\xdc" +
"#n|r\xd7\xd9\x89\xef\xbc\xf7\xd8D\x81\xb9\x7fF" +
"\x1f\xca\xfbgP`\x1e\x9bA\xce\xce\xec\xc2\xc3O4" +
"\xc7~\x12\x0d\xf3\xcc\xeac\xae\xb3\xd5\x14\xe6\xd3\xdf^" +
"T\xa3\xbf3\xfaD\xd9n\xae\xe23\xd5\xdd(\xbfQ" +
"M\xbb\x1dr\x95\xbb\xaf\xb8}{\xfc\xe8\xedO\x91\xa5" +
"\x11|\xe3\x09\xd7\xcf\x1a\x13\xe5\xad5\xf4sKM\x1d" +
"\x03t\xea\x1f\xfe\xe2\x0f\x17e\x0f=;\x81\xa5r\\" +
":!K\x12\xfd\xaa\x91\xc8\xd0#M\xfb\xbe\xf1\xd7\x9b" +
"_>P2\x14\xdd\\\x94\\T\xd6K\x84J\x00j" +
"Y\x94\\\xcd\xdd\xd2 \xca\xfb\xdc\xed\x1er\xb5\x85\xa3" +
"\xea\x9c\x8d\xbf\xfb\xf2\xe1\x08\x0e\xfb\xa47\x11b\xce\xb2" +
"\xaf^1X\xb9\xfe\xc8\x91\xe8A\xf7KnD\xf6\xbb" +
"K\xff\xf6\xddc\xb7\x1e\xcfg\xff\xe4\xe6\x92\x1f\xb3C" +
"\xd2B\x01P~W\xa2,\xafk\xa8\xe9\x9c{\xb0\xe7" +
"\x98\x07\xa5\xb7\xc5\xf3\xa7.\"\x85\xa3\xa7\xd2\x16\x17\\" +
"\xd5\xceW-\xb8\xf4\x18H\xf5l\xcc5\x88\xcb\x0bQ" +
"\x9e)\xd3\x02I\xbe\x1e\xe5\xb3Ru\x00\xce-\xd7v" +
",\xff\xc2\xdc'ODM\x9a\x93\xa2\xb4\x92\x9bR\xb4" +
"\xdf\x9a\x05\xc7\xbfr\xc6-\xbf=Q\x16HWqi" +
"\xaa\x11\xe5\xd5)r\xfd2R~g\xf1\xb7\x0f\xd4'" +
"\xeb\xdf/\x0bS\x85\x9bA\xa9A\x94\xb7\x91n\xcb\xcd" +
"\xa9\xa7(\x99\xbe\xf9\xfa\x95\xc3\xaf\\\xf7\xde?\xca\x11" +
"u\xb7\xde4\xab\x17\xe5\x9d\xb3h\xebm\xb3\x08\xff;" +
"W\xfce\xc3\xf1\x9d\xb3>\x18\xe7Ws\xdd \xca\x9d" +
"u\xa4\xd9^w\xbd|/\xfdr^\x12\xefk\xee\xd8" +
"\xf0\xec\x87\x91\x8c\xdfZ\xd7M\x19\x7f\x87x\xcf\x91\x8d" +
"\xbf\xbf\xf2\xa3\xa8\xc3[\xea\xde$\x87\xef\xae#\x87\xd7" +
"\xbds\xf7\xc5\xb7\xaez\xf0\xe3\x08|\xfb\xebFi\xa9" +
"]\xd4u\x9e3\x0b\xb1\xccy\xfe\xcf\xcc\xb9\x19\xb5\xa0" +
"\x17\x16\xb6\x17\xed\x01\xae\xdbZF\xb5y/o\xb5\x0a" +
"\x86n\xf1\x1eD\xa5\x96\xc5\x00b\x08 \xa9\x83\x00\xca" +
"U\x0c\x95\x9c\x80\x12b\x8a\x00\x964\x12\x0e0Tl" +
"\x01%AHQ\xc9\x90\xae\x9e\x0b\xa0\xe4\x18*\xc3\x02" +
"\"K!\x03\x90\x8a\xdb\x01\x94a\x86\xcaf\x01\x9d\x02" +
"7\xf3\xaa\xceuH\xda\x9d\xa6\x89\xd5 `5\xa0c" +
"r\xdb\x1cQ\xfbr\x90\xe4\x11\xb18x\x8d\x8d5 " +
"`\x0d\xa03`\x14Mk\xa5n\xa3\x96\xeb\xe5kL" +
"n\xe1\x00V\x80\x80\x15\x80\x81{l\xbc{\x17\xe54" +
"\xae\xdb\xc9.}\x8dQ\xe6T\xf7DNu\x97\x9c\xda" +
"\x1cqj\xd3\"\x00e\x1dC\xe5\x06\x01%V\xf2j" +
"K#\xf5\x05\x86\xcaM\x02:\x19\xf7\x90\xae,\x00\x04" +
"\xf6\xae\xe1\xaa]4\xb9E\xb2S\x00{\x18\xban\x9d" +
"\x02\xb8a\x88\x9b\x96f\xe8\xbe\x9bI\xd5\xcc\x0c\x04\xa1" +
"\x98\x04\xaa\xcea\xcd\xb25\xbd\x7f\x85+o\xed1r" +
"Zf\x84\xbc\xaav\xed<}!\x00\xa24\xf3r\x00" +
"\x14$i\x11@\xab\xd6\xaf\x1b&w\xb2\x9a\x951t" +
"\x9d\x03\xcb\xd8\x1b\xfa\xd4\x9c\xaagxpP\xc5\xf8\x83" +
"\xbc\x03\xd2\xdc\x1c\xe2\xe6\xb9j$A\xe6\xf5\xa8\xa6\xca" +
"\xf2\x96R\x1d\xc4\xb1\xf3r\x00\xa5\x83\xa1\xd2\x13\x89\xe3" +
"R\x8a\xe3\x12\x86\xca\xa5\x918\xae\xa48\xf60TV" +
"\x09\xe8\x18\xa6\xd6\xaf\xe9\x17q`f\x14c\xcb\xd6\xd5" +
"<\xa7\x98\x95\xe2\xb1\xc1(\xd8\x9a\xa1[X\x1b\xd6Q" +
"@\xac\x9d\x1cu\xcf\x81$\xe56\xc5'\x11X{\x16" +
"Y\xfb\xff\x0c\x95\xcfE\xacm\xa6T>\x9f\xa1r\xa1" +
"\x80\x8e\x9a\xc9\x18E\xdd^\x01L\xed/\x03%\xcd!" +
"\x991yh\xaf\x7fl|\x82\xbc\xa3xg\xc8\xf4^" +
"\xee\xdd\xa9sMn\x89\xc5\x9cM\xd6T;\x8eg\xce" +
"|\x00e\x1eC\xe5|\x01k\xf0c\xc7\xb3\xa7i{" +
"hO\x037M\xc3\xc4\xda\xb0\xe6\x94\xbc\xcf\x94\x0e@" +
"C\xef\xe0\xb6\xaa\xe5\x90b\x14\xb4\xb9\xb2\x18M\x05r" +
"Q7y\xbff\xd9\xdc\xf4\xc4\xf3Z\x09\xe9\xbc\xa5\xc4" +
"\x82\xd0\xd5\xec\x02Pj\x19*\xa7\x09\xe8\xf4\x9bj\x86" +
"\xf7p\x135#\xbbL\xd5\x8d4\xe3\x19\x8c\x83\x80\xf1" +
"\xc8\xa1\xa7\x9c\xec\xa1\xbd\xdc*\xe6l\x0b\x82U\x93\xaf" +
"7y)\x08\xa5\xe5=\x0d\x9e\xcd\xa9\xc0\xe6\xf5s\xc3" +
"\xda\x13\xc0\xbd\xa9/\xbc\xbaArn\xa5\xc4\xb8\x81\xa1" +
"\xb2#r\xc9\xb7Q\x1a\xdf\xc6P\xb9G@)\x16K" +
"a\x0c@\xba\x9b\xd2x\x07Ce\x8f0\xb6B\xf1!" +
"\xae\xdb\x1dZ?\x88\xdc\x0a\xa5db\x87\xd6\xcf\x81Y" +
"\x9f5\xd1\x13S\xc4\xc3\xe8\xb3\x8c\x1c\xb7y\x07\xcf\xe4" +
"TS\xb5\xb5!\xee}/%\xa3\x0f\xead\x1b\xf6\xba" +
"\x88\xd0bC\x1f\x07S\x98\xd4%\xa8\xd0\x9a\xac^\x85" +
"\xea\xcb\x0b\xb6&\x1a\xbaE\x971\x82\xce\xc2\x89\xd01" +
"Ct\xd0\x07g4\x0a\x0e\x96\xc0\xd9\x15\xe2 \xc5\x04" +
"\x0f\x9c\xdd{\x01\x94=\x0c\x95\x07\x05l\xf5\xca2\xd6" +
"\x86l\xbc\x14P\xaf\xf8,1\xa0!\xa3\xe6\xba\x0a\x01" +
",&/\xe4\xd4\x0c\xef\xc4R\xa1\x05D\x10\x10]\x14" +
"\xf3\x05\x93[\x16j\x86\xae\x14\xd5\x9c\xc6\xec\x91\xa0\xfd" +
"\xe8\xc5|\x8f\xc9\x8744\x8aV\xbbm\xf3\xbcX\xb0" +
"\xadq\xcdi\xd2\x00\xd1\x15\x16\xb5\x9cUV\xad\x1a\xc3" +
"\xf2\x10\x04\xa8\x89\xaa\xd59\x0c\x95\x05\x02&\x8bE-" +
"\x1b\x98\x9f32.n\x90\\\xa6\xe6\xf9\xb8\x8eR1" +
"\xe5u\x1as\x19{\xd4\xa4{\x9b\xfe\x9bJ\xfd\xe4\xfc" +
"\x85\\\x07\xb7\x1f\x86&S\x01hc\xa8,\x89\x98\xdc" +
"5?\xe2\x87o\xf2\xd2\xbe\xd0\x0f\xf1\xeb|\xc4\xb7\xaa" +
"\x81\xe7\xa9\xb8\xfa\xc1,9\xd3\x0e\xe2%\xa1\xced\xf6" +
"E/\xd4\xf2B\x83\xeb!\xd9\xb8\xc0\xb7Q\x1e\xc1n" +
"\x80\xf402Lo\xc6\xd0Ly\x13.\x02H\xaf#" +
"\xf9\x0d\x18Z*o\xc1z\x80\xf4F\x92\xdf\x84\x01\xcf" +
"\x92\xb7\xe2\x03\x00\xe9\x9bH|\x17\xa9\xc7\x98{%\xe4" +
"\x9d\xee\xf6;H\xbe\x87\xe4\xf1X\x0a\xe3\x00\xf2nl" +
"\x04H\xdfE\xf2GI^!\xa4\xb0\x02@\xde\x87\x83" +
"\x00\xe9\x87I\xfe8\xc9\xc5x\x8a\xa8\xa6\xbc\x1fM\x80" +
"\xf4OI\xfek\x92'f\xa70\x01 ?\xe9\xca\x9f" +
" \xf9s$\xaf\x9c\x93\xc2J\x00\xf9\x19\x1c\x05H?" +
"M\xf2\x03$\xaf\xc2\x14V\x01\xc8/\xe3.\x80\xf4\x01" +
"\x92\xff\x81\xe43*R8\x03@~\xc3\xb5\xe7 \xc9" +
"\xdf\"yu,\x85\xd5\x00\xf2\x1fq/@\xfa-\x92" +
"\xff\x9d\xe45b\x0ak\x00\xe4\xb7]\xbf\x8e\x93<!" +
"\x94\x910?\xa3\xca\x98\x163\xac\x002^\xba\xe3\xe8" +
"\xa5{\x8f\x91$6\x85\xc9\xf05\x0e\x88I@\xa7`" +
"\x18\xb9ec35i\xab\xfd\x96\xcf\xeaj\xc3\x17\x1b" +
" \x09\x83\xd6\x0cIC\xef\xca\x06\x85\xa0\xbc\xea\xf8\x96" +
"hV{\xd16\x8a\x05h\xc8\xaa6\xcf\x065\xc7," +
"\xea\x8bM#\xbf\x02\xb9\x99\xd7t57E5\xaa\x04" +
"\x01+\xa1T\x12\xfc\xbd'/M\x9f\xccQ\x83\x8c\x16" +
"\xca3\xba\xa1\xb0p\x85\xda?\x9d:5?\xa4ZI" +
"=R\x90\x1a\x86\xd4\\\xf1\xd3\x94\xa7\xb1\xdd\xbe\xb7\xd5" +
"c\x0bQ\x8aB\x0d%\xc1PI\x09\xd8jz\x1d\xaa" +
"\xd6\x7f\xdaM]J\xc6r\xb6\xa4\xff\x10r\xf7\xc7\xc8" +
"\xb8\x84\xce\x11J\xfbO\xdb\xfc~n{\xbf\xe8)B" +
"<Z\x8cv\xe2\x93[\xdd\xcb\xad\xe4t\\\x0f\x9f\xc0" +
"e\xce\x8b\xd3j\xfc\x13\xb4}\x9f\x16F\xdeQ\x84\xfd" +
"*\x86\xca@\x04{NM!\xcbP)\x84M<\xdf" +
"\x1b\xbe\x0d%&\x94\x1e\x87\xd4(\x0a\x0c\x95u\x02&" +
"\xe9\xa5\x81\xb5\xe1\x0ck\x8c\xd1c_W\x94\x0a]z" +
"\x96\x03\x0e\xfb\xd9\x1ci\x1f\xc1lhj\x025=\xb7" +
"}b:e\xc0\x83y\xcbt\xdf(\xad\xde\xa1\x94g" +
"\xb3Y\x1c \x98=\xa1?\xdf\x90\xf6\xad\x05A\xfa\xbe" +
"\x88\xe1|\x06\xfdq\x8c\xb4\xdb\x04A\xda)\xa2\x10\x0c" +
"\xe8\xd0\x1f\xc4I[o\x04A\xda\"\"\x0b\xe6k\xe8" +
"O\x08\x9aG\xaa\x10\x04i\xbd\x88\xb1`\xa6\x88\xfe|" +
"A\xbaz\x10\x04I\x131\x1e\x8c\xee\xd0\x1f4I\xab" +
"GA\x90V\x8a\x8e\x1f$h\xf5\xfchC\xc7\xcfQ" +
"hp\xb3\xb4\x0d\x1d\x9f9\xa2O*\x00\xda\xd0\xf1i" +
"*\xfb$\x9e\xeaj\xf9\xcfNH\xd2\xc3\xb3\x8d\xc8\x99" +
"w\xff\xb1T\x00\xa0\x0d\x95\x18F\xc6+\x00\x9f\x96\xea" +
"\xf4\xf2\x86\xcfRJ&\x00\xd7;'\x18;D\xf6%" +
"\xf6V\xcdP\x99-LI\xd8b\x9f\xe4\x85\x9f\xb4I" +
"ZL\xfb\xffo\xb0\xff\xcbDx\x9ec\xa8\x1c\x8c\\" +
"\xc7WI\xf8\x12C\xe5p\x84\xf0\x1c\xa2;z\x90\xa1" +
"\xf2~8\xaby\xf7F\x00\xe5}\x86\xbd\x11\x02!\xfd" +
"\x8b\x14?\xa26\xeb\xd2\x07\xf4\xe8C\x1c\xb7\x03\xa4\x13" +
"\xd4~S.}\x88y\xf4A\xc2>\x80t-\xc9O" +
"\x8b\xd2\x879x9@z6\xc9\xe7\xa1\x80\"\x8f\xcc" +
"y\x8afH\xb0rF\xff\x12M\x9f\xb0'\xf9\xc3#" +
"\xb4\x17\xabZ\xaehr\x08[b\xa9HtD\xba\xb4" +
"7Uj_C\xe9\x97\xa6\xe4\xc9\xa2\x85\"\x08(\x9e" +
"\xdccmZ\x1d\xa3\xd34\x0d4\xcb\xc8\xe7\xfc\x90|" +
"\x06\xdc\x938\xf4\xc5\x0c\x95\x15\x04E\x9b\x07\x85\xd2\x17" +
"\xd2\xe5\x86\x8cZ\xb4\xf88\x1f\x80q3x`[\x03" +
"F1\x97\xed\xe5 \xda\xe6HY\x08\xa6$\xa1i\x9e" +
"\xf4+N\xc2\xad8\xfe\xe8\x16\xfd\x09\xad\xd4\xbc\x0b\x04" +
"\xa9\x89*\x8e?\x8dD\x7f\x10/\xfd\xdf\x03 H\xa7" +
"\x87\x05\x00\xfd\x180C\x1f{\xe5\xbd\x0fn\x8e\xb6a" +
"\x0f\xe2\x7f\xe2\xd9\xe9\xb5\x9f\x93\xb8\xe9c\xa6WT\xc6" +
"\xc5\xe9\xf4\xcd\xe0\xcf\x9e\xb2\x9b^\xf9Y_\xe0~#" +
"\xf9w\x00\x00\x00\xff\xffH\xa22\xa3"
type RegisterUdpSessionResponse struct{ capnp.Struct }
// RegisterUdpSessionResponse_TypeID is the unique identifier for the type RegisterUdpSessionResponse.
const RegisterUdpSessionResponse_TypeID = 0xab6d5210c1f26687
func NewRegisterUdpSessionResponse(s *capnp.Segment) (RegisterUdpSessionResponse, error) {
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1})
return RegisterUdpSessionResponse{st}, err
}
func NewRootRegisterUdpSessionResponse(s *capnp.Segment) (RegisterUdpSessionResponse, error) {
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1})
return RegisterUdpSessionResponse{st}, err
}
func ReadRootRegisterUdpSessionResponse(msg *capnp.Message) (RegisterUdpSessionResponse, error) {
root, err := msg.RootPtr()
return RegisterUdpSessionResponse{root.Struct()}, err
}
func (s RegisterUdpSessionResponse) String() string {
str, _ := text.Marshal(0xab6d5210c1f26687, s.Struct)
return str
}
func (s RegisterUdpSessionResponse) Err() (string, error) {
p, err := s.Struct.Ptr(0)
return p.Text(), err
}
func (s RegisterUdpSessionResponse) HasErr() bool {
p, err := s.Struct.Ptr(0)
return p.IsValid() || err != nil
}
func (s RegisterUdpSessionResponse) ErrBytes() ([]byte, error) {
p, err := s.Struct.Ptr(0)
return p.TextBytes(), err
}
func (s RegisterUdpSessionResponse) SetErr(v string) error {
return s.Struct.SetText(0, v)
}
// RegisterUdpSessionResponse_List is a list of RegisterUdpSessionResponse.
type RegisterUdpSessionResponse_List struct{ capnp.List }
// NewRegisterUdpSessionResponse creates a new list of RegisterUdpSessionResponse.
func NewRegisterUdpSessionResponse_List(s *capnp.Segment, sz int32) (RegisterUdpSessionResponse_List, error) {
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz)
return RegisterUdpSessionResponse_List{l}, err
}
func (s RegisterUdpSessionResponse_List) At(i int) RegisterUdpSessionResponse {
return RegisterUdpSessionResponse{s.List.Struct(i)}
}
func (s RegisterUdpSessionResponse_List) Set(i int, v RegisterUdpSessionResponse) error {
return s.List.SetStruct(i, v.Struct)
}
func (s RegisterUdpSessionResponse_List) String() string {
str, _ := text.MarshalList(0xab6d5210c1f26687, s.List)
return str
}
// RegisterUdpSessionResponse_Promise is a wrapper for a RegisterUdpSessionResponse promised by a client call.
type RegisterUdpSessionResponse_Promise struct{ *capnp.Pipeline }
func (p RegisterUdpSessionResponse_Promise) Struct() (RegisterUdpSessionResponse, error) {
s, err := p.Pipeline.Struct()
return RegisterUdpSessionResponse{s}, err
}
type SessionManager struct{ Client capnp.Client }
// SessionManager_TypeID is the unique identifier for the type SessionManager.
const SessionManager_TypeID = 0x839445a59fb01686
func (c SessionManager) RegisterUdpSession(ctx context.Context, params func(SessionManager_registerUdpSession_Params) error, opts ...capnp.CallOption) SessionManager_registerUdpSession_Results_Promise {
if c.Client == nil {
return SessionManager_registerUdpSession_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))}
}
call := &capnp.Call{
Ctx: ctx,
Method: capnp.Method{
InterfaceID: 0x839445a59fb01686,
MethodID: 0,
InterfaceName: "tunnelrpc/tunnelrpc.capnp:SessionManager",
MethodName: "registerUdpSession",
},
Options: capnp.NewCallOptions(opts),
}
if params != nil {
call.ParamsSize = capnp.ObjectSize{DataSize: 16, PointerCount: 2}
call.ParamsFunc = func(s capnp.Struct) error { return params(SessionManager_registerUdpSession_Params{Struct: s}) }
}
return SessionManager_registerUdpSession_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))}
}
func (c SessionManager) UnregisterUdpSession(ctx context.Context, params func(SessionManager_unregisterUdpSession_Params) error, opts ...capnp.CallOption) SessionManager_unregisterUdpSession_Results_Promise {
if c.Client == nil {
return SessionManager_unregisterUdpSession_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))}
}
call := &capnp.Call{
Ctx: ctx,
Method: capnp.Method{
InterfaceID: 0x839445a59fb01686,
MethodID: 1,
InterfaceName: "tunnelrpc/tunnelrpc.capnp:SessionManager",
MethodName: "unregisterUdpSession",
},
Options: capnp.NewCallOptions(opts),
}
if params != nil {
call.ParamsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 1}
call.ParamsFunc = func(s capnp.Struct) error { return params(SessionManager_unregisterUdpSession_Params{Struct: s}) }
}
return SessionManager_unregisterUdpSession_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))}
}
type SessionManager_Server interface {
RegisterUdpSession(SessionManager_registerUdpSession) error
UnregisterUdpSession(SessionManager_unregisterUdpSession) error
}
func SessionManager_ServerToClient(s SessionManager_Server) SessionManager {
c, _ := s.(server.Closer)
return SessionManager{Client: server.New(SessionManager_Methods(nil, s), c)}
}
func SessionManager_Methods(methods []server.Method, s SessionManager_Server) []server.Method {
if cap(methods) == 0 {
methods = make([]server.Method, 0, 2)
}
methods = append(methods, server.Method{
Method: capnp.Method{
InterfaceID: 0x839445a59fb01686,
MethodID: 0,
InterfaceName: "tunnelrpc/tunnelrpc.capnp:SessionManager",
MethodName: "registerUdpSession",
},
Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error {
call := SessionManager_registerUdpSession{c, opts, SessionManager_registerUdpSession_Params{Struct: p}, SessionManager_registerUdpSession_Results{Struct: r}}
return s.RegisterUdpSession(call)
},
ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 1},
})
methods = append(methods, server.Method{
Method: capnp.Method{
InterfaceID: 0x839445a59fb01686,
MethodID: 1,
InterfaceName: "tunnelrpc/tunnelrpc.capnp:SessionManager",
MethodName: "unregisterUdpSession",
},
Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error {
call := SessionManager_unregisterUdpSession{c, opts, SessionManager_unregisterUdpSession_Params{Struct: p}, SessionManager_unregisterUdpSession_Results{Struct: r}}
return s.UnregisterUdpSession(call)
},
ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 0},
})
return methods
}
// SessionManager_registerUdpSession holds the arguments for a server call to SessionManager.registerUdpSession.
type SessionManager_registerUdpSession struct {
Ctx context.Context
Options capnp.CallOptions
Params SessionManager_registerUdpSession_Params
Results SessionManager_registerUdpSession_Results
}
// SessionManager_unregisterUdpSession holds the arguments for a server call to SessionManager.unregisterUdpSession.
type SessionManager_unregisterUdpSession struct {
Ctx context.Context
Options capnp.CallOptions
Params SessionManager_unregisterUdpSession_Params
Results SessionManager_unregisterUdpSession_Results
}
type SessionManager_registerUdpSession_Params struct{ capnp.Struct }
// SessionManager_registerUdpSession_Params_TypeID is the unique identifier for the type SessionManager_registerUdpSession_Params.
const SessionManager_registerUdpSession_Params_TypeID = 0x904e297b87fbecea
func NewSessionManager_registerUdpSession_Params(s *capnp.Segment) (SessionManager_registerUdpSession_Params, error) {
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 16, PointerCount: 2})
return SessionManager_registerUdpSession_Params{st}, err
}
func NewRootSessionManager_registerUdpSession_Params(s *capnp.Segment) (SessionManager_registerUdpSession_Params, error) {
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 16, PointerCount: 2})
return SessionManager_registerUdpSession_Params{st}, err
}
func ReadRootSessionManager_registerUdpSession_Params(msg *capnp.Message) (SessionManager_registerUdpSession_Params, error) {
root, err := msg.RootPtr()
return SessionManager_registerUdpSession_Params{root.Struct()}, err
}
func (s SessionManager_registerUdpSession_Params) String() string {
str, _ := text.Marshal(0x904e297b87fbecea, s.Struct)
return str
}
func (s SessionManager_registerUdpSession_Params) SessionId() ([]byte, error) {
p, err := s.Struct.Ptr(0)
return []byte(p.Data()), err
}
func (s SessionManager_registerUdpSession_Params) HasSessionId() bool {
p, err := s.Struct.Ptr(0)
return p.IsValid() || err != nil
}
func (s SessionManager_registerUdpSession_Params) SetSessionId(v []byte) error {
return s.Struct.SetData(0, v)
}
func (s SessionManager_registerUdpSession_Params) DstIp() ([]byte, error) {
p, err := s.Struct.Ptr(1)
return []byte(p.Data()), err
}
func (s SessionManager_registerUdpSession_Params) HasDstIp() bool {
p, err := s.Struct.Ptr(1)
return p.IsValid() || err != nil
}
func (s SessionManager_registerUdpSession_Params) SetDstIp(v []byte) error {
return s.Struct.SetData(1, v)
}
func (s SessionManager_registerUdpSession_Params) DstPort() uint16 {
return s.Struct.Uint16(0)
}
func (s SessionManager_registerUdpSession_Params) SetDstPort(v uint16) {
s.Struct.SetUint16(0, v)
}
func (s SessionManager_registerUdpSession_Params) CloseAfterIdleHint() int64 {
return int64(s.Struct.Uint64(8))
}
func (s SessionManager_registerUdpSession_Params) SetCloseAfterIdleHint(v int64) {
s.Struct.SetUint64(8, uint64(v))
}
// SessionManager_registerUdpSession_Params_List is a list of SessionManager_registerUdpSession_Params.
type SessionManager_registerUdpSession_Params_List struct{ capnp.List }
// NewSessionManager_registerUdpSession_Params creates a new list of SessionManager_registerUdpSession_Params.
func NewSessionManager_registerUdpSession_Params_List(s *capnp.Segment, sz int32) (SessionManager_registerUdpSession_Params_List, error) {
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 16, PointerCount: 2}, sz)
return SessionManager_registerUdpSession_Params_List{l}, err
}
func (s SessionManager_registerUdpSession_Params_List) At(i int) SessionManager_registerUdpSession_Params {
return SessionManager_registerUdpSession_Params{s.List.Struct(i)}
}
func (s SessionManager_registerUdpSession_Params_List) Set(i int, v SessionManager_registerUdpSession_Params) error {
return s.List.SetStruct(i, v.Struct)
}
func (s SessionManager_registerUdpSession_Params_List) String() string {
str, _ := text.MarshalList(0x904e297b87fbecea, s.List)
return str
}
// SessionManager_registerUdpSession_Params_Promise is a wrapper for a SessionManager_registerUdpSession_Params promised by a client call.
type SessionManager_registerUdpSession_Params_Promise struct{ *capnp.Pipeline }
func (p SessionManager_registerUdpSession_Params_Promise) Struct() (SessionManager_registerUdpSession_Params, error) {
s, err := p.Pipeline.Struct()
return SessionManager_registerUdpSession_Params{s}, err
}
type SessionManager_registerUdpSession_Results struct{ capnp.Struct }
// SessionManager_registerUdpSession_Results_TypeID is the unique identifier for the type SessionManager_registerUdpSession_Results.
const SessionManager_registerUdpSession_Results_TypeID = 0x8635c6b4f45bf5cd
func NewSessionManager_registerUdpSession_Results(s *capnp.Segment) (SessionManager_registerUdpSession_Results, error) {
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1})
return SessionManager_registerUdpSession_Results{st}, err
}
func NewRootSessionManager_registerUdpSession_Results(s *capnp.Segment) (SessionManager_registerUdpSession_Results, error) {
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1})
return SessionManager_registerUdpSession_Results{st}, err
}
func ReadRootSessionManager_registerUdpSession_Results(msg *capnp.Message) (SessionManager_registerUdpSession_Results, error) {
root, err := msg.RootPtr()
return SessionManager_registerUdpSession_Results{root.Struct()}, err
}
func (s SessionManager_registerUdpSession_Results) String() string {
str, _ := text.Marshal(0x8635c6b4f45bf5cd, s.Struct)
return str
}
func (s SessionManager_registerUdpSession_Results) Result() (RegisterUdpSessionResponse, error) {
p, err := s.Struct.Ptr(0)
return RegisterUdpSessionResponse{Struct: p.Struct()}, err
}
func (s SessionManager_registerUdpSession_Results) HasResult() bool {
p, err := s.Struct.Ptr(0)
return p.IsValid() || err != nil
}
func (s SessionManager_registerUdpSession_Results) SetResult(v RegisterUdpSessionResponse) error {
return s.Struct.SetPtr(0, v.Struct.ToPtr())
}
// NewResult sets the result field to a newly
// allocated RegisterUdpSessionResponse struct, preferring placement in s's segment.
func (s SessionManager_registerUdpSession_Results) NewResult() (RegisterUdpSessionResponse, error) {
ss, err := NewRegisterUdpSessionResponse(s.Struct.Segment())
if err != nil {
return RegisterUdpSessionResponse{}, err
}
err = s.Struct.SetPtr(0, ss.Struct.ToPtr())
return ss, err
}
// SessionManager_registerUdpSession_Results_List is a list of SessionManager_registerUdpSession_Results.
type SessionManager_registerUdpSession_Results_List struct{ capnp.List }
// NewSessionManager_registerUdpSession_Results creates a new list of SessionManager_registerUdpSession_Results.
func NewSessionManager_registerUdpSession_Results_List(s *capnp.Segment, sz int32) (SessionManager_registerUdpSession_Results_List, error) {
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz)
return SessionManager_registerUdpSession_Results_List{l}, err
}
func (s SessionManager_registerUdpSession_Results_List) At(i int) SessionManager_registerUdpSession_Results {
return SessionManager_registerUdpSession_Results{s.List.Struct(i)}
}
func (s SessionManager_registerUdpSession_Results_List) Set(i int, v SessionManager_registerUdpSession_Results) error {
return s.List.SetStruct(i, v.Struct)
}
func (s SessionManager_registerUdpSession_Results_List) String() string {
str, _ := text.MarshalList(0x8635c6b4f45bf5cd, s.List)
return str
}
// SessionManager_registerUdpSession_Results_Promise is a wrapper for a SessionManager_registerUdpSession_Results promised by a client call.
type SessionManager_registerUdpSession_Results_Promise struct{ *capnp.Pipeline }
func (p SessionManager_registerUdpSession_Results_Promise) Struct() (SessionManager_registerUdpSession_Results, error) {
s, err := p.Pipeline.Struct()
return SessionManager_registerUdpSession_Results{s}, err
}
func (p SessionManager_registerUdpSession_Results_Promise) Result() RegisterUdpSessionResponse_Promise {
return RegisterUdpSessionResponse_Promise{Pipeline: p.Pipeline.GetPipeline(0)}
}
type SessionManager_unregisterUdpSession_Params struct{ capnp.Struct }
// SessionManager_unregisterUdpSession_Params_TypeID is the unique identifier for the type SessionManager_unregisterUdpSession_Params.
const SessionManager_unregisterUdpSession_Params_TypeID = 0x96b74375ce9b0ef6
func NewSessionManager_unregisterUdpSession_Params(s *capnp.Segment) (SessionManager_unregisterUdpSession_Params, error) {
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1})
return SessionManager_unregisterUdpSession_Params{st}, err
}
func NewRootSessionManager_unregisterUdpSession_Params(s *capnp.Segment) (SessionManager_unregisterUdpSession_Params, error) {
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1})
return SessionManager_unregisterUdpSession_Params{st}, err
}
func ReadRootSessionManager_unregisterUdpSession_Params(msg *capnp.Message) (SessionManager_unregisterUdpSession_Params, error) {
root, err := msg.RootPtr()
return SessionManager_unregisterUdpSession_Params{root.Struct()}, err
}
func (s SessionManager_unregisterUdpSession_Params) String() string {
str, _ := text.Marshal(0x96b74375ce9b0ef6, s.Struct)
return str
}
func (s SessionManager_unregisterUdpSession_Params) SessionId() ([]byte, error) {
p, err := s.Struct.Ptr(0)
return []byte(p.Data()), err
}
func (s SessionManager_unregisterUdpSession_Params) HasSessionId() bool {
p, err := s.Struct.Ptr(0)
return p.IsValid() || err != nil
}
func (s SessionManager_unregisterUdpSession_Params) SetSessionId(v []byte) error {
return s.Struct.SetData(0, v)
}
// SessionManager_unregisterUdpSession_Params_List is a list of SessionManager_unregisterUdpSession_Params.
type SessionManager_unregisterUdpSession_Params_List struct{ capnp.List }
// NewSessionManager_unregisterUdpSession_Params creates a new list of SessionManager_unregisterUdpSession_Params.
func NewSessionManager_unregisterUdpSession_Params_List(s *capnp.Segment, sz int32) (SessionManager_unregisterUdpSession_Params_List, error) {
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz)
return SessionManager_unregisterUdpSession_Params_List{l}, err
}
func (s SessionManager_unregisterUdpSession_Params_List) At(i int) SessionManager_unregisterUdpSession_Params {
return SessionManager_unregisterUdpSession_Params{s.List.Struct(i)}
}
func (s SessionManager_unregisterUdpSession_Params_List) Set(i int, v SessionManager_unregisterUdpSession_Params) error {
return s.List.SetStruct(i, v.Struct)
}
func (s SessionManager_unregisterUdpSession_Params_List) String() string {
str, _ := text.MarshalList(0x96b74375ce9b0ef6, s.List)
return str
}
// SessionManager_unregisterUdpSession_Params_Promise is a wrapper for a SessionManager_unregisterUdpSession_Params promised by a client call.
type SessionManager_unregisterUdpSession_Params_Promise struct{ *capnp.Pipeline }
func (p SessionManager_unregisterUdpSession_Params_Promise) Struct() (SessionManager_unregisterUdpSession_Params, error) {
s, err := p.Pipeline.Struct()
return SessionManager_unregisterUdpSession_Params{s}, err
}
type SessionManager_unregisterUdpSession_Results struct{ capnp.Struct }
// SessionManager_unregisterUdpSession_Results_TypeID is the unique identifier for the type SessionManager_unregisterUdpSession_Results.
const SessionManager_unregisterUdpSession_Results_TypeID = 0xf24ec4ab5891b676
func NewSessionManager_unregisterUdpSession_Results(s *capnp.Segment) (SessionManager_unregisterUdpSession_Results, error) {
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0})
return SessionManager_unregisterUdpSession_Results{st}, err
}
func NewRootSessionManager_unregisterUdpSession_Results(s *capnp.Segment) (SessionManager_unregisterUdpSession_Results, error) {
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0})
return SessionManager_unregisterUdpSession_Results{st}, err
}
func ReadRootSessionManager_unregisterUdpSession_Results(msg *capnp.Message) (SessionManager_unregisterUdpSession_Results, error) {
root, err := msg.RootPtr()
return SessionManager_unregisterUdpSession_Results{root.Struct()}, err
}
func (s SessionManager_unregisterUdpSession_Results) String() string {
str, _ := text.Marshal(0xf24ec4ab5891b676, s.Struct)
return str
}
// SessionManager_unregisterUdpSession_Results_List is a list of SessionManager_unregisterUdpSession_Results.
type SessionManager_unregisterUdpSession_Results_List struct{ capnp.List }
// NewSessionManager_unregisterUdpSession_Results creates a new list of SessionManager_unregisterUdpSession_Results.
func NewSessionManager_unregisterUdpSession_Results_List(s *capnp.Segment, sz int32) (SessionManager_unregisterUdpSession_Results_List, error) {
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}, sz)
return SessionManager_unregisterUdpSession_Results_List{l}, err
}
func (s SessionManager_unregisterUdpSession_Results_List) At(i int) SessionManager_unregisterUdpSession_Results {
return SessionManager_unregisterUdpSession_Results{s.List.Struct(i)}
}
func (s SessionManager_unregisterUdpSession_Results_List) Set(i int, v SessionManager_unregisterUdpSession_Results) error {
return s.List.SetStruct(i, v.Struct)
}
func (s SessionManager_unregisterUdpSession_Results_List) String() string {
str, _ := text.MarshalList(0xf24ec4ab5891b676, s.List)
return str
}
// SessionManager_unregisterUdpSession_Results_Promise is a wrapper for a SessionManager_unregisterUdpSession_Results promised by a client call.
type SessionManager_unregisterUdpSession_Results_Promise struct{ *capnp.Pipeline }
func (p SessionManager_unregisterUdpSession_Results_Promise) Struct() (SessionManager_unregisterUdpSession_Results, error) {
s, err := p.Pipeline.Struct()
return SessionManager_unregisterUdpSession_Results{s}, err
}
const schema_db8274f9144abc7e = "x\xda\xccY{p\x14e\xb6?\xa7{&\x9d@\x86" +
"IW\x0f\x04\xa6\xe4\xe6^\x0a\xcaK\x14\x14\xb8\xdeB" +
"\xae\xde\x04L\xb8&\xf2H\xcf\x90\xbb\x96\xa0eg\xe6" +
"#tv\xa6{\xe8\xee\x89\x04A\x1e\x82\x88\xe5\x0b\x04" +
"E\x94\x95\xc5r\xb7D\xdd\x85U\xd7eKke\xd7" +
"\x17\xa5\xa8X\xb8\x05\x8a\xb5\xab\xc8>(\\W\xc4\xb5" +
"\xdcu\xed\xad\xd3=\xfd\xc8$$A\xf6\x8f\xfdor" +
"\xfa\xfb\xcew\xce\xef\xfc\xbes\xcewr\xe9w*\x1b" +
"\xb9)\xd1\xda\x18\x80\xbc-Za\xb3\xfaw\x96\xef\x9c" +
"\xf0\xab\xb5 '\x11\xed[\x9eoM|e\xad}\x1f" +
"\xa2\xbc\x000mi\xc5r\x94\xd6W\x08\x00\xd2\x9a\x8a" +
"\xdf\x03\xda\xb7\x8d\xda\xf3\xc8c\xcd[n\x051\xc9\x07" +
"\x8b\x01\xa71\xa1\x15\xa5\x1e\x81V\x16\x85\x0d\xd2\xbb\xf4" +
"\xcb\xbeF\xbcda\xe2\xed7iuXu\x84T\xbf" +
" \xd4\xa3t\xd0\xd9p@ \xd5W\xe4\xdf\xda\xf5\xdf" +
"[__\x07b\x92\xeb\xa5\xfa\xd9\xca\xe5(\x1d\xa8\xa4" +
"\x95/W\xce\x07\xb4?\xdf2\xfa\x89\xef\xbf\xf9\xdaz" +
"\x10/D(Y\xfaA\xe5{\x08(}V\xf9c@" +
"\xfb\xe0\x17\x0b\xcf<\xf3\xcae\xb7\x818\x91\x16 -" +
"\xd8T5\x8e\x03\x94\x1e\xafj\x00\xb4O\x9e\xfa\xdb\x86" +
"\x9b'\xce\xbb\x17\xe4\x89\xc8\x01D9Zq\xa0*I" +
"+>\xac\"k\x1af\x1f\xdc\x97\x9cv\xff\x962\xd3" +
"\x9d\x85\xfb\x87\xd5\xa3th\x18\x19tp\xd8M\x80\xf6" +
"_F<\xf4f\xf1\xaa\xe7\xee\x0f\x9f7ex=i" +
"k\x19N\xe7\x8d\xeb\x9ep\xe3/_~\xfa\x01\x90'" +
"!\xda\xc7:.z\x97\xdf\xb1\xfb}hG\x81\x8e\x9f" +
"\x96\x1f\xbe\x8b\x8c_9\x9c\x94\xbdu\xf1\xf3?\xbf\xf7" +
"\xe9\x0d\x0f\x81|!\"\x80\x03\xd6\x87\xc3\xffJ\x0b\xbe" +
"p\x94m9\xf2\xc2\xbc\xfc\xa6\xed\xbb\\\xf7\x9d\xefc" +
"\xab9\x0e\"\xf6\xba\x96/\xf3\xed\x8f\xa6\x1f-\x01\x13" +
"\xa5Ob\xf5i\x04\x9c6\xa1\xba\x0e\x01\xed\xcb\xde;" +
"1\x7f\xeeO\x16\xff0\xb4wfl9\xed\xdd\xb0\xf8" +
"\xf4\xfe\x9aT\xfe\x892\x87\x1d_\xae\x8c\xedF\xa9=" +
"F\x0e\xcb12\xe1\xa9\x7f\xbb\xa6j\xd9\x89\xd9{@" +
"\x9c\xe4\xa9Y\x1aK\x91\x9a\xc8\xf5\xfc7\xca\xb6_<" +
"SN'\x07\xb8|\xac\x03\xa55\xa4g\xda\xca\x98c" +
"\xcf\x1d\xfb\xb7_T\xf9\xc8\xe7\xcf\xf6\x07\xf3c#:" +
"P\xda7\x82N}v\x04!3\xb2\x05\x8f\xbd8%" +
"\xf2\\8\xee#\xe3'\x09\x99\x89q\x8a\xfb\xd8Of" +
"\xc5\xb4O\xd7\xbeX\xa6\xcdYx \xde\x8a\xd2\x07q" +
"\xd2v\xd4Y\xdc\xba\xf0\xbe\xcd\xd1\x13\xf7\xbdJ\x96\x86" +
"\x08\x17%\xa2M+\xd6\x18(m\xac\xa1\x9f\xebkj" +
"y@;\xb9\xe7\x7f~4+{\xf4\xf5~,\x95\xa2" +
"\x89\xd3\x92\x98\xa0_\xb1\x04\x19z|\xd2\xde\x9b\xffx" +
"\xd7\xa1\xc3%C\x1d\x0c\xd5\x84\x13\xc2\x95\x09\xc2\xcfg" +
"@\x19J\xce\xca\x1d\x89.\x94\xf6:\xea\x9erVs" +
"'\x941\xab\x7f\xfd\xbf\xc7BA\xdb\x9b\xf8\x08!b" +
"\xcf\xfb\xff\x85]U+\x8f\x1f\x0f\x1f\xf4X\xc2Ad" +
"\x9f\xb3\xf5O?8y\xcf\xa9|\xf6w\x0e\xf1<\xcc" +
"\x8e&f\x105?K\x10\xd1k\xebb\xcd\xe3\x8e\xb4" +
"\x9dtC\xe9\xaa88r\x16-81\x92T\\v" +
"\xe3L\xb6h\xfa\xb5'\xfb\\\xf9\xe8\xa8\x19(\x8d\x1c" +
"\xe5\x90l\xd4\x06\x94&\xd6\xd6\x02\xd8\xdd?\xddt\xed" +
"\x13/\xcd;\xed\xde\x05\xc7\xd81\xb5S\x89\x1aw\xdf" +
"\xd24\xff\xf2q\xfbO\x87\x8d\x15k\x89\x9d\xd2\x84Z" +
":i\xf1\xf4S\xff7\xe1\xeeWN\xf7G\xc1\xe6\xda" +
"z\x94\xdak\x1d\x0a\xd2\xe2Og\x7f\xefp2\x9e<" +
"S\x06`\x85\x13\xbc\xda.\x946\xd6:\xc1\xab}\x95" +
"hv\xdb\xfb7,{\xe7\xd6\xcf\xbf(\x8f\xb5\xa3\xba" +
"gL\x0a\xa5\xbb\xc6\x90\xea\x8dc\x88\x19\x0f,\xf8\xc3" +
"\xaaS[G}\xd9\xc7\xe3\x89\xc9.\x94\xaeL\xd2\xca" +
"\xcb\x93\x1b\xa4\x07\xe9\x97\xfd\xb6\xf0\xe8\x94\xa6U\xaf\x7f" +
"\x15\xba\x0bk\x92\xad\xe4\xf0\xfd\xc2\xc3\xc7W\xff\xe6\x86" +
"\xaf\xc3\x0e\xafL~D\x0eoJ\x92\xc3+>}\xf0" +
"\xea{\x16=\xf9M8\xb0\xc9\xb5\xb4\xd5*j\x1a\xcb" +
"\x19\x85H\xe6\x12\xefgfrF)h\x85\x193\x8b" +
"\xd6\x12\xa6YjF\xb1X\x8a5\x98\x05]3Y\x1b" +
"\xa2\\\xc3G\x00\"\x08 *]\x00\xf2\x8d<\xca9" +
"\x0eE\xc4\x04\x85^TI\xb8\x84G\xd9\xe2P\xe4\xb8" +
"\x04e\x1eq\xe98\x009\xc7\xa3\xbc\x8cC\xe4\x13\xc8" +
"\x03\x88\xc5\xcd\x00\xf22\x1e\xe5u\x1c\xda\x05f\xe4\x15" +
"\x8di\x10\xb7\x9a\x0d\x03\xab\x81\xc3j@\xdb`\x96\xd1" +
"\xa3t\xe4 \xceBb\xa1\xeb&\x0bc\xc0a\x0c\xd0" +
"^\xa2\x17\x0d\xb3]\xb3P\xcd\xa5\xd8b\x83\x99\xb8\x04" +
"+\x80\xc3\x0a\xc0\x81\xdcK3\xd3Tum\xae\xa2)" +
"\x9d\xcc\x00 \xcf*\xf9(\x80\x9f\xb4\xd1K\xef\xe2\x94" +
"\xed\xc0\x89\x93\x04\x0c20z\xf4\x13\xffc7p\xe2" +
"X\xc16X\xa7jZ\xcc\xc0\xf6l\xc1\xd1\xcd\xebZ" +
"#\xdaE\xcd\xfd\x80\xccp?\xc4\xe9\xd4Fl\xc3\xc0" +
":\xbe\xafuW\xe5T\xa6Y\xf1\x16m\xb1^\x06y" +
"k\x7f\x90\xb7\x96 _\x17\x82|\xcd,\x00y\x05\x8f" +
"\xf2\xed\x1c\x8a|\x09\xf3\xf5\xf5\x00\xf2j\x1e\xe5;9" +
"\xb43\xce!-Y\x00\xf0\xd1\\\xcc\x14\xabh0\x93" +
"d#\x00\xdbxt@\x1f\x01\xb8\xaa\x9b\x19d\xbb\x17" +
"\x84\xb8bd\x96\xf8\x81\x1a\x00\xe9\xe6e\xaai\xa9Z" +
"\xe7\x02G\xde\xd0\xa6\xe7\xd4L\x0fyU\xed\xd89v" +
"\x06\x00\xa28\xf2:\x00\xe4Dq\x16@\x83\xda\xa9\xe9" +
"\x06\xb3\xb3\xaa\x99\xd15\x8d\x01\x9f\xb1Vu(9E" +
"\xcb0\xff\xa0\x8a\xbe\x07\xb9\x07\xa4\x99\xd1\xcd\x8c\xc9J" +
"\x88\xbe\xe3\xdb\x14C\xe1\xf3\xa6\\\xed\xe3\xd8|\x1d\x80" +
"\xdc\xc4\xa3\xdc\x16\xc2q.\xe18\x87G\xf9\xda\x10\x8e" +
"\xed\x84c\x1b\x8f\xf2\"\x0em\xddP;U\xed*\x06" +
"\xbc\x11f\xa0iiJ\x9e\x11f%<V\xe9\x05K" +
"\xd55\x13k\x82\xfc\x0f\x885!\xa4\x84\xc189\xd9" +
"\xa3\x94\xc7(]\x1b\x9fbfQ\xc8Y\xa6\x1c\xf1=" +
"\x89\xcd\x00\x90+y\x94\x13\x1c6\x18\xcc,\xe6,\xac" +
"\x09\xca\xec?\xe3T\x0f\xbe\x10\x0dS\xfd\xd1p*\x80" +
"\x9c\xe5Q.p\x88%\xf4\xf2\xb3B\xd9\x80G\x97\x85" +
"K\xb7\x03\xc8\x16\x8f\xf2j\x0em\xd3=\xa4\x050\xeb" +
"!Z\x975\xad\x96\x82\xf7\xd7\xaa\xaci\xb5\xe9\x86\x85" +
"\x02p(\x00\xf1V7\xd9\xcc\xc5t\xa7Z\xb29v" +
"\xb5\xcak\x16F\x81\xc3(\x0cx\xa9\\~\xc4)\xb1" +
"\xb9\xb7\xdd\xf3f\"\x91\xe1?y\x94\xff+\xe4\xcd\x14" +
"\xcac\x97\xf2(_\xc1\xa1\xadd2zQ\xb3\x16\x00" +
"\xaft\x96q>\xcd \x9e1X@\x87\xa1C\xed%" +
"\x872\xb0\xe3\x86\x92\xef\x15a\x02\xbb\x9aGyt\xff" +
"p\xf9'F\xfbI$t\x812\xc4\xc5\x14sS\xf8" +
"d\x83\x99B1g\x91\xff\xd5\xb6\xed\x02@\x91\x1b\xcf" +
"\xa3|)\x871\xfc\xc6v\x11\x98\xb49@\xa0\x8e\x19" +
"\x86n`MP\xe2J\xc4\xca\x94\x0e@]kb\x96" +
"\xa2\xe6\x90H\xef\xf7[e\xf4\x1b\xec\xd6\x06\x88\xb8\xe2" +
"\xf1\x0d\xc4\xbd\xdeh\x10yjx\x94/\xe0\xd0\xee4" +
"\x94\x0ckc\x06\xaazv\x9e\xa2\xe9i\x9ee\xfaP" +
"a\xc4\xb9\x1e\x9arn\x91\x09\xfe\xae\x81\xf7\x1b\xac\x04" +
"Bi{[\x9dks\xc2\xb7y\xe5\xb8\xa0\xd4\xf9\x04" +
"[\xd3\x11\xe4b?\xdbl$*\xde\xce\xa3\xbc%\x94" +
"\xb57Q^\xba\x97G\xf9a\x0e\xc5H$\x81\x11\x00" +
"\xf1A\xbaY[x\x94wr\xbd\x0b\"\xebf\x9a\xd5" +
"\xa4v\x82\xc0\xcc@J&6\xa9\x9d\x0cx\xf3|3" +
"W\xe5 x\xe8\x1d\xa6\x9ec\x16kb\x99\x9cb(" +
"\x96\xda\xcd\xdc\xef%2zA\x1d\x88\xb7\xa9>\x17\x83" +
"\xf8\x1b\xf7z\x90\x10\x1d\xc6\x05\xe9O`\xa1\xd6a\x00" +
"k]\xe5d\x99\xae\xf5\xe1@pcJ<@s\xa0" +
"\xea\x16,\x9f_\xb0TA\xd7L\xb2/\x14\xfa\x19\xfd" +
"\x85\xde\x08B\xefe\xca\x8dk\xc3\x91/e\xcaM\xdb" +
"\x83 \x8b\x11\xce\x8d\xfc\x8e]\x00\xf2N\x1e\xe5'9" +
"lp\x8b8\xd6\x04\x8f\xe0R\xb4\xdcR5G\x87\xba" +
"\x8c\x92\x0b\xb2\xa9m\xb0BN\xc9\xb0f,\x95e@" +
"\x04\x0e\xd1\xa1H\xbe`0\xd3DU\xd7\xe4\xa2\x92S" +
"y\xab\xc7o\xa5\xb4b\xbe\xcd`\xdd*\xeaEs\xa6" +
"e\xb1\xbcP\xb0\xcc\xa14Z\x01@\x94\x1f\x045g" +
"\x96%\xdf\xfa \xf7\xf8\x00M\xa2\xe4{1\x8f\xf2t" +
"\x0e\xe3\xc5\xa2\x1a\xe4\xba\x9c\x9eq\xe2\x06\xf1yJ\x9e" +
"\xf5\x89v\xc5\xa0w\xb5\xd7M\xf7\x92\xed\xbfRc0" +
"p/N\xae;\xcdj\xc8d\xba\x02\x8d<\xcasB" +
"&\xb7L\x0d\xf9\xe1\x99<\xb7#\xf0C\xf8.\xeb\xf1" +
"\xac\xaacy\xca\xdc\x1e\x98%gf\x82pM\xb0f" +
" \xfb\xc2\x17j~\xa1\xce\xf1\x90l\x9c\xee\xd9(\xf5" +
"`+@z\x19\xf2\x98^\x87\x81\x99\xd2\x1a\x9c\x05\x90" +
"^A\xf2\xdb1\xb0TZ\x8fI\x80\xf4j\x92\xdf\x89" +
"\xfe\x9bA\xda\x88\xbb\x01\xd2w\x92x\x1b-\x8f\xf0\xce" +
"\x95\x90\xb6:\xea\xb7\x90|'\xc9\xa3\x91\x04F\x01\xa4" +
"\x1dX\x0f\x90\xdeF\xf2gH^\xc1%\xb0\x02@\xda" +
"\x8b]\x00\xe9=$\x7f\x9e\xe4B4A\xcf&i\x1f" +
"\x1a\x00\xe9\x9f\x91\xfc%\x92W\x8eN`%\x80\xb4\xdf" +
"\x91\xbfH\xf27H^5&\x81U\x00\xd2\x01\\\x0b" +
"\x90~\x8d\xe4\x87I>\x0c\x138\x0c@:\x84\xdb\x01" +
"\xd2\x87I\xfe[\x92\x0f\xafH\xe0p\x00\xe9\x03\xc7\x9e" +
"#$\xff\x98\xe4\xd5\x91\x04V\x03H\x1f\xe2.\x80\xf4" +
"\xc7$\xff3\xc9cB\x02c\x00\xd2'\x8e_\xa7H" +
"^\xc9\x95\xb5\xec\x1e\xa3\xca\xfar^7\xfd\x90\xb1\xd2" +
"\x1dG\x97\xeemz\x9czo\x8c\x07C0@\x8c\x03" +
"\xda\x05]\xcf\xcd\xeb\xcd\xd4\xb8\xa5t\x9a\xde\x1b\xa0&" +
"\x98K\x00\x92\xd0\xaf\xfb\x10\xd7\xb5\x96\xac\x9f\x08\xca\xb3" +
"\x8eg\x89j\xce,Zz\xb1\x00uY\xc5bY?" +
"\xe7\x18Em\xb6\xa1\xe7\x17 3\xf2\xaa\xa6\xe4\x06\xc9" +
"FU\xc0a\x15\x94R\x82\xa7{\xe0\xd4t\xf6\x17\x8d" +
"\xcfh\xae\x9c\xd1u\x85\x19\x0b\x94\xce\xa1\xe4\xa9\xa9A" +
"\xe7\x18\xd7B\x09\xa9\xae[\xc9\x15\xbfMz\xea\xddJ" +
"\xa4\x1a\xdcVd\xb0~\xdf\x1bS\x0c\x9eJz7\x84" +
"\xbd\x0b*\x86&\x88t\x0eW\xd2?d\xf3;\x99\xe5" +
"\xfe\xa2\x87+=\x1b\x84p\x99?\xb7\xdd)f\xc6\x87" +
"\xe2z0\xce\x19\xfc\xa9\xd3O\xe1\xef\xa7\xec{=g" +
"\xe8\xb9C\xb1_\xc4\xa3\xbc$\x14{\xd6\xda\xcfs'" +
"\x15\xcc9D\x9e+\x0d:\xa8P\x14x\x94Wp\x18" +
"\xa7w)\xd6\x04s\xdf^F\xf7~\x8b\x13\x15Z\xb4" +
",\x03\\\xe6\xb19T>\xfc\x09\xe8\xe0\xdd\xd9\xd0\xdc" +
"\xf6\xba\xdeA\x01\xf7\xa7\x8ae'\x9f\xf5\xc9\xd5\xe0\x1e" +
"J<\x1b\xed\x8cX\xbc\x09+z\xb3:q\xefr\xe0" +
"\xc4\xc7\x05\x0c\xa6\x90\xe8\x0d\x1d\xc5\x1d\x06p\xe2V\x01" +
"9\x7ff\x8d\xdelZ\xdcx\x07p\xe2z\x01y\x7f" +
"\xe4\x8c\xde\xb4kJ\xcf0\x04N\\)`\xc4\x1f\xe5" +
"\xa37+\x13\x97v\x01'\xaa\x02F\xfdi6z\xe3" +
"T\xf1\xfa\xb5\xc0\x89\xed\xc1L\x07\x1a\\?\x1a\xd1\xf6" +
"8\x0au\x0eK{Ox\xdcU\x00\x8dh{=0" +
"\x7f\xb6&\xd8Y\xe5\x0d) \x9eQ,\xd6H\xcd\x99" +
"{\xff\xb1\x94\x00\xa0\x11\xe5\x08\x86F\x85\x00\xe7\xfb\xbe" +
"L\xb1:'\xce\xdf\xb6e\xf2\xf6\x7f\xcb\x94\xc4\xf7g" +
"5\x9d\xe3\x0f\xbbBz\xbbB\x0f\xdfA\x1a\xbf\xc8\xd9" +
"\xbc\xf0\xc8\x1f\xa7\xcd\xa4\xff\xdf}\xfd\x87\xa8qz\x83" +
"G\xf9H\xe8Z\xbfK\xc2\xb7y\x94\x8f\x85\x1a\xa7\xa3" +
"t\xd7\x8f\xf0(\x9f\x09\xe6\x97\x9f\xdd\x01 \x9f\xe11" +
"\x15jD\xc4\xbf\xd3\xc2\xaf\xa9\\;m\x08\xbamH" +
"\x147\x03\xa4+\xa9\x8c'\x9c6$\xe2\xb6!\"v" +
"\x00\xa4kH~A\xb8\x0d\x19\x83\xd7\x01\xa4G\x93|" +
"<\xf6~\xd7\x08E#h\xd4rz\xe7\x1cU\xeb\xb7" +
"\xb6y\x03U\xb4f+j\xaeh0\x08Jk)\xd9" +
"4\x85\xaa\xbd;iu\x87*i\"a\x16M\x7f\xe0" +
"r\x0e/\xca!U\x9ef\xc3\xd0\xd1(kb\xa7\x06" +
"M\xac\xdf\xc3R/~5\x8f\xf2\x02\x0aE\xa3\x1b\x0a" +
"\xb9#h\xbb\xeb2J\xd1d}|\x00\x9e\x19\xfe\x14" +
"\xc0\\\xa2\x17s\xd9\x14\x03\xc12z\xca \x18\xb4\x99" +
"M\xb3\xb8\x97\xb9\xdc\xe1\xb0\xf7\x8f\x0e\xf4\xfe\x9f\x11\x1a" +
"\x0e{\x13z\xf4\xfem\xd5w8\xeca\xd0g8\xec" +
"~p8\xda{8|\x1e\xcfW\xb7\x8c\x852\xc69" +
"\xcdL\x87<j\xf4\xff\xb3[v\xd3\xab\xcewL\xe0" +
"\x15\xa4\x7f\x04\x00\x00\xff\xff\xa5\x0ed\xc9"
func init() {
schemas.Register(schema_db8274f9144abc7e,
0x82c325a07ad22a65,
0x839445a59fb01686,
0x83ced0145b2f114b,
0x84cb9536a2cf6d3c,
0x85c8cea1ab1894f3,
0x8635c6b4f45bf5cd,
0x904e297b87fbecea,
0x9496331ab9cd463f,
0x96b74375ce9b0ef6,
0x97b3c5c260257622,
0x9b87b390babc2ccf,
0xa29a916d4ebdd894,
0xa353a3556df74984,
0xa766b24d4fe5da35,
0xab6d5210c1f26687,
0xb046e578094b1ead,
0xb4bf9861fe035d04,
0xb5f39f082b9ac18a,
@@ -3574,6 +4090,7 @@ func init() {
0xe6646dec8feaa6ee,
0xea50d822450d1f17,
0xea58385c65416035,
0xf24ec4ab5891b676,
0xf2c122394f447e8e,
0xf2c68e2547ec3866,
0xf41a0f001ad49e46,
+55
View File
@@ -0,0 +1,55 @@
package tunnelstate
import (
"sync"
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/connection"
)
type ConnTracker struct {
sync.RWMutex
isConnected map[int]bool
log *zerolog.Logger
}
func NewConnTracker(log *zerolog.Logger) *ConnTracker {
return &ConnTracker{
isConnected: make(map[int]bool, 0),
log: log,
}
}
func MockedConnTracker(mocked map[int]bool) *ConnTracker {
return &ConnTracker{
isConnected: mocked,
}
}
func (ct *ConnTracker) OnTunnelEvent(c connection.Event) {
switch c.EventType {
case connection.Connected:
ct.Lock()
ct.isConnected[int(c.Index)] = true
ct.Unlock()
case connection.Disconnected, connection.Reconnecting, connection.RegisteringTunnel, connection.Unregistering:
ct.Lock()
ct.isConnected[int(c.Index)] = false
ct.Unlock()
default:
ct.log.Error().Msgf("Unknown connection event case %v", c)
}
}
func (ct *ConnTracker) CountActiveConns() uint {
ct.RLock()
defer ct.RUnlock()
active := uint(0)
for _, connected := range ct.isConnected {
if connected {
active++
}
}
return active
}
+15 -3
View File
@@ -15,10 +15,10 @@ import (
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"golang.org/x/net/http2"
"github.com/cloudflare/cloudflared/teamnet"
"github.com/cloudflare/cloudflared/vnet"
)
const (
@@ -236,8 +236,14 @@ type Client interface {
// Teamnet endpoints
ListRoutes(filter *teamnet.Filter) ([]*teamnet.DetailedRoute, error)
AddRoute(newRoute teamnet.NewRoute) (teamnet.Route, error)
DeleteRoute(network net.IPNet) error
GetByIP(ip net.IP) (teamnet.DetailedRoute, error)
DeleteRoute(params teamnet.DeleteRouteParams) error
GetByIP(params teamnet.GetRouteByIpParams) (teamnet.DetailedRoute, error)
// Virtual Networks endpoints
CreateVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet.VirtualNetwork, error)
ListVirtualNetworks(filter *vnet.Filter) ([]*vnet.VirtualNetwork, error)
DeleteVirtualNetwork(id uuid.UUID) error
UpdateVirtualNetwork(id uuid.UUID, updates vnet.UpdateVirtualNetwork) error
}
type RESTClient struct {
@@ -252,6 +258,7 @@ type baseEndpoints struct {
accountLevel url.URL
zoneLevel url.URL
accountRoutes url.URL
accountVnets url.URL
}
var _ Client = (*RESTClient)(nil)
@@ -268,6 +275,10 @@ func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, lo
if err != nil {
return nil, errors.Wrap(err, "failed to create route account-level endpoint")
}
accountVnetsEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/virtual_networks", baseURL, accountTag))
if err != nil {
return nil, errors.Wrap(err, "failed to create virtual network account-level endpoint")
}
zoneLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/zones/%s/tunnels", baseURL, zoneTag))
if err != nil {
return nil, errors.Wrap(err, "failed to create account level endpoint")
@@ -282,6 +293,7 @@ func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, lo
accountLevel: *accountLevelEndpoint,
zoneLevel: *zoneLevelEndpoint,
accountRoutes: *accountRoutesEndpoint,
accountVnets: *accountVnetsEndpoint,
},
authToken: authToken,
userAgent: userAgent,
+19 -11
View File
@@ -2,11 +2,11 @@ package tunnelstore
import (
"io"
"net"
"net/http"
"net/url"
"path"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/cloudflare/cloudflared/teamnet"
@@ -47,9 +47,11 @@ func (r *RESTClient) AddRoute(newRoute teamnet.NewRoute) (teamnet.Route, error)
}
// DeleteRoute calls the Tunnelstore DELETE endpoint for a given route.
func (r *RESTClient) DeleteRoute(network net.IPNet) error {
func (r *RESTClient) DeleteRoute(params teamnet.DeleteRouteParams) error {
endpoint := r.baseEndpoints.accountRoutes
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(network.String()))
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(params.Network.String()))
setVnetParam(&endpoint, params.VNetID)
resp, err := r.sendRequest("DELETE", endpoint, nil)
if err != nil {
return errors.Wrap(err, "REST request failed")
@@ -65,9 +67,11 @@ func (r *RESTClient) DeleteRoute(network net.IPNet) error {
}
// GetByIP checks which route will proxy a given IP.
func (r *RESTClient) GetByIP(ip net.IP) (teamnet.DetailedRoute, error) {
func (r *RESTClient) GetByIP(params teamnet.GetRouteByIpParams) (teamnet.DetailedRoute, error) {
endpoint := r.baseEndpoints.accountRoutes
endpoint.Path = path.Join(endpoint.Path, "ip", url.PathEscape(ip.String()))
endpoint.Path = path.Join(endpoint.Path, "ip", url.PathEscape(params.Ip.String()))
setVnetParam(&endpoint, params.VNetID)
resp, err := r.sendRequest("GET", endpoint, nil)
if err != nil {
return teamnet.DetailedRoute{}, errors.Wrap(err, "REST request failed")
@@ -81,12 +85,6 @@ func (r *RESTClient) GetByIP(ip net.IP) (teamnet.DetailedRoute, error) {
return teamnet.DetailedRoute{}, r.statusCodeToError("get route by IP", resp)
}
func parseListRoutes(body io.ReadCloser) ([]*teamnet.Route, error) {
var routes []*teamnet.Route
err := parseResponse(body, &routes)
return routes, err
}
func parseListDetailedRoutes(body io.ReadCloser) ([]*teamnet.DetailedRoute, error) {
var routes []*teamnet.DetailedRoute
err := parseResponse(body, &routes)
@@ -104,3 +102,13 @@ func parseDetailedRoute(body io.ReadCloser) (teamnet.DetailedRoute, error) {
err := parseResponse(body, &route)
return route, err
}
// setVnetParam overwrites the URL's query parameters with a query param to scope the Route action to a certain
// virtual network (if one is provided).
func setVnetParam(endpoint *url.URL, vnetID *uuid.UUID) {
queryParams := url.Values{}
if vnetID != nil {
queryParams.Set("virtual_network_id", vnetID.String())
}
endpoint.RawQuery = queryParams.Encode()
}
+89
View File
@@ -0,0 +1,89 @@
package tunnelstore
import (
"io"
"net/http"
"net/url"
"path"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/cloudflare/cloudflared/vnet"
)
func (r *RESTClient) CreateVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet.VirtualNetwork, error) {
resp, err := r.sendRequest("POST", r.baseEndpoints.accountVnets, newVnet)
if err != nil {
return vnet.VirtualNetwork{}, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return parseVnet(resp.Body)
}
return vnet.VirtualNetwork{}, r.statusCodeToError("add virtual network", resp)
}
func (r *RESTClient) ListVirtualNetworks(filter *vnet.Filter) ([]*vnet.VirtualNetwork, error) {
endpoint := r.baseEndpoints.accountVnets
endpoint.RawQuery = filter.Encode()
resp, err := r.sendRequest("GET", endpoint, nil)
if err != nil {
return nil, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return parseListVnets(resp.Body)
}
return nil, r.statusCodeToError("list virtual networks", resp)
}
func (r *RESTClient) DeleteVirtualNetwork(id uuid.UUID) error {
endpoint := r.baseEndpoints.accountVnets
endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String()))
resp, err := r.sendRequest("DELETE", endpoint, nil)
if err != nil {
return errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
_, err := parseVnet(resp.Body)
return err
}
return r.statusCodeToError("delete virtual network", resp)
}
func (r *RESTClient) UpdateVirtualNetwork(id uuid.UUID, updates vnet.UpdateVirtualNetwork) error {
endpoint := r.baseEndpoints.accountVnets
endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String()))
resp, err := r.sendRequest("PATCH", endpoint, updates)
if err != nil {
return errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
_, err := parseVnet(resp.Body)
return err
}
return r.statusCodeToError("update virtual network", resp)
}
func parseListVnets(body io.ReadCloser) ([]*vnet.VirtualNetwork, error) {
var vnets []*vnet.VirtualNetwork
err := parseResponse(body, &vnets)
return vnets, err
}
func parseVnet(body io.ReadCloser) (vnet.VirtualNetwork, error) {
var vnet vnet.VirtualNetwork
err := parseResponse(body, &vnet)
return vnet, err
}
-4
View File
@@ -21,7 +21,6 @@ linters:
- depguard
- exhaustive
- exportloopref
- goconst
- goimports
- gofmt # redundant, since gofmt *should* be a no-op after gofumpt
- gofumpt
@@ -41,9 +40,6 @@ linters:
issues:
exclude-rules:
- path: qlog/
linters:
- goconst
- path: internal/qtls
linters:
- depguard
+13
View File
@@ -44,6 +44,19 @@ http.Client{
}
```
## Projects using quic-go
| Project | Description | Stars |
|------------------------------------------------------|--------------------------------------------------------------------------------------------------------|-------|
| [algernon](https://github.com/xyproto/algernon) | Small self-contained pure-Go web server with Lua, Markdown, HTTP/2, QUIC, Redis and PostgreSQL support | ![GitHub Repo stars](https://img.shields.io/github/stars/xyproto/algernon?style=flat-square) |
| [caddy](https://github.com/caddyserver/caddy/) | Fast, multi-platform web server with automatic HTTPS | ![GitHub Repo stars](https://img.shields.io/github/stars/caddyserver/caddy?style=flat-square) |
| [go-ipfs](https://github.com/ipfs/go-ipfs) | IPFS implementation in go | ![GitHub Repo stars](https://img.shields.io/github/stars/ipfs/go-ipfs?style=flat-square) |
| [nextdns](https://github.com/nextdns/nextdns) | NextDNS CLI client (DoH Proxy) | ![GitHub Repo stars](https://img.shields.io/github/stars/nextdns/nextdns?style=flat-square) |
| [syncthing](https://github.com/syncthing/syncthing/) | Open Source Continuous File Synchronization | ![GitHub Repo stars](https://img.shields.io/github/stars/syncthing/syncthing?style=flat-square) |
| [traefik](https://github.com/traefik/traefik) | The Cloud Native Application Proxy | ![GitHub Repo stars](https://img.shields.io/github/stars/traefik/traefik?style=flat-square) |
| [v2ray-core](https://github.com/v2fly/v2ray-core) | A platform for building proxies to bypass network restrictions | ![GitHub Repo stars](https://img.shields.io/github/stars/v2fly/v2ray-core?style=flat-square) |
## Contributing
We are always happy to welcome new contributors! We have a number of self-contained issues that are suitable for first-time contributors, they are tagged with [help wanted](https://github.com/lucas-clemente/quic-go/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22). If you have any questions, please feel free to reach out by opening an issue or leaving a comment.
+3
View File
@@ -1,9 +1,12 @@
//go:build !darwin && !linux && !freebsd && !windows
// +build !darwin,!linux,!freebsd,!windows
package quic
import "net"
const disablePathMTUDiscovery = false
func newConn(c net.PacketConn) (connection, error) {
return &basicConn{PacketConn: c}, nil
}
+5 -1
View File
@@ -1,10 +1,14 @@
//go:build darwin
// +build darwin
package quic
import "golang.org/x/sys/unix"
const msgTypeIPTOS = unix.IP_RECVTOS
const (
msgTypeIPTOS = unix.IP_RECVTOS
disablePathMTUDiscovery = false
)
const (
ipv4RECVPKTINFO = unix.IP_RECVPKTINFO
+5 -1
View File
@@ -1,10 +1,14 @@
//go:build freebsd
// +build freebsd
package quic
import "golang.org/x/sys/unix"
const msgTypeIPTOS = unix.IP_RECVTOS
const (
msgTypeIPTOS = unix.IP_RECVTOS
disablePathMTUDiscovery = false
)
const (
ipv4RECVPKTINFO = 0x7
+5 -1
View File
@@ -1,10 +1,14 @@
//go:build linux
// +build linux
package quic
import "golang.org/x/sys/unix"
const msgTypeIPTOS = unix.IP_TOS
const (
msgTypeIPTOS = unix.IP_TOS
disablePathMTUDiscovery = false
)
const (
ipv4RECVPKTINFO = unix.IP_PKTINFO
+10 -40
View File
@@ -1,3 +1,4 @@
//go:build darwin || linux || freebsd
// +build darwin linux freebsd
package quic
@@ -7,10 +8,8 @@ import (
"errors"
"fmt"
"net"
"runtime"
"syscall"
"time"
"unsafe"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
@@ -238,50 +237,21 @@ func (info *packetInfo) OOB() []byte {
// struct in_addr ipi_spec_dst; /* Local address */
// struct in_addr ipi_addr; /* Header Destination address */
// };
msgLen := 12
if runtime.GOOS == "freebsd" {
msgLen = 4
cm := ipv4.ControlMessage{
Src: ip4,
IfIndex: int(info.ifIndex),
}
cmsglen := cmsgLen(msgLen)
oob := make([]byte, cmsglen)
cmsg := (*syscall.Cmsghdr)(unsafe.Pointer(&oob[0]))
cmsg.Level = syscall.IPPROTO_TCP
cmsg.Type = msgTypeIPv4PKTINFO
cmsg.SetLen(cmsglen)
off := cmsgLen(0)
if runtime.GOOS != "freebsd" {
// FreeBSD does not support in_pktinfo, just an in_addr is sent
binary.LittleEndian.PutUint32(oob[off:], info.ifIndex)
off += 4
}
copy(oob[off:], ip4)
return oob
return cm.Marshal()
} else if len(info.addr) == 16 {
// struct in6_pktinfo {
// struct in6_addr ipi6_addr; /* src/dst IPv6 address */
// unsigned int ipi6_ifindex; /* send/recv interface index */
// };
const msgLen = 20
cmsglen := cmsgLen(msgLen)
oob := make([]byte, cmsglen)
cmsg := (*syscall.Cmsghdr)(unsafe.Pointer(&oob[0]))
cmsg.Level = syscall.IPPROTO_IPV6
cmsg.Type = msgTypeIPv6PKTINFO
cmsg.SetLen(cmsglen)
off := cmsgLen(0)
off += copy(oob[off:], info.addr)
binary.LittleEndian.PutUint32(oob[off:], info.ifIndex)
return oob
cm := ipv6.ControlMessage{
Src: info.addr,
IfIndex: int(info.ifIndex),
}
return cm.Marshal()
}
return nil
}
func cmsgLen(datalen int) int {
return cmsgAlign(syscall.SizeofCmsghdr) + datalen
}
func cmsgAlign(salen int) int {
const sizeOfPtr = 0x8
salign := sizeOfPtr
return (salen + salign - 1) & ^(salign - 1)
}
+5 -1
View File
@@ -1,3 +1,4 @@
//go:build windows
// +build windows
package quic
@@ -11,7 +12,10 @@ import (
"golang.org/x/sys/windows"
)
const IP_DONTFRAGMENT = 14
const (
disablePathMTUDiscovery = true
IP_DONTFRAGMENT = 14
)
func newConn(c OOBCapablePacketConn) (connection, error) {
rawConn, err := c.SyscallConn()
+1
View File
@@ -283,6 +283,7 @@ type Config struct {
KeepAlive bool
// DisablePathMTUDiscovery disables Path MTU Discovery (RFC 8899).
// Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.
// Note that Path MTU discovery is always disabled on Windows, see https://github.com/lucas-clemente/quic-go/issues/3273.
DisablePathMTUDiscovery bool
// DisableVersionNegotiationPackets disables the sending of Version Negotiation packets.
// This can be useful if version information is exchanged out-of-band.
+2 -2
View File
@@ -1,5 +1,5 @@
// +build go1.16
// +build !go1.17
//go:build go1.16 && !go1.17
// +build go1.16,!go1.17
package qtls
+1
View File
@@ -1,3 +1,4 @@
//go:build go1.17
// +build go1.17
package qtls
+1
View File
@@ -1,3 +1,4 @@
//go:build go1.18
// +build go1.18
package qtls
-3
View File
@@ -166,13 +166,10 @@ func (s *receiveStream) readImpl(p []byte) (bool /*stream completed */, int, err
return false, bytesRead, fmt.Errorf("BUG: readPosInFrame (%d) > frame.DataLen (%d) in stream.Read", s.readPosInFrame, len(s.currentFrame))
}
s.mutex.Unlock()
m := copy(p[bytesRead:], s.currentFrame[s.readPosInFrame:])
s.readPosInFrame += m
bytesRead += m
s.mutex.Lock()
// when a RESET_STREAM was received, the was already informed about the final byteOffset for this stream
if !s.resetRemotely {
s.flowController.AddBytesRead(protocol.ByteCount(m))
+10 -4
View File
@@ -130,6 +130,10 @@ func (e *errCloseForRecreating) Error() string {
var sessionTracingID uint64 // to be accessed atomically
func nextSessionTracingID() uint64 { return atomic.AddUint64(&sessionTracingID, 1) }
func pathMTUDiscoveryEnabled(config *Config) bool {
return !disablePathMTUDiscovery && !config.DisablePathMTUDiscovery
}
// A Session is a QUIC session
type session struct {
// Destination connection ID used during the handshake.
@@ -590,7 +594,9 @@ runLoop:
default:
}
}
} else if !processedUndecryptablePacket {
}
// If we processed any undecryptable packets, jump to the resetting of the timers directly.
if !processedUndecryptablePacket {
select {
case closeErr = <-s.closeChan:
break runLoop
@@ -743,7 +749,7 @@ func (s *session) maybeResetTimer() {
deadline = s.idleTimeoutStartTime().Add(s.idleTimeout)
}
}
if s.handshakeConfirmed && !s.config.DisablePathMTUDiscovery {
if s.handshakeConfirmed && pathMTUDiscoveryEnabled(s.config) {
if probeTime := s.mtuDiscoverer.NextProbeTime(); !probeTime.IsZero() {
deadline = utils.MinTime(deadline, probeTime)
}
@@ -807,7 +813,7 @@ func (s *session) handleHandshakeConfirmed() {
s.sentPacketHandler.SetHandshakeConfirmed()
s.cryptoStreamHandler.SetHandshakeConfirmed()
if !s.config.DisablePathMTUDiscovery {
if pathMTUDiscoveryEnabled(s.config) {
maxPacketSize := s.peerParams.MaxUDPPayloadSize
if maxPacketSize == 0 {
maxPacketSize = protocol.MaxByteCount
@@ -1768,7 +1774,7 @@ func (s *session) sendPacket() (bool, error) {
s.sendQueue.Send(packet.buffer)
return true, nil
}
if !s.config.DisablePathMTUDiscovery && s.mtuDiscoverer.ShouldSendProbe(now) {
if pathMTUDiscoveryEnabled(s.config) && s.mtuDiscoverer.ShouldSendProbe(now) {
packet, err := s.packer.PackMTUProbePacket(s.mtuDiscoverer.GetPing())
if err != nil {
return false, err
+1
View File
@@ -1,3 +1,4 @@
//go:build tools
// +build tools
package quic
-20
View File
@@ -1,20 +0,0 @@
This is a work-in-progress HTTP/2 implementation for Go.
It will eventually live in the Go standard library and won't require
any changes to your code to use. It will just be automatic.
Status:
* The server support is pretty good. A few things are missing
but are being worked on.
* The client work has just started but shares a lot of code
is coming along much quicker.
Docs are at https://godoc.org/golang.org/x/net/http2
Demo test server at https://http2.golang.org/
Help & bug reports welcome!
Contributing: https://golang.org/doc/contribute.html
Bugs: https://golang.org/issue/new?title=x/net/http2:+
+53
View File
@@ -0,0 +1,53 @@
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package http2
import "strings"
// The HTTP protocols are defined in terms of ASCII, not Unicode. This file
// contains helper functions which may use Unicode-aware functions which would
// otherwise be unsafe and could introduce vulnerabilities if used improperly.
// asciiEqualFold is strings.EqualFold, ASCII only. It reports whether s and t
// are equal, ASCII-case-insensitively.
func asciiEqualFold(s, t string) bool {
if len(s) != len(t) {
return false
}
for i := 0; i < len(s); i++ {
if lower(s[i]) != lower(t[i]) {
return false
}
}
return true
}
// lower returns the ASCII lowercase version of b.
func lower(b byte) byte {
if 'A' <= b && b <= 'Z' {
return b + ('a' - 'A')
}
return b
}
// isASCIIPrint returns whether s is ASCII and printable according to
// https://tools.ietf.org/html/rfc20#section-4.2.
func isASCIIPrint(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] < ' ' || s[i] > '~' {
return false
}
}
return true
}
// asciiToLower returns the lowercase version of s if s is ASCII and printable,
// and whether or not it was.
func asciiToLower(s string) (lower string, ok bool) {
if !isASCIIPrint(s) {
return "", false
}
return strings.ToLower(s), true
}
+76 -44
View File
@@ -7,13 +7,21 @@
package http2
import (
"context"
"crypto/tls"
"errors"
"net/http"
"sync"
)
// ClientConnPool manages a pool of HTTP/2 client connections.
type ClientConnPool interface {
// GetClientConn returns a specific HTTP/2 connection (usually
// a TLS-TCP connection) to an HTTP/2 server. On success, the
// returned ClientConn accounts for the upcoming RoundTrip
// call, so the caller should not omit it. If the caller needs
// to, ClientConn.RoundTrip can be called with a bogus
// new(http.Request) to release the stream reservation.
GetClientConn(req *http.Request, addr string) (*ClientConn, error)
MarkDead(*ClientConn)
}
@@ -40,7 +48,7 @@ type clientConnPool struct {
conns map[string][]*ClientConn // key is host:port
dialing map[string]*dialCall // currently in-flight dials
keys map[*ClientConn][]string
addConnCalls map[string]*addConnCall // in-flight addConnIfNeede calls
addConnCalls map[string]*addConnCall // in-flight addConnIfNeeded calls
}
func (p *clientConnPool) GetClientConn(req *http.Request, addr string) (*ClientConn, error) {
@@ -52,87 +60,85 @@ const (
noDialOnMiss = false
)
// shouldTraceGetConn reports whether getClientConn should call any
// ClientTrace.GetConn hook associated with the http.Request.
//
// This complexity is needed to avoid double calls of the GetConn hook
// during the back-and-forth between net/http and x/net/http2 (when the
// net/http.Transport is upgraded to also speak http2), as well as support
// the case where x/net/http2 is being used directly.
func (p *clientConnPool) shouldTraceGetConn(st clientConnIdleState) bool {
// If our Transport wasn't made via ConfigureTransport, always
// trace the GetConn hook if provided, because that means the
// http2 package is being used directly and it's the one
// dialing, as opposed to net/http.
if _, ok := p.t.ConnPool.(noDialClientConnPool); !ok {
return true
}
// Otherwise, only use the GetConn hook if this connection has
// been used previously for other requests. For fresh
// connections, the net/http package does the dialing.
return !st.freshConn
}
func (p *clientConnPool) getClientConn(req *http.Request, addr string, dialOnMiss bool) (*ClientConn, error) {
// TODO(dneil): Dial a new connection when t.DisableKeepAlives is set?
if isConnectionCloseRequest(req) && dialOnMiss {
// It gets its own connection.
traceGetConn(req, addr)
const singleUse = true
cc, err := p.t.dialClientConn(addr, singleUse)
cc, err := p.t.dialClientConn(req.Context(), addr, singleUse)
if err != nil {
return nil, err
}
return cc, nil
}
p.mu.Lock()
for _, cc := range p.conns[addr] {
if st := cc.idleState(); st.canTakeNewRequest {
if p.shouldTraceGetConn(st) {
traceGetConn(req, addr)
for {
p.mu.Lock()
for _, cc := range p.conns[addr] {
if cc.ReserveNewRequest() {
// When a connection is presented to us by the net/http package,
// the GetConn hook has already been called.
// Don't call it a second time here.
if !cc.getConnCalled {
traceGetConn(req, addr)
}
cc.getConnCalled = false
p.mu.Unlock()
return cc, nil
}
}
if !dialOnMiss {
p.mu.Unlock()
return nil, ErrNoCachedConn
}
traceGetConn(req, addr)
call := p.getStartDialLocked(req.Context(), addr)
p.mu.Unlock()
<-call.done
if shouldRetryDial(call, req) {
continue
}
cc, err := call.res, call.err
if err != nil {
return nil, err
}
if cc.ReserveNewRequest() {
return cc, nil
}
}
if !dialOnMiss {
p.mu.Unlock()
return nil, ErrNoCachedConn
}
traceGetConn(req, addr)
call := p.getStartDialLocked(addr)
p.mu.Unlock()
<-call.done
return call.res, call.err
}
// dialCall is an in-flight Transport dial call to a host.
type dialCall struct {
_ incomparable
p *clientConnPool
_ incomparable
p *clientConnPool
// the context associated with the request
// that created this dialCall
ctx context.Context
done chan struct{} // closed when done
res *ClientConn // valid after done is closed
err error // valid after done is closed
}
// requires p.mu is held.
func (p *clientConnPool) getStartDialLocked(addr string) *dialCall {
func (p *clientConnPool) getStartDialLocked(ctx context.Context, addr string) *dialCall {
if call, ok := p.dialing[addr]; ok {
// A dial is already in-flight. Don't start another.
return call
}
call := &dialCall{p: p, done: make(chan struct{})}
call := &dialCall{p: p, done: make(chan struct{}), ctx: ctx}
if p.dialing == nil {
p.dialing = make(map[string]*dialCall)
}
p.dialing[addr] = call
go call.dial(addr)
go call.dial(call.ctx, addr)
return call
}
// run in its own goroutine.
func (c *dialCall) dial(addr string) {
func (c *dialCall) dial(ctx context.Context, addr string) {
const singleUse = false // shared conn
c.res, c.err = c.p.t.dialClientConn(addr, singleUse)
c.res, c.err = c.p.t.dialClientConn(ctx, addr, singleUse)
close(c.done)
c.p.mu.Lock()
@@ -195,6 +201,7 @@ func (c *addConnCall) run(t *Transport, key string, tc *tls.Conn) {
if err != nil {
c.err = err
} else {
cc.getConnCalled = true // already called by the net/http package
p.addConnLocked(key, cc)
}
delete(p.addConnCalls, key)
@@ -276,3 +283,28 @@ type noDialClientConnPool struct{ *clientConnPool }
func (p noDialClientConnPool) GetClientConn(req *http.Request, addr string) (*ClientConn, error) {
return p.getClientConn(req, addr, noDialOnMiss)
}
// shouldRetryDial reports whether the current request should
// retry dialing after the call finished unsuccessfully, for example
// if the dial was canceled because of a context cancellation or
// deadline expiry.
func shouldRetryDial(call *dialCall, req *http.Request) bool {
if call.err == nil {
// No error, no need to retry
return false
}
if call.ctx == req.Context() {
// If the call has the same context as the request, the dial
// should not be retried, since any cancellation will have come
// from this request.
return false
}
if !errors.Is(call.err, context.Canceled) && !errors.Is(call.err, context.DeadlineExceeded) {
// If the call error is not because of a context cancellation or a deadline expiry,
// the dial should not be retried.
return false
}
// Only retry if the error is a context cancellation error or deadline expiry
// and the context associated with the call was canceled or expired.
return call.ctx.Err() != nil
}
+12
View File
@@ -53,6 +53,13 @@ func (e ErrCode) String() string {
return fmt.Sprintf("unknown error code 0x%x", uint32(e))
}
func (e ErrCode) stringToken() string {
if s, ok := errCodeName[e]; ok {
return s
}
return fmt.Sprintf("ERR_UNKNOWN_%d", uint32(e))
}
// ConnectionError is an error that results in the termination of the
// entire connection.
type ConnectionError ErrCode
@@ -67,6 +74,11 @@ type StreamError struct {
Cause error // optional additional detail
}
// errFromPeer is a sentinel error value for StreamError.Cause to
// indicate that the StreamError was sent from the peer over the wire
// and wasn't locally generated in the Transport.
var errFromPeer = errors.New("received from peer")
func streamError(id uint32, code ErrCode) StreamError {
return StreamError{StreamID: id, Code: code}
}
+48 -14
View File
@@ -122,7 +122,7 @@ var flagName = map[FrameType]map[Flags]string{
// a frameParser parses a frame given its FrameHeader and payload
// bytes. The length of payload will always equal fh.Length (which
// might be 0).
type frameParser func(fc *frameCache, fh FrameHeader, payload []byte) (Frame, error)
type frameParser func(fc *frameCache, fh FrameHeader, countError func(string), payload []byte) (Frame, error)
var frameParsers = map[FrameType]frameParser{
FrameData: parseDataFrame,
@@ -267,6 +267,11 @@ type Framer struct {
lastFrame Frame
errDetail error
// countError is a non-nil func that's called on a frame parse
// error with some unique error path token. It's initialized
// from Transport.CountError or Server.CountError.
countError func(errToken string)
// lastHeaderStream is non-zero if the last frame was an
// unfinished HEADERS/CONTINUATION.
lastHeaderStream uint32
@@ -426,6 +431,7 @@ func NewFramer(w io.Writer, r io.Reader) *Framer {
fr := &Framer{
w: w,
r: r,
countError: func(string) {},
logReads: logFrameReads,
logWrites: logFrameWrites,
debugReadLoggerf: log.Printf,
@@ -500,7 +506,7 @@ func (fr *Framer) ReadFrame() (Frame, error) {
if _, err := io.ReadFull(fr.r, payload); err != nil {
return nil, err
}
f, err := typeFrameParser(fh.Type)(fr.frameCache, fh, payload)
f, err := typeFrameParser(fh.Type)(fr.frameCache, fh, fr.countError, payload)
if err != nil {
if ce, ok := err.(connError); ok {
return nil, fr.connError(ce.Code, ce.Reason)
@@ -588,13 +594,14 @@ func (f *DataFrame) Data() []byte {
return f.data
}
func parseDataFrame(fc *frameCache, fh FrameHeader, payload []byte) (Frame, error) {
func parseDataFrame(fc *frameCache, fh FrameHeader, countError func(string), payload []byte) (Frame, error) {
if fh.StreamID == 0 {
// DATA frames MUST be associated with a stream. If a
// DATA frame is received whose stream identifier
// field is 0x0, the recipient MUST respond with a
// connection error (Section 5.4.1) of type
// PROTOCOL_ERROR.
countError("frame_data_stream_0")
return nil, connError{ErrCodeProtocol, "DATA frame with stream ID 0"}
}
f := fc.getDataFrame()
@@ -605,6 +612,7 @@ func parseDataFrame(fc *frameCache, fh FrameHeader, payload []byte) (Frame, erro
var err error
payload, padSize, err = readByte(payload)
if err != nil {
countError("frame_data_pad_byte_short")
return nil, err
}
}
@@ -613,6 +621,7 @@ func parseDataFrame(fc *frameCache, fh FrameHeader, payload []byte) (Frame, erro
// length of the frame payload, the recipient MUST
// treat this as a connection error.
// Filed: https://github.com/http2/http2-spec/issues/610
countError("frame_data_pad_too_big")
return nil, connError{ErrCodeProtocol, "pad size larger than data payload"}
}
f.data = payload[:len(payload)-int(padSize)]
@@ -695,7 +704,7 @@ type SettingsFrame struct {
p []byte
}
func parseSettingsFrame(_ *frameCache, fh FrameHeader, p []byte) (Frame, error) {
func parseSettingsFrame(_ *frameCache, fh FrameHeader, countError func(string), p []byte) (Frame, error) {
if fh.Flags.Has(FlagSettingsAck) && fh.Length > 0 {
// When this (ACK 0x1) bit is set, the payload of the
// SETTINGS frame MUST be empty. Receipt of a
@@ -703,6 +712,7 @@ func parseSettingsFrame(_ *frameCache, fh FrameHeader, p []byte) (Frame, error)
// field value other than 0 MUST be treated as a
// connection error (Section 5.4.1) of type
// FRAME_SIZE_ERROR.
countError("frame_settings_ack_with_length")
return nil, ConnectionError(ErrCodeFrameSize)
}
if fh.StreamID != 0 {
@@ -713,14 +723,17 @@ func parseSettingsFrame(_ *frameCache, fh FrameHeader, p []byte) (Frame, error)
// field is anything other than 0x0, the endpoint MUST
// respond with a connection error (Section 5.4.1) of
// type PROTOCOL_ERROR.
countError("frame_settings_has_stream")
return nil, ConnectionError(ErrCodeProtocol)
}
if len(p)%6 != 0 {
countError("frame_settings_mod_6")
// Expecting even number of 6 byte settings.
return nil, ConnectionError(ErrCodeFrameSize)
}
f := &SettingsFrame{FrameHeader: fh, p: p}
if v, ok := f.Value(SettingInitialWindowSize); ok && v > (1<<31)-1 {
countError("frame_settings_window_size_too_big")
// Values above the maximum flow control window size of 2^31 - 1 MUST
// be treated as a connection error (Section 5.4.1) of type
// FLOW_CONTROL_ERROR.
@@ -832,11 +845,13 @@ type PingFrame struct {
func (f *PingFrame) IsAck() bool { return f.Flags.Has(FlagPingAck) }
func parsePingFrame(_ *frameCache, fh FrameHeader, payload []byte) (Frame, error) {
func parsePingFrame(_ *frameCache, fh FrameHeader, countError func(string), payload []byte) (Frame, error) {
if len(payload) != 8 {
countError("frame_ping_length")
return nil, ConnectionError(ErrCodeFrameSize)
}
if fh.StreamID != 0 {
countError("frame_ping_has_stream")
return nil, ConnectionError(ErrCodeProtocol)
}
f := &PingFrame{FrameHeader: fh}
@@ -872,11 +887,13 @@ func (f *GoAwayFrame) DebugData() []byte {
return f.debugData
}
func parseGoAwayFrame(_ *frameCache, fh FrameHeader, p []byte) (Frame, error) {
func parseGoAwayFrame(_ *frameCache, fh FrameHeader, countError func(string), p []byte) (Frame, error) {
if fh.StreamID != 0 {
countError("frame_goaway_has_stream")
return nil, ConnectionError(ErrCodeProtocol)
}
if len(p) < 8 {
countError("frame_goaway_short")
return nil, ConnectionError(ErrCodeFrameSize)
}
return &GoAwayFrame{
@@ -912,7 +929,7 @@ func (f *UnknownFrame) Payload() []byte {
return f.p
}
func parseUnknownFrame(_ *frameCache, fh FrameHeader, p []byte) (Frame, error) {
func parseUnknownFrame(_ *frameCache, fh FrameHeader, countError func(string), p []byte) (Frame, error) {
return &UnknownFrame{fh, p}, nil
}
@@ -923,8 +940,9 @@ type WindowUpdateFrame struct {
Increment uint32 // never read with high bit set
}
func parseWindowUpdateFrame(_ *frameCache, fh FrameHeader, p []byte) (Frame, error) {
func parseWindowUpdateFrame(_ *frameCache, fh FrameHeader, countError func(string), p []byte) (Frame, error) {
if len(p) != 4 {
countError("frame_windowupdate_bad_len")
return nil, ConnectionError(ErrCodeFrameSize)
}
inc := binary.BigEndian.Uint32(p[:4]) & 0x7fffffff // mask off high reserved bit
@@ -936,8 +954,10 @@ func parseWindowUpdateFrame(_ *frameCache, fh FrameHeader, p []byte) (Frame, err
// control window MUST be treated as a connection
// error (Section 5.4.1).
if fh.StreamID == 0 {
countError("frame_windowupdate_zero_inc_conn")
return nil, ConnectionError(ErrCodeProtocol)
}
countError("frame_windowupdate_zero_inc_stream")
return nil, streamError(fh.StreamID, ErrCodeProtocol)
}
return &WindowUpdateFrame{
@@ -988,7 +1008,7 @@ func (f *HeadersFrame) HasPriority() bool {
return f.FrameHeader.Flags.Has(FlagHeadersPriority)
}
func parseHeadersFrame(_ *frameCache, fh FrameHeader, p []byte) (_ Frame, err error) {
func parseHeadersFrame(_ *frameCache, fh FrameHeader, countError func(string), p []byte) (_ Frame, err error) {
hf := &HeadersFrame{
FrameHeader: fh,
}
@@ -997,11 +1017,13 @@ func parseHeadersFrame(_ *frameCache, fh FrameHeader, p []byte) (_ Frame, err er
// is received whose stream identifier field is 0x0, the recipient MUST
// respond with a connection error (Section 5.4.1) of type
// PROTOCOL_ERROR.
countError("frame_headers_zero_stream")
return nil, connError{ErrCodeProtocol, "HEADERS frame with stream ID 0"}
}
var padLength uint8
if fh.Flags.Has(FlagHeadersPadded) {
if p, padLength, err = readByte(p); err != nil {
countError("frame_headers_pad_short")
return
}
}
@@ -1009,16 +1031,19 @@ func parseHeadersFrame(_ *frameCache, fh FrameHeader, p []byte) (_ Frame, err er
var v uint32
p, v, err = readUint32(p)
if err != nil {
countError("frame_headers_prio_short")
return nil, err
}
hf.Priority.StreamDep = v & 0x7fffffff
hf.Priority.Exclusive = (v != hf.Priority.StreamDep) // high bit was set
p, hf.Priority.Weight, err = readByte(p)
if err != nil {
countError("frame_headers_prio_weight_short")
return nil, err
}
}
if len(p)-int(padLength) <= 0 {
if len(p)-int(padLength) < 0 {
countError("frame_headers_pad_too_big")
return nil, streamError(fh.StreamID, ErrCodeProtocol)
}
hf.headerFragBuf = p[:len(p)-int(padLength)]
@@ -1125,11 +1150,13 @@ func (p PriorityParam) IsZero() bool {
return p == PriorityParam{}
}
func parsePriorityFrame(_ *frameCache, fh FrameHeader, payload []byte) (Frame, error) {
func parsePriorityFrame(_ *frameCache, fh FrameHeader, countError func(string), payload []byte) (Frame, error) {
if fh.StreamID == 0 {
countError("frame_priority_zero_stream")
return nil, connError{ErrCodeProtocol, "PRIORITY frame with stream ID 0"}
}
if len(payload) != 5 {
countError("frame_priority_bad_length")
return nil, connError{ErrCodeFrameSize, fmt.Sprintf("PRIORITY frame payload size was %d; want 5", len(payload))}
}
v := binary.BigEndian.Uint32(payload[:4])
@@ -1172,11 +1199,13 @@ type RSTStreamFrame struct {
ErrCode ErrCode
}
func parseRSTStreamFrame(_ *frameCache, fh FrameHeader, p []byte) (Frame, error) {
func parseRSTStreamFrame(_ *frameCache, fh FrameHeader, countError func(string), p []byte) (Frame, error) {
if len(p) != 4 {
countError("frame_rststream_bad_len")
return nil, ConnectionError(ErrCodeFrameSize)
}
if fh.StreamID == 0 {
countError("frame_rststream_zero_stream")
return nil, ConnectionError(ErrCodeProtocol)
}
return &RSTStreamFrame{fh, ErrCode(binary.BigEndian.Uint32(p[:4]))}, nil
@@ -1202,8 +1231,9 @@ type ContinuationFrame struct {
headerFragBuf []byte
}
func parseContinuationFrame(_ *frameCache, fh FrameHeader, p []byte) (Frame, error) {
func parseContinuationFrame(_ *frameCache, fh FrameHeader, countError func(string), p []byte) (Frame, error) {
if fh.StreamID == 0 {
countError("frame_continuation_zero_stream")
return nil, connError{ErrCodeProtocol, "CONTINUATION frame with stream ID 0"}
}
return &ContinuationFrame{fh, p}, nil
@@ -1252,7 +1282,7 @@ func (f *PushPromiseFrame) HeadersEnded() bool {
return f.FrameHeader.Flags.Has(FlagPushPromiseEndHeaders)
}
func parsePushPromise(_ *frameCache, fh FrameHeader, p []byte) (_ Frame, err error) {
func parsePushPromise(_ *frameCache, fh FrameHeader, countError func(string), p []byte) (_ Frame, err error) {
pp := &PushPromiseFrame{
FrameHeader: fh,
}
@@ -1263,6 +1293,7 @@ func parsePushPromise(_ *frameCache, fh FrameHeader, p []byte) (_ Frame, err err
// with. If the stream identifier field specifies the value
// 0x0, a recipient MUST respond with a connection error
// (Section 5.4.1) of type PROTOCOL_ERROR.
countError("frame_pushpromise_zero_stream")
return nil, ConnectionError(ErrCodeProtocol)
}
// The PUSH_PROMISE frame includes optional padding.
@@ -1270,18 +1301,21 @@ func parsePushPromise(_ *frameCache, fh FrameHeader, p []byte) (_ Frame, err err
var padLength uint8
if fh.Flags.Has(FlagPushPromisePadded) {
if p, padLength, err = readByte(p); err != nil {
countError("frame_pushpromise_pad_short")
return
}
}
p, pp.PromiseID, err = readUint32(p)
if err != nil {
countError("frame_pushpromise_promiseid_short")
return
}
pp.PromiseID = pp.PromiseID & (1<<31 - 1)
if int(padLength) > len(p) {
// like the DATA frame, error out if padding is longer than the body.
countError("frame_pushpromise_pad_too_big")
return nil, ConnectionError(ErrCodeProtocol)
}
pp.headerFragBuf = p[:len(p)-int(padLength)]
+27
View File
@@ -0,0 +1,27 @@
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.15
// +build go1.15
package http2
import (
"context"
"crypto/tls"
)
// dialTLSWithContext uses tls.Dialer, added in Go 1.15, to open a TLS
// connection.
func (t *Transport) dialTLSWithContext(ctx context.Context, network, addr string, cfg *tls.Config) (*tls.Conn, error) {
dialer := &tls.Dialer{
Config: cfg,
}
cn, err := dialer.DialContext(ctx, network, addr)
if err != nil {
return nil, err
}
tlsCn := cn.(*tls.Conn) // DialContext comment promises this will always succeed
return tlsCn, nil
}
+3 -4
View File
@@ -6,7 +6,6 @@ package http2
import (
"net/http"
"strings"
"sync"
)
@@ -79,10 +78,10 @@ func buildCommonHeaderMaps() {
}
}
func lowerHeader(v string) string {
func lowerHeader(v string) (lower string, ascii bool) {
buildCommonHeaderMapsOnce()
if s, ok := commonLowerHeader[v]; ok {
return s
return s, true
}
return strings.ToLower(v)
return asciiToLower(v)
}
+21 -17
View File
@@ -140,25 +140,29 @@ func buildRootHuffmanNode() {
panic("unexpected size")
}
lazyRootHuffmanNode = newInternalNode()
for i, code := range huffmanCodes {
addDecoderNode(byte(i), code, huffmanCodeLen[i])
}
}
// allocate a leaf node for each of the 256 symbols
leaves := new([256]node)
func addDecoderNode(sym byte, code uint32, codeLen uint8) {
cur := lazyRootHuffmanNode
for codeLen > 8 {
codeLen -= 8
i := uint8(code >> codeLen)
if cur.children[i] == nil {
cur.children[i] = newInternalNode()
for sym, code := range huffmanCodes {
codeLen := huffmanCodeLen[sym]
cur := lazyRootHuffmanNode
for codeLen > 8 {
codeLen -= 8
i := uint8(code >> codeLen)
if cur.children[i] == nil {
cur.children[i] = newInternalNode()
}
cur = cur.children[i]
}
shift := 8 - codeLen
start, end := int(uint8(code<<shift)), int(1<<shift)
leaves[sym].sym = byte(sym)
leaves[sym].codeLen = codeLen
for i := start; i < start+end; i++ {
cur.children[i] = &leaves[sym]
}
cur = cur.children[i]
}
shift := 8 - codeLen
start, end := int(uint8(code<<shift)), int(1<<shift)
for i := start; i < start+end; i++ {
cur.children[i] = &node{sym: sym, codeLen: codeLen}
}
}
+31
View File
@@ -0,0 +1,31 @@
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !go1.15
// +build !go1.15
package http2
import (
"context"
"crypto/tls"
)
// dialTLSWithContext opens a TLS connection.
func (t *Transport) dialTLSWithContext(ctx context.Context, network, addr string, cfg *tls.Config) (*tls.Conn, error) {
cn, err := tls.Dial(network, addr, cfg)
if err != nil {
return nil, err
}
if err := cn.Handshake(); err != nil {
return nil, err
}
if cfg.InsecureSkipVerify {
return cn, nil
}
if err := cn.VerifyHostname(cfg.ServerName); err != nil {
return nil, err
}
return cn, nil
}
+11
View File
@@ -30,6 +30,17 @@ type pipeBuffer interface {
io.Reader
}
// setBuffer initializes the pipe buffer.
// It has no effect if the pipe is already closed.
func (p *pipe) setBuffer(b pipeBuffer) {
p.mu.Lock()
defer p.mu.Unlock()
if p.err != nil || p.breakErr != nil {
return
}
p.b = b
}
func (p *pipe) Len() int {
p.mu.Lock()
defer p.mu.Unlock()
+95 -55
View File
@@ -130,6 +130,12 @@ type Server struct {
// If nil, a default scheduler is chosen.
NewWriteScheduler func() WriteScheduler
// CountError, if non-nil, is called on HTTP/2 server errors.
// It's intended to increment a metric for monitoring, such
// as an expvar or Prometheus metric.
// The errType consists of only ASCII word characters.
CountError func(errType string)
// Internal state. This is a pointer (rather than embedded directly)
// so that we don't embed a Mutex in this struct, which will make the
// struct non-copyable, which might break some callers.
@@ -231,13 +237,12 @@ func ConfigureServer(s *http.Server, conf *Server) error {
if s.TLSConfig == nil {
s.TLSConfig = new(tls.Config)
} else if s.TLSConfig.CipherSuites != nil {
// If they already provided a CipherSuite list, return
// an error if it has a bad order or is missing
// ECDHE_RSA_WITH_AES_128_GCM_SHA256 or ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.
} else if s.TLSConfig.CipherSuites != nil && s.TLSConfig.MinVersion < tls.VersionTLS13 {
// If they already provided a TLS 1.01.2 CipherSuite list, return an
// error if it is missing ECDHE_RSA_WITH_AES_128_GCM_SHA256 or
// ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.
haveRequired := false
sawBad := false
for i, cs := range s.TLSConfig.CipherSuites {
for _, cs := range s.TLSConfig.CipherSuites {
switch cs {
case tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
// Alternative MTI cipher to not discourage ECDSA-only servers.
@@ -245,14 +250,9 @@ func ConfigureServer(s *http.Server, conf *Server) error {
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:
haveRequired = true
}
if isBadCipher(cs) {
sawBad = true
} else if sawBad {
return fmt.Errorf("http2: TLSConfig.CipherSuites index %d contains an HTTP/2-approved cipher suite (%#04x), but it comes after unapproved cipher suites. With this configuration, clients that don't support previous, approved cipher suites may be given an unapproved one and reject the connection.", i, cs)
}
}
if !haveRequired {
return fmt.Errorf("http2: TLSConfig.CipherSuites is missing an HTTP/2-required AES_128_GCM_SHA256 cipher (need at least one of TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 or TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256).")
return fmt.Errorf("http2: TLSConfig.CipherSuites is missing an HTTP/2-required AES_128_GCM_SHA256 cipher (need at least one of TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 or TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256)")
}
}
@@ -265,16 +265,12 @@ func ConfigureServer(s *http.Server, conf *Server) error {
s.TLSConfig.PreferServerCipherSuites = true
haveNPN := false
for _, p := range s.TLSConfig.NextProtos {
if p == NextProtoTLS {
haveNPN = true
break
}
}
if !haveNPN {
if !strSliceContains(s.TLSConfig.NextProtos, NextProtoTLS) {
s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, NextProtoTLS)
}
if !strSliceContains(s.TLSConfig.NextProtos, "http/1.1") {
s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, "http/1.1")
}
if s.TLSNextProto == nil {
s.TLSNextProto = map[string]func(*http.Server, *tls.Conn, http.Handler){}
@@ -415,6 +411,9 @@ func (s *Server) ServeConn(c net.Conn, opts *ServeConnOpts) {
sc.hpackEncoder = hpack.NewEncoder(&sc.headerWriteBuf)
fr := NewFramer(sc.bw, c)
if s.CountError != nil {
fr.countError = s.CountError
}
fr.ReadMetaHeaders = hpack.NewDecoder(initialHeaderTableSize, nil)
fr.MaxHeaderListSize = sc.maxHeaderListSize()
fr.SetMaxReadFrameSize(s.maxReadFrameSize())
@@ -826,7 +825,7 @@ func (sc *serverConn) serve() {
})
sc.unackedSettings++
// Each connection starts with intialWindowSize inflow tokens.
// Each connection starts with initialWindowSize inflow tokens.
// If a higher value is configured, we add more tokens.
if diff := sc.srv.initialConnRecvWindowSize() - initialWindowSize; diff > 0 {
sc.sendWindowUpdate(nil, int(diff))
@@ -866,6 +865,15 @@ func (sc *serverConn) serve() {
case res := <-sc.wroteFrameCh:
sc.wroteFrame(res)
case res := <-sc.readFrameCh:
// Process any written frames before reading new frames from the client since a
// written frame could have triggered a new stream to be started.
if sc.writingFrameAsync {
select {
case wroteRes := <-sc.wroteFrameCh:
sc.wroteFrame(wroteRes)
default:
}
}
if !sc.processFrameFromReader(res) {
return
}
@@ -1400,7 +1408,7 @@ func (sc *serverConn) processFrame(f Frame) error {
// First frame received must be SETTINGS.
if !sc.sawFirstSettings {
if _, ok := f.(*SettingsFrame); !ok {
return ConnectionError(ErrCodeProtocol)
return sc.countError("first_settings", ConnectionError(ErrCodeProtocol))
}
sc.sawFirstSettings = true
}
@@ -1425,7 +1433,7 @@ func (sc *serverConn) processFrame(f Frame) error {
case *PushPromiseFrame:
// A client cannot push. Thus, servers MUST treat the receipt of a PUSH_PROMISE
// frame as a connection error (Section 5.4.1) of type PROTOCOL_ERROR.
return ConnectionError(ErrCodeProtocol)
return sc.countError("push_promise", ConnectionError(ErrCodeProtocol))
default:
sc.vlogf("http2: server ignoring frame: %v", f.Header())
return nil
@@ -1445,7 +1453,7 @@ func (sc *serverConn) processPing(f *PingFrame) error {
// identifier field value other than 0x0, the recipient MUST
// respond with a connection error (Section 5.4.1) of type
// PROTOCOL_ERROR."
return ConnectionError(ErrCodeProtocol)
return sc.countError("ping_on_stream", ConnectionError(ErrCodeProtocol))
}
if sc.inGoAway && sc.goAwayCode != ErrCodeNo {
return nil
@@ -1464,7 +1472,7 @@ func (sc *serverConn) processWindowUpdate(f *WindowUpdateFrame) error {
// or PRIORITY on a stream in this state MUST be
// treated as a connection error (Section 5.4.1) of
// type PROTOCOL_ERROR."
return ConnectionError(ErrCodeProtocol)
return sc.countError("stream_idle", ConnectionError(ErrCodeProtocol))
}
if st == nil {
// "WINDOW_UPDATE can be sent by a peer that has sent a
@@ -1475,7 +1483,7 @@ func (sc *serverConn) processWindowUpdate(f *WindowUpdateFrame) error {
return nil
}
if !st.flow.add(int32(f.Increment)) {
return streamError(f.StreamID, ErrCodeFlowControl)
return sc.countError("bad_flow", streamError(f.StreamID, ErrCodeFlowControl))
}
default: // connection-level flow control
if !sc.flow.add(int32(f.Increment)) {
@@ -1496,7 +1504,7 @@ func (sc *serverConn) processResetStream(f *RSTStreamFrame) error {
// identifying an idle stream is received, the
// recipient MUST treat this as a connection error
// (Section 5.4.1) of type PROTOCOL_ERROR.
return ConnectionError(ErrCodeProtocol)
return sc.countError("reset_idle_stream", ConnectionError(ErrCodeProtocol))
}
if st != nil {
st.cancelCtx()
@@ -1548,7 +1556,7 @@ func (sc *serverConn) processSettings(f *SettingsFrame) error {
// Why is the peer ACKing settings we never sent?
// The spec doesn't mention this case, but
// hang up on them anyway.
return ConnectionError(ErrCodeProtocol)
return sc.countError("ack_mystery", ConnectionError(ErrCodeProtocol))
}
return nil
}
@@ -1556,7 +1564,7 @@ func (sc *serverConn) processSettings(f *SettingsFrame) error {
// This isn't actually in the spec, but hang up on
// suspiciously large settings frames or those with
// duplicate entries.
return ConnectionError(ErrCodeProtocol)
return sc.countError("settings_big_or_dups", ConnectionError(ErrCodeProtocol))
}
if err := f.ForeachSetting(sc.processSetting); err != nil {
return err
@@ -1623,7 +1631,7 @@ func (sc *serverConn) processSettingInitialWindowSize(val uint32) error {
// control window to exceed the maximum size as a
// connection error (Section 5.4.1) of type
// FLOW_CONTROL_ERROR."
return ConnectionError(ErrCodeFlowControl)
return sc.countError("setting_win_size", ConnectionError(ErrCodeFlowControl))
}
}
return nil
@@ -1656,7 +1664,7 @@ func (sc *serverConn) processData(f *DataFrame) error {
// or PRIORITY on a stream in this state MUST be
// treated as a connection error (Section 5.4.1) of
// type PROTOCOL_ERROR."
return ConnectionError(ErrCodeProtocol)
return sc.countError("data_on_idle", ConnectionError(ErrCodeProtocol))
}
// "If a DATA frame is received whose stream is not in "open"
@@ -1673,7 +1681,7 @@ func (sc *serverConn) processData(f *DataFrame) error {
// and return any flow control bytes since we're not going
// to consume them.
if sc.inflow.available() < int32(f.Length) {
return streamError(id, ErrCodeFlowControl)
return sc.countError("data_flow", streamError(id, ErrCodeFlowControl))
}
// Deduct the flow control from inflow, since we're
// going to immediately add it back in
@@ -1686,7 +1694,7 @@ func (sc *serverConn) processData(f *DataFrame) error {
// Already have a stream error in flight. Don't send another.
return nil
}
return streamError(id, ErrCodeStreamClosed)
return sc.countError("closed", streamError(id, ErrCodeStreamClosed))
}
if st.body == nil {
panic("internal error: should have a body in this state")
@@ -1698,12 +1706,12 @@ func (sc *serverConn) processData(f *DataFrame) error {
// RFC 7540, sec 8.1.2.6: A request or response is also malformed if the
// value of a content-length header field does not equal the sum of the
// DATA frame payload lengths that form the body.
return streamError(id, ErrCodeProtocol)
return sc.countError("send_too_much", streamError(id, ErrCodeProtocol))
}
if f.Length > 0 {
// Check whether the client has flow control quota.
if st.inflow.available() < int32(f.Length) {
return streamError(id, ErrCodeFlowControl)
return sc.countError("flow_on_data_length", streamError(id, ErrCodeFlowControl))
}
st.inflow.take(int32(f.Length))
@@ -1711,7 +1719,7 @@ func (sc *serverConn) processData(f *DataFrame) error {
wrote, err := st.body.Write(data)
if err != nil {
sc.sendWindowUpdate(nil, int(f.Length)-wrote)
return streamError(id, ErrCodeStreamClosed)
return sc.countError("body_write_err", streamError(id, ErrCodeStreamClosed))
}
if wrote != len(data) {
panic("internal error: bad Writer")
@@ -1797,7 +1805,7 @@ func (sc *serverConn) processHeaders(f *MetaHeadersFrame) error {
// stream identifier MUST respond with a connection error
// (Section 5.4.1) of type PROTOCOL_ERROR.
if id%2 != 1 {
return ConnectionError(ErrCodeProtocol)
return sc.countError("headers_even", ConnectionError(ErrCodeProtocol))
}
// A HEADERS frame can be used to create a new stream or
// send a trailer for an open one. If we already have a stream
@@ -1814,7 +1822,7 @@ func (sc *serverConn) processHeaders(f *MetaHeadersFrame) error {
// this state, it MUST respond with a stream error (Section 5.4.2) of
// type STREAM_CLOSED.
if st.state == stateHalfClosedRemote {
return streamError(id, ErrCodeStreamClosed)
return sc.countError("headers_half_closed", streamError(id, ErrCodeStreamClosed))
}
return st.processTrailerHeaders(f)
}
@@ -1825,7 +1833,7 @@ func (sc *serverConn) processHeaders(f *MetaHeadersFrame) error {
// receives an unexpected stream identifier MUST respond with
// a connection error (Section 5.4.1) of type PROTOCOL_ERROR.
if id <= sc.maxClientStreamID {
return ConnectionError(ErrCodeProtocol)
return sc.countError("stream_went_down", ConnectionError(ErrCodeProtocol))
}
sc.maxClientStreamID = id
@@ -1842,14 +1850,14 @@ func (sc *serverConn) processHeaders(f *MetaHeadersFrame) error {
if sc.curClientStreams+1 > sc.advMaxStreams {
if sc.unackedSettings == 0 {
// They should know better.
return streamError(id, ErrCodeProtocol)
return sc.countError("over_max_streams", streamError(id, ErrCodeProtocol))
}
// Assume it's a network race, where they just haven't
// received our last SETTINGS update. But actually
// this can't happen yet, because we don't yet provide
// a way for users to adjust server parameters at
// runtime.
return streamError(id, ErrCodeRefusedStream)
return sc.countError("over_max_streams_race", streamError(id, ErrCodeRefusedStream))
}
initialState := stateOpen
@@ -1859,7 +1867,7 @@ func (sc *serverConn) processHeaders(f *MetaHeadersFrame) error {
st := sc.newStream(id, 0, initialState)
if f.HasPriority() {
if err := checkPriority(f.StreamID, f.Priority); err != nil {
if err := sc.checkPriority(f.StreamID, f.Priority); err != nil {
return err
}
sc.writeSched.AdjustStream(st.id, f.Priority)
@@ -1903,15 +1911,15 @@ func (st *stream) processTrailerHeaders(f *MetaHeadersFrame) error {
sc := st.sc
sc.serveG.check()
if st.gotTrailerHeader {
return ConnectionError(ErrCodeProtocol)
return sc.countError("dup_trailers", ConnectionError(ErrCodeProtocol))
}
st.gotTrailerHeader = true
if !f.StreamEnded() {
return streamError(st.id, ErrCodeProtocol)
return sc.countError("trailers_not_ended", streamError(st.id, ErrCodeProtocol))
}
if len(f.PseudoFields()) > 0 {
return streamError(st.id, ErrCodeProtocol)
return sc.countError("trailers_pseudo", streamError(st.id, ErrCodeProtocol))
}
if st.trailer != nil {
for _, hf := range f.RegularFields() {
@@ -1920,7 +1928,7 @@ func (st *stream) processTrailerHeaders(f *MetaHeadersFrame) error {
// TODO: send more details to the peer somehow. But http2 has
// no way to send debug data at a stream level. Discuss with
// HTTP folk.
return streamError(st.id, ErrCodeProtocol)
return sc.countError("trailers_bogus", streamError(st.id, ErrCodeProtocol))
}
st.trailer[key] = append(st.trailer[key], hf.Value)
}
@@ -1929,13 +1937,13 @@ func (st *stream) processTrailerHeaders(f *MetaHeadersFrame) error {
return nil
}
func checkPriority(streamID uint32, p PriorityParam) error {
func (sc *serverConn) checkPriority(streamID uint32, p PriorityParam) error {
if streamID == p.StreamDep {
// Section 5.3.1: "A stream cannot depend on itself. An endpoint MUST treat
// this as a stream error (Section 5.4.2) of type PROTOCOL_ERROR."
// Section 5.3.3 says that a stream can depend on one of its dependencies,
// so it's only self-dependencies that are forbidden.
return streamError(streamID, ErrCodeProtocol)
return sc.countError("priority", streamError(streamID, ErrCodeProtocol))
}
return nil
}
@@ -1944,7 +1952,7 @@ func (sc *serverConn) processPriority(f *PriorityFrame) error {
if sc.inGoAway {
return nil
}
if err := checkPriority(f.StreamID, f.PriorityParam); err != nil {
if err := sc.checkPriority(f.StreamID, f.PriorityParam); err != nil {
return err
}
sc.writeSched.AdjustStream(f.StreamID, f.PriorityParam)
@@ -2001,7 +2009,7 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res
isConnect := rp.method == "CONNECT"
if isConnect {
if rp.path != "" || rp.scheme != "" || rp.authority == "" {
return nil, nil, streamError(f.StreamID, ErrCodeProtocol)
return nil, nil, sc.countError("bad_connect", streamError(f.StreamID, ErrCodeProtocol))
}
} else if rp.method == "" || rp.path == "" || (rp.scheme != "https" && rp.scheme != "http") {
// See 8.1.2.6 Malformed Requests and Responses:
@@ -2014,13 +2022,13 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res
// "All HTTP/2 requests MUST include exactly one valid
// value for the :method, :scheme, and :path
// pseudo-header fields"
return nil, nil, streamError(f.StreamID, ErrCodeProtocol)
return nil, nil, sc.countError("bad_path_method", streamError(f.StreamID, ErrCodeProtocol))
}
bodyOpen := !f.StreamEnded()
if rp.method == "HEAD" && bodyOpen {
// HEAD requests can't have bodies
return nil, nil, streamError(f.StreamID, ErrCodeProtocol)
return nil, nil, sc.countError("head_body", streamError(f.StreamID, ErrCodeProtocol))
}
rp.header = make(http.Header)
@@ -2103,7 +2111,7 @@ func (sc *serverConn) newWriterAndRequestNoBody(st *stream, rp requestParam) (*r
var err error
url_, err = url.ParseRequestURI(rp.path)
if err != nil {
return nil, nil, streamError(st.id, ErrCodeProtocol)
return nil, nil, sc.countError("bad_path", streamError(st.id, ErrCodeProtocol))
}
requestURI = rp.path
}
@@ -2789,8 +2797,12 @@ func (w *responseWriter) Push(target string, opts *http.PushOptions) error {
// but PUSH_PROMISE requests cannot have a body.
// http://tools.ietf.org/html/rfc7540#section-8.2
// Also disallow Host, since the promised URL must be absolute.
switch strings.ToLower(k) {
case "content-length", "content-encoding", "trailer", "te", "expect", "host":
if asciiEqualFold(k, "content-length") ||
asciiEqualFold(k, "content-encoding") ||
asciiEqualFold(k, "trailer") ||
asciiEqualFold(k, "te") ||
asciiEqualFold(k, "expect") ||
asciiEqualFold(k, "host") {
return fmt.Errorf("promised request headers cannot include %q", k)
}
}
@@ -2982,3 +2994,31 @@ func h1ServerKeepAlivesDisabled(hs *http.Server) bool {
}
return false
}
func (sc *serverConn) countError(name string, err error) error {
if sc == nil || sc.srv == nil {
return err
}
f := sc.srv.CountError
if f == nil {
return err
}
var typ string
var code ErrCode
switch e := err.(type) {
case ConnectionError:
typ = "conn"
code = ErrCode(e)
case StreamError:
typ = "stream"
code = ErrCode(e.Code)
default:
return err
}
codeStr := errCodeName[code]
if codeStr == "" {
codeStr = strconv.Itoa(int(code))
}
f(fmt.Sprintf("%s_%s_%s", typ, codeStr, name))
return err
}
+934 -686
View File
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -341,7 +341,12 @@ func encodeHeaders(enc *hpack.Encoder, h http.Header, keys []string) {
}
for _, k := range keys {
vv := h[k]
k = lowerHeader(k)
k, ascii := lowerHeader(k)
if !ascii {
// Skip writing invalid headers. Per RFC 7540, Section 8.1.2, header
// field names have to be ASCII characters (just as in HTTP/1.x).
continue
}
if !validWireHeaderFieldName(k) {
// Skip it as backup paranoia. Per
// golang.org/issue/14048, these should
+14
View File
@@ -0,0 +1,14 @@
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.18
// +build go1.18
package idna
// Transitional processing is disabled by default in Go 1.18.
// https://golang.org/issue/47510
const transitionalLookup = false
+76 -41
View File
@@ -59,23 +59,22 @@ type Option func(*options)
// Transitional sets a Profile to use the Transitional mapping as defined in UTS
// #46. This will cause, for example, "ß" to be mapped to "ss". Using the
// transitional mapping provides a compromise between IDNA2003 and IDNA2008
// compatibility. It is used by most browsers when resolving domain names. This
// compatibility. It is used by some browsers when resolving domain names. This
// option is only meaningful if combined with MapForLookup.
func Transitional(transitional bool) Option {
return func(o *options) { o.transitional = true }
return func(o *options) { o.transitional = transitional }
}
// VerifyDNSLength sets whether a Profile should fail if any of the IDN parts
// are longer than allowed by the RFC.
//
// This option corresponds to the VerifyDnsLength flag in UTS #46.
func VerifyDNSLength(verify bool) Option {
return func(o *options) { o.verifyDNSLength = verify }
}
// RemoveLeadingDots removes leading label separators. Leading runes that map to
// dots, such as U+3002 IDEOGRAPHIC FULL STOP, are removed as well.
//
// This is the behavior suggested by the UTS #46 and is adopted by some
// browsers.
func RemoveLeadingDots(remove bool) Option {
return func(o *options) { o.removeLeadingDots = remove }
}
@@ -83,6 +82,8 @@ func RemoveLeadingDots(remove bool) Option {
// ValidateLabels sets whether to check the mandatory label validation criteria
// as defined in Section 5.4 of RFC 5891. This includes testing for correct use
// of hyphens ('-'), normalization, validity of runes, and the context rules.
// In particular, ValidateLabels also sets the CheckHyphens and CheckJoiners flags
// in UTS #46.
func ValidateLabels(enable bool) Option {
return func(o *options) {
// Don't override existing mappings, but set one that at least checks
@@ -91,25 +92,48 @@ func ValidateLabels(enable bool) Option {
o.mapping = normalize
}
o.trie = trie
o.validateLabels = enable
o.fromPuny = validateFromPunycode
o.checkJoiners = enable
o.checkHyphens = enable
if enable {
o.fromPuny = validateFromPunycode
} else {
o.fromPuny = nil
}
}
}
// CheckHyphens sets whether to check for correct use of hyphens ('-') in
// labels. Most web browsers do not have this option set, since labels such as
// "r3---sn-apo3qvuoxuxbt-j5pe" are in common use.
//
// This option corresponds to the CheckHyphens flag in UTS #46.
func CheckHyphens(enable bool) Option {
return func(o *options) { o.checkHyphens = enable }
}
// CheckJoiners sets whether to check the ContextJ rules as defined in Appendix
// A of RFC 5892, concerning the use of joiner runes.
//
// This option corresponds to the CheckJoiners flag in UTS #46.
func CheckJoiners(enable bool) Option {
return func(o *options) {
o.trie = trie
o.checkJoiners = enable
}
}
// StrictDomainName limits the set of permissible ASCII characters to those
// allowed in domain names as defined in RFC 1034 (A-Z, a-z, 0-9 and the
// hyphen). This is set by default for MapForLookup and ValidateForRegistration.
// hyphen). This is set by default for MapForLookup and ValidateForRegistration,
// but is only useful if ValidateLabels is set.
//
// This option is useful, for instance, for browsers that allow characters
// outside this range, for example a '_' (U+005F LOW LINE). See
// http://www.rfc-editor.org/std/std3.txt for more details This option
// corresponds to the UseSTD3ASCIIRules option in UTS #46.
// http://www.rfc-editor.org/std/std3.txt for more details.
//
// This option corresponds to the UseSTD3ASCIIRules flag in UTS #46.
func StrictDomainName(use bool) Option {
return func(o *options) {
o.trie = trie
o.useSTD3Rules = use
o.fromPuny = validateFromPunycode
}
return func(o *options) { o.useSTD3Rules = use }
}
// NOTE: the following options pull in tables. The tables should not be linked
@@ -117,6 +141,8 @@ func StrictDomainName(use bool) Option {
// BidiRule enables the Bidi rule as defined in RFC 5893. Any application
// that relies on proper validation of labels should include this rule.
//
// This option corresponds to the CheckBidi flag in UTS #46.
func BidiRule() Option {
return func(o *options) { o.bidirule = bidirule.ValidString }
}
@@ -152,7 +178,8 @@ func MapForLookup() Option {
type options struct {
transitional bool
useSTD3Rules bool
validateLabels bool
checkHyphens bool
checkJoiners bool
verifyDNSLength bool
removeLeadingDots bool
@@ -225,8 +252,11 @@ func (p *Profile) String() string {
if p.useSTD3Rules {
s += ":UseSTD3Rules"
}
if p.validateLabels {
s += ":ValidateLabels"
if p.checkHyphens {
s += ":CheckHyphens"
}
if p.checkJoiners {
s += ":CheckJoiners"
}
if p.verifyDNSLength {
s += ":VerifyDNSLength"
@@ -254,26 +284,29 @@ var (
punycode = &Profile{}
lookup = &Profile{options{
transitional: true,
useSTD3Rules: true,
validateLabels: true,
trie: trie,
fromPuny: validateFromPunycode,
mapping: validateAndMap,
bidirule: bidirule.ValidString,
transitional: transitionalLookup,
useSTD3Rules: true,
checkHyphens: true,
checkJoiners: true,
trie: trie,
fromPuny: validateFromPunycode,
mapping: validateAndMap,
bidirule: bidirule.ValidString,
}}
display = &Profile{options{
useSTD3Rules: true,
validateLabels: true,
trie: trie,
fromPuny: validateFromPunycode,
mapping: validateAndMap,
bidirule: bidirule.ValidString,
useSTD3Rules: true,
checkHyphens: true,
checkJoiners: true,
trie: trie,
fromPuny: validateFromPunycode,
mapping: validateAndMap,
bidirule: bidirule.ValidString,
}}
registration = &Profile{options{
useSTD3Rules: true,
validateLabels: true,
verifyDNSLength: true,
checkHyphens: true,
checkJoiners: true,
trie: trie,
fromPuny: validateFromPunycode,
mapping: validateRegistration,
@@ -340,7 +373,7 @@ func (p *Profile) process(s string, toASCII bool) (string, error) {
}
isBidi = isBidi || bidirule.DirectionString(u) != bidi.LeftToRight
labels.set(u)
if err == nil && p.validateLabels {
if err == nil && p.fromPuny != nil {
err = p.fromPuny(p, u)
}
if err == nil {
@@ -681,16 +714,18 @@ func (p *Profile) validateLabel(s string) (err error) {
}
return nil
}
if !p.validateLabels {
if p.checkHyphens {
if len(s) > 4 && s[2] == '-' && s[3] == '-' {
return &labelError{s, "V2"}
}
if s[0] == '-' || s[len(s)-1] == '-' {
return &labelError{s, "V3"}
}
}
if !p.checkJoiners {
return nil
}
trie := p.trie // p.validateLabels is only set if trie is set.
if len(s) > 4 && s[2] == '-' && s[3] == '-' {
return &labelError{s, "V2"}
}
if s[0] == '-' || s[len(s)-1] == '-' {
return &labelError{s, "V3"}
}
trie := p.trie // p.checkJoiners is only set if trie is set.
// TODO: merge the use of this in the trie.
v, sz := trie.lookupString(s)
x := info(v)
+66 -31
View File
@@ -58,23 +58,22 @@ type Option func(*options)
// Transitional sets a Profile to use the Transitional mapping as defined in UTS
// #46. This will cause, for example, "ß" to be mapped to "ss". Using the
// transitional mapping provides a compromise between IDNA2003 and IDNA2008
// compatibility. It is used by most browsers when resolving domain names. This
// compatibility. It is used by some browsers when resolving domain names. This
// option is only meaningful if combined with MapForLookup.
func Transitional(transitional bool) Option {
return func(o *options) { o.transitional = true }
return func(o *options) { o.transitional = transitional }
}
// VerifyDNSLength sets whether a Profile should fail if any of the IDN parts
// are longer than allowed by the RFC.
//
// This option corresponds to the VerifyDnsLength flag in UTS #46.
func VerifyDNSLength(verify bool) Option {
return func(o *options) { o.verifyDNSLength = verify }
}
// RemoveLeadingDots removes leading label separators. Leading runes that map to
// dots, such as U+3002 IDEOGRAPHIC FULL STOP, are removed as well.
//
// This is the behavior suggested by the UTS #46 and is adopted by some
// browsers.
func RemoveLeadingDots(remove bool) Option {
return func(o *options) { o.removeLeadingDots = remove }
}
@@ -82,6 +81,8 @@ func RemoveLeadingDots(remove bool) Option {
// ValidateLabels sets whether to check the mandatory label validation criteria
// as defined in Section 5.4 of RFC 5891. This includes testing for correct use
// of hyphens ('-'), normalization, validity of runes, and the context rules.
// In particular, ValidateLabels also sets the CheckHyphens and CheckJoiners flags
// in UTS #46.
func ValidateLabels(enable bool) Option {
return func(o *options) {
// Don't override existing mappings, but set one that at least checks
@@ -90,25 +91,48 @@ func ValidateLabels(enable bool) Option {
o.mapping = normalize
}
o.trie = trie
o.validateLabels = enable
o.fromPuny = validateFromPunycode
o.checkJoiners = enable
o.checkHyphens = enable
if enable {
o.fromPuny = validateFromPunycode
} else {
o.fromPuny = nil
}
}
}
// CheckHyphens sets whether to check for correct use of hyphens ('-') in
// labels. Most web browsers do not have this option set, since labels such as
// "r3---sn-apo3qvuoxuxbt-j5pe" are in common use.
//
// This option corresponds to the CheckHyphens flag in UTS #46.
func CheckHyphens(enable bool) Option {
return func(o *options) { o.checkHyphens = enable }
}
// CheckJoiners sets whether to check the ContextJ rules as defined in Appendix
// A of RFC 5892, concerning the use of joiner runes.
//
// This option corresponds to the CheckJoiners flag in UTS #46.
func CheckJoiners(enable bool) Option {
return func(o *options) {
o.trie = trie
o.checkJoiners = enable
}
}
// StrictDomainName limits the set of permissable ASCII characters to those
// allowed in domain names as defined in RFC 1034 (A-Z, a-z, 0-9 and the
// hyphen). This is set by default for MapForLookup and ValidateForRegistration.
// hyphen). This is set by default for MapForLookup and ValidateForRegistration,
// but is only useful if ValidateLabels is set.
//
// This option is useful, for instance, for browsers that allow characters
// outside this range, for example a '_' (U+005F LOW LINE). See
// http://www.rfc-editor.org/std/std3.txt for more details This option
// corresponds to the UseSTD3ASCIIRules option in UTS #46.
// http://www.rfc-editor.org/std/std3.txt for more details.
//
// This option corresponds to the UseSTD3ASCIIRules flag in UTS #46.
func StrictDomainName(use bool) Option {
return func(o *options) {
o.trie = trie
o.useSTD3Rules = use
o.fromPuny = validateFromPunycode
}
return func(o *options) { o.useSTD3Rules = use }
}
// NOTE: the following options pull in tables. The tables should not be linked
@@ -116,6 +140,8 @@ func StrictDomainName(use bool) Option {
// BidiRule enables the Bidi rule as defined in RFC 5893. Any application
// that relies on proper validation of labels should include this rule.
//
// This option corresponds to the CheckBidi flag in UTS #46.
func BidiRule() Option {
return func(o *options) { o.bidirule = bidirule.ValidString }
}
@@ -152,7 +178,8 @@ func MapForLookup() Option {
type options struct {
transitional bool
useSTD3Rules bool
validateLabels bool
checkHyphens bool
checkJoiners bool
verifyDNSLength bool
removeLeadingDots bool
@@ -225,8 +252,11 @@ func (p *Profile) String() string {
if p.useSTD3Rules {
s += ":UseSTD3Rules"
}
if p.validateLabels {
s += ":ValidateLabels"
if p.checkHyphens {
s += ":CheckHyphens"
}
if p.checkJoiners {
s += ":CheckJoiners"
}
if p.verifyDNSLength {
s += ":VerifyDNSLength"
@@ -255,9 +285,10 @@ var (
punycode = &Profile{}
lookup = &Profile{options{
transitional: true,
useSTD3Rules: true,
validateLabels: true,
removeLeadingDots: true,
useSTD3Rules: true,
checkHyphens: true,
checkJoiners: true,
trie: trie,
fromPuny: validateFromPunycode,
mapping: validateAndMap,
@@ -265,8 +296,9 @@ var (
}}
display = &Profile{options{
useSTD3Rules: true,
validateLabels: true,
removeLeadingDots: true,
checkHyphens: true,
checkJoiners: true,
trie: trie,
fromPuny: validateFromPunycode,
mapping: validateAndMap,
@@ -274,8 +306,9 @@ var (
}}
registration = &Profile{options{
useSTD3Rules: true,
validateLabels: true,
verifyDNSLength: true,
checkHyphens: true,
checkJoiners: true,
trie: trie,
fromPuny: validateFromPunycode,
mapping: validateRegistration,
@@ -339,7 +372,7 @@ func (p *Profile) process(s string, toASCII bool) (string, error) {
continue
}
labels.set(u)
if err == nil && p.validateLabels {
if err == nil && p.fromPuny != nil {
err = p.fromPuny(p, u)
}
if err == nil {
@@ -629,16 +662,18 @@ func (p *Profile) validateLabel(s string) error {
if p.bidirule != nil && !p.bidirule(s) {
return &labelError{s, "B"}
}
if !p.validateLabels {
if p.checkHyphens {
if len(s) > 4 && s[2] == '-' && s[3] == '-' {
return &labelError{s, "V2"}
}
if s[0] == '-' || s[len(s)-1] == '-' {
return &labelError{s, "V3"}
}
}
if !p.checkJoiners {
return nil
}
trie := p.trie // p.validateLabels is only set if trie is set.
if len(s) > 4 && s[2] == '-' && s[3] == '-' {
return &labelError{s, "V2"}
}
if s[0] == '-' || s[len(s)-1] == '-' {
return &labelError{s, "V3"}
}
trie := p.trie // p.checkJoiners is only set if trie is set.
// TODO: merge the use of this in the trie.
v, sz := trie.lookupString(s)
x := info(v)

Some files were not shown because too many files have changed in this diff Show More