github.com/astaguna/popon-core@v0.0.0-20231019235610-96e42d76a5ff/psiphon/UDPConn.go (about)

     1  /*
     2   * Copyright (c) 2018, Psiphon Inc.
     3   * All rights reserved.
     4   *
     5   * This program is free software: you can redistribute it and/or modify
     6   * it under the terms of the GNU General Public License as published by
     7   * the Free Software Foundation, either version 3 of the License, or
     8   * (at your option) any later version.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package psiphon
    21  
    22  import (
    23  	"context"
    24  	"math/rand"
    25  	"net"
    26  	"strconv"
    27  	"syscall"
    28  
    29  	"github.com/astaguna/popon-core/psiphon/common/errors"
    30  )
    31  
    32  // NewUDPConn resolves addr and configures a new *net.UDPConn. The UDP socket
    33  // is created using options in DialConfig, including DeviceBinder. The
    34  // returned UDPAddr uses DialConfig options IPv6Synthesizer and
    35  // ResolvedIPCallback.
    36  //
    37  // The UDP conn is not dialed; it is intended for use with WriteTo using the
    38  // returned UDPAddr, not Write.
    39  //
    40  // The returned conn is not a common.Closer; the caller is expected to wrap
    41  // this conn with another higher-level conn that provides that interface.
    42  func NewUDPConn(
    43  	ctx context.Context, addr string, config *DialConfig) (net.PacketConn, *net.UDPAddr, error) {
    44  
    45  	host, strPort, err := net.SplitHostPort(addr)
    46  	if err != nil {
    47  		return nil, nil, errors.Trace(err)
    48  	}
    49  	port, err := strconv.Atoi(strPort)
    50  	if err != nil {
    51  		return nil, nil, errors.Trace(err)
    52  	}
    53  
    54  	if port <= 0 || port >= 65536 {
    55  		return nil, nil, errors.Tracef("invalid destination port: %d", port)
    56  	}
    57  
    58  	if config.ResolveIP == nil {
    59  		// Fail even if we don't need a resolver for this dial: this is a code
    60  		// misconfiguration.
    61  		return nil, nil, errors.TraceNew("missing resolver")
    62  	}
    63  	ipAddrs, err := config.ResolveIP(ctx, host)
    64  	if err != nil {
    65  		return nil, nil, errors.Trace(err)
    66  	}
    67  	if len(ipAddrs) < 1 {
    68  		return nil, nil, errors.TraceNew("no IP address")
    69  	}
    70  
    71  	ipAddr := ipAddrs[rand.Intn(len(ipAddrs))]
    72  
    73  	if config.IPv6Synthesizer != nil {
    74  		if ipAddr.To4() != nil {
    75  			synthesizedIPAddress := config.IPv6Synthesizer.IPv6Synthesize(ipAddr.String())
    76  			if synthesizedIPAddress != "" {
    77  				synthesizedAddr := net.ParseIP(synthesizedIPAddress)
    78  				if synthesizedAddr != nil {
    79  					ipAddr = synthesizedAddr
    80  				}
    81  			}
    82  		}
    83  	}
    84  
    85  	listen := &net.ListenConfig{
    86  		Control: func(_, _ string, c syscall.RawConn) error {
    87  			var controlErr error
    88  			err := c.Control(func(fd uintptr) {
    89  
    90  				socketFD := int(fd)
    91  
    92  				setAdditionalSocketOptions(socketFD)
    93  
    94  				if config.BPFProgramInstructions != nil {
    95  					err := setSocketBPF(config.BPFProgramInstructions, socketFD)
    96  					if err != nil {
    97  						controlErr = errors.Tracef("setSocketBPF failed: %s", err)
    98  						return
    99  					}
   100  				}
   101  
   102  				if config.DeviceBinder != nil {
   103  					_, err := config.DeviceBinder.BindToDevice(socketFD)
   104  					if err != nil {
   105  						controlErr = errors.Tracef("BindToDevice failed: %s", err)
   106  						return
   107  					}
   108  				}
   109  			})
   110  			if controlErr != nil {
   111  				return errors.Trace(controlErr)
   112  			}
   113  			return errors.Trace(err)
   114  		},
   115  	}
   116  
   117  	network := "udp4"
   118  	if ipAddr.To4() == nil {
   119  		network = "udp6"
   120  	}
   121  
   122  	// It's necessary to create an "unconnected" UDP socket, for use with
   123  	// WriteTo, as required by quic-go. As documented in net.ListenUDP: with
   124  	// an unspecified IP address, the resulting conn "listens on all
   125  	// available IP addresses of the local system except multicast IP
   126  	// addresses".
   127  	//
   128  	// Limitation: these UDP sockets are not necessarily closed when a device
   129  	// changes active network (e.g., WiFi to mobile). It's possible that a
   130  	// QUIC connection does not immediately close on a network change, and
   131  	// instead outbound packets are sent from a different active interface.
   132  	// As quic-go does not yet support connection migration, these packets
   133  	// will be dropped by the server. This situation is mitigated by use of
   134  	// DeviceBinder; by network change event detection, which initiates new
   135  	// tunnel connections; and by timeouts/keep-alives.
   136  
   137  	conn, err := listen.ListenPacket(ctx, network, "")
   138  	if err != nil {
   139  		return nil, nil, errors.Trace(err)
   140  	}
   141  
   142  	udpConn, ok := conn.(*net.UDPConn)
   143  	if !ok {
   144  		return nil, nil, errors.Tracef("unexpected conn type: %T", conn)
   145  	}
   146  
   147  	if config.ResolvedIPCallback != nil {
   148  		config.ResolvedIPCallback(ipAddr.String())
   149  	}
   150  
   151  	return udpConn, &net.UDPAddr{IP: ipAddr, Port: port}, nil
   152  }