Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create opts package #316

Merged
merged 3 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions x/sockopt/sockopt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2024 The Outline Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package sockopt provides cross-platform ways to interact with socket options.
package sockopt

import (
"fmt"
"net"
"net/netip"

"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
)

// HasHopLimit enables manipulation of the hop limit option.
type HasHopLimit interface {
// HopLimit returns the hop limit field value for outgoing packets.
HopLimit() (int, error)
// SetHopLimit sets the hop limit field value for future outgoing packets.
SetHopLimit(hoplim int) error
}

// hopLimitOption implements HasHopLimit.
type hopLimitOption struct {
hopLimit func() (int, error)
setHopLimit func(hoplim int) error
}

func (o *hopLimitOption) HopLimit() (int, error) {
return o.hopLimit()
}

func (o *hopLimitOption) SetHopLimit(hoplim int) error {
return o.setHopLimit(hoplim)
}

var _ HasHopLimit = (*hopLimitOption)(nil)

// TCPOptions represents options for TCP connections.
type TCPOptions interface {
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved
HasHopLimit
}

type tcpOptions struct {
hopLimitOption
}

var _ TCPOptions = (*tcpOptions)(nil)

// newHopLimit creates a hopLimitOption from a [net.Conn]. Works for both TCP or UDP.
func newHopLimit(conn net.Conn) (*hopLimitOption, error) {
addr, err := netip.ParseAddrPort(conn.LocalAddr().String())
if err != nil {
return nil, err
}
opt := &hopLimitOption{}
switch {
case addr.Addr().Is4():
conn := ipv4.NewConn(conn)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we create a new conn here, is it the same as the old conn (passed in through the parameter)? What are the relationship between them?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ipv4/ipv6 packages have this Conn abstraction that is only used to set options. It's not technically a connection, more like an options interface:

I've renamed the local one to ipConn to avoid confusions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, it wraps around an existing fd, though the source code is actually located in the net package rather than the x/net: https://cs.opensource.google/go/go/+/master:src/net/rawconn.go;l=79;drc=9088883cf4a5181cf796c968bbce5a5bc3edc7ab

opt.hopLimit = conn.TTL
opt.setHopLimit = conn.SetTTL
case addr.Addr().Is6():
conn := ipv6.NewConn(conn)
opt.hopLimit = conn.HopLimit
opt.setHopLimit = conn.SetHopLimit
default:
return nil, fmt.Errorf("address is not IPv4 or IPv6 (%v)", addr.Addr().String())
}
return opt, nil
}

// NewTCPOptions creates a [TCPOptions] for the given [net.TCPConn].
func NewTCPOptions(conn *net.TCPConn) (TCPOptions, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have to accept a TCPConn? Can we just accept a net.Conn and returns error if it's not a TCPConn?

Copy link
Contributor Author

@fortuna fortuna Nov 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it doesn't accept every type of net.Conn. I would rather be explicit about it.

Also, options for UDP may differ from options for TCP.

hopLimit, err := newHopLimit(conn)
if err != nil {
return nil, err
}
return &tcpOptions{hopLimitOption: *hopLimit}, nil
}
54 changes: 54 additions & 0 deletions x/sockopt/sockopt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2024 The Outline Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package sockopt

import (
"net"
"testing"

"github.com/stretchr/testify/require"
)

func TestTCPOptions(t *testing.T) {
type Params struct {
Net string
Addr string
}
for _, params := range []Params{{Net: "tcp4", Addr: "127.0.0.1:0"}, {Net: "tcp6", Addr: "[::1]:0"}} {
l, err := net.Listen(params.Net, params.Addr)
require.NoError(t, err)
defer l.Close()

conn, err := net.Dial("tcp", l.Addr().String())
require.NoError(t, err)
tcpConn, ok := conn.(*net.TCPConn)
require.True(t, ok)

opts, err := NewTCPOptions(tcpConn)
require.NoError(t, err)

require.NoError(t, opts.SetHopLimit(1))

hoplim, err := opts.HopLimit()
require.NoError(t, err)
require.Equal(t, 1, hoplim)

require.NoError(t, opts.SetHopLimit(20))

hoplim, err = opts.HopLimit()
require.NoError(t, err)
require.Equal(t, 20, hoplim)
}
}
Loading