Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0b1c4d27f |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -59,7 +59,7 @@ jobs:
|
||||
- name: Run tests in test container
|
||||
run: |
|
||||
touch coverage.txt
|
||||
docker run --rm --device /dev/net/tun \
|
||||
docker run --rm \
|
||||
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
||||
test-container
|
||||
|
||||
|
||||
2
.github/workflows/markdown.yml
vendored
2
.github/workflows/markdown.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v18
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v16
|
||||
with:
|
||||
globs: "**.md"
|
||||
config: .markdownlint.json
|
||||
|
||||
@@ -125,8 +125,6 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
||||
VPN_PORT_FORWARDING_USERNAME= \
|
||||
VPN_PORT_FORWARDING_PASSWORD= \
|
||||
VPN_PORT_FORWARDING_UP_COMMAND= \
|
||||
VPN_PORT_FORWARDING_DOWN_COMMAND= \
|
||||
# # Cyberghost only:
|
||||
OPENVPN_CERT= \
|
||||
OPENVPN_KEY= \
|
||||
|
||||
@@ -380,7 +380,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
|
||||
portForwardLogger := logger.New(log.SetComponent("port forwarding"))
|
||||
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
|
||||
routingConf, httpClient, firewallConf, portForwardLogger, cmder, puid, pgid)
|
||||
routingConf, httpClient, firewallConf, portForwardLogger, puid, pgid)
|
||||
portForwardRunError, err := portForwardLooper.Start(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting port forwarding loop: %w", err)
|
||||
|
||||
14
go.mod
14
go.mod
@@ -3,27 +3,27 @@ module github.com/qdm12/gluetun
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/breml/rootcerts v0.2.19
|
||||
github.com/breml/rootcerts v0.2.18
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/klauspost/compress v1.17.11
|
||||
github.com/klauspost/pgzip v1.2.6
|
||||
github.com/pelletier/go-toml/v2 v2.2.3
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc8
|
||||
github.com/qdm12/gosettings v0.4.4
|
||||
github.com/qdm12/gosettings v0.4.3
|
||||
github.com/qdm12/goshutdown v0.3.0
|
||||
github.com/qdm12/gosplash v0.2.0
|
||||
github.com/qdm12/gotree v0.3.0
|
||||
github.com/qdm12/log v0.1.0
|
||||
github.com/qdm12/ss-server v0.6.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/ulikunitz/xz v0.5.11
|
||||
github.com/vishvananda/netlink v1.2.1
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
||||
golang.org/x/net v0.31.0
|
||||
golang.org/x/net v0.30.0
|
||||
golang.org/x/sys v0.27.0
|
||||
golang.org/x/text v0.20.0
|
||||
golang.org/x/text v0.19.0
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
@@ -50,9 +50,9 @@ require (
|
||||
github.com/qdm12/goservices v0.1.0 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
golang.org/x/crypto v0.29.0 // indirect
|
||||
golang.org/x/crypto v0.28.0 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/sync v0.9.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
|
||||
28
go.sum
28
go.sum
@@ -1,7 +1,7 @@
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/breml/rootcerts v0.2.19 h1:3D/qwAC1xoh82GmZ21mYzQ1NaLOICUVntIo+MRZYr4U=
|
||||
github.com/breml/rootcerts v0.2.19/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
||||
github.com/breml/rootcerts v0.2.18 h1:KjZaNT7AX/akUjzpStuwTMQs42YHlPyc6NmdwShVba0=
|
||||
github.com/breml/rootcerts v0.2.18/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -57,8 +57,8 @@ github.com/qdm12/dns/v2 v2.0.0-rc8 h1:kbgKPkbT+79nScfuZ0ZcVhksTGo8IUqQ8TTQGnQlZ1
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc8/go.mod h1:VaF02KWEL7xNV4oKfG4N9nEv/kR6bqyIcBReCV5NJhw=
|
||||
github.com/qdm12/goservices v0.1.0 h1:9sODefm/yuIGS7ynCkEnNlMTAYn9GzPhtcK4F69JWvc=
|
||||
github.com/qdm12/goservices v0.1.0/go.mod h1:/JOFsAnHFiSjyoXxa5FlfX903h20K5u/3rLzCjYVMck=
|
||||
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
|
||||
github.com/qdm12/gosettings v0.4.4/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
|
||||
github.com/qdm12/gosettings v0.4.3 h1:oGAjiKVtml9oHVlPQo6H3yk6TmtWpVYicNeGFcM7AP8=
|
||||
github.com/qdm12/gosettings v0.4.3/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
|
||||
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
|
||||
github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
|
||||
github.com/qdm12/gosplash v0.2.0 h1:DOxCEizbW6ZG+FgpH2oK1atT6bM8MHL9GZ2ywSS4zZY=
|
||||
@@ -73,8 +73,8 @@ github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/vishvananda/netlink v1.2.1 h1:pfLv/qlJUwOTPvtWREA7c3PI4u81YkqZw1DYhI2HmLA=
|
||||
@@ -87,8 +87,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
@@ -97,12 +97,12 @@ golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -117,8 +117,8 @@ golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
|
||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCommandEmpty = errors.New("command is empty")
|
||||
ErrSingleQuoteUnterminated = errors.New("unterminated single-quoted string")
|
||||
ErrDoubleQuoteUnterminated = errors.New("unterminated double-quoted string")
|
||||
ErrEscapeUnterminated = errors.New("unterminated backslash-escape")
|
||||
)
|
||||
|
||||
// Split splits a command string into a slice of arguments.
|
||||
// This is especially important for commands such as:
|
||||
// /bin/sh -c "echo hello"
|
||||
// which should be split into: ["/bin/sh", "-c", "echo hello"]
|
||||
// It supports backslash-escapes, single-quotes and double-quotes.
|
||||
// It does not support:
|
||||
// - the $" quoting style.
|
||||
// - expansion (brace, shell or pathname).
|
||||
func Split(command string) (words []string, err error) {
|
||||
if command == "" {
|
||||
return nil, fmt.Errorf("%w", ErrCommandEmpty)
|
||||
}
|
||||
|
||||
const bufferSize = 1024
|
||||
buffer := bytes.NewBuffer(make([]byte, bufferSize))
|
||||
|
||||
startIndex := 0
|
||||
|
||||
for startIndex < len(command) {
|
||||
// skip any split characters at the start
|
||||
character, runeSize := utf8.DecodeRuneInString(command[startIndex:])
|
||||
switch {
|
||||
case strings.ContainsRune(" \n\t", character):
|
||||
startIndex += runeSize
|
||||
case character == '\\':
|
||||
// Look ahead to eventually skip an escaped newline
|
||||
if command[startIndex+runeSize:] == "" {
|
||||
return nil, fmt.Errorf("%w: %q", ErrEscapeUnterminated, command)
|
||||
}
|
||||
character, runeSize := utf8.DecodeRuneInString(command[startIndex+runeSize:])
|
||||
if character == '\n' {
|
||||
startIndex += runeSize + runeSize // backslash and newline
|
||||
}
|
||||
default:
|
||||
var word string
|
||||
buffer.Reset()
|
||||
word, startIndex, err = splitWord(command, startIndex, buffer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("splitting word in %q: %w", command, err)
|
||||
}
|
||||
words = append(words, word)
|
||||
}
|
||||
}
|
||||
return words, nil
|
||||
}
|
||||
|
||||
// WARNING: buffer must be cleared before calling this function.
|
||||
func splitWord(input string, startIndex int, buffer *bytes.Buffer) (
|
||||
word string, newStartIndex int, err error,
|
||||
) {
|
||||
cursor := startIndex
|
||||
for cursor < len(input) {
|
||||
character, runeLength := utf8.DecodeRuneInString(input[cursor:])
|
||||
cursor += runeLength
|
||||
if character == '"' ||
|
||||
character == '\'' ||
|
||||
character == '\\' ||
|
||||
character == ' ' ||
|
||||
character == '\n' ||
|
||||
character == '\t' {
|
||||
buffer.WriteString(input[startIndex : cursor-runeLength])
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.ContainsRune(" \n\t", character): // spacing character
|
||||
return buffer.String(), cursor, nil
|
||||
case character == '"':
|
||||
return handleDoubleQuoted(input, cursor, buffer)
|
||||
case character == '\'':
|
||||
return handleSingleQuoted(input, cursor, buffer)
|
||||
case character == '\\':
|
||||
return handleEscaped(input, cursor, buffer)
|
||||
}
|
||||
}
|
||||
|
||||
buffer.WriteString(input[startIndex:])
|
||||
return buffer.String(), len(input), nil
|
||||
}
|
||||
|
||||
func handleDoubleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
|
||||
word string, newStartIndex int, err error,
|
||||
) {
|
||||
cursor := startIndex
|
||||
for cursor < len(input) {
|
||||
nextCharacter, nextRuneLength := utf8.DecodeRuneInString(input[cursor:])
|
||||
cursor += nextRuneLength
|
||||
switch nextCharacter {
|
||||
case '"': // end of the double quoted string
|
||||
buffer.WriteString(input[startIndex : cursor-nextRuneLength])
|
||||
return splitWord(input, cursor, buffer)
|
||||
case '\\': // escaped character
|
||||
escapedCharacter, escapedRuneLength := utf8.DecodeRuneInString(input[cursor:])
|
||||
cursor += escapedRuneLength
|
||||
if !strings.ContainsRune("$`\"\n\\", escapedCharacter) {
|
||||
break
|
||||
}
|
||||
buffer.WriteString(input[startIndex : cursor-nextRuneLength-escapedRuneLength])
|
||||
if escapedCharacter != '\n' {
|
||||
// skip backslash entirely for the newline character
|
||||
buffer.WriteRune(escapedCharacter)
|
||||
}
|
||||
startIndex = cursor
|
||||
}
|
||||
}
|
||||
return "", 0, fmt.Errorf("%w", ErrDoubleQuoteUnterminated)
|
||||
}
|
||||
|
||||
func handleSingleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
|
||||
word string, newStartIndex int, err error,
|
||||
) {
|
||||
closingQuoteIndex := strings.IndexRune(input[startIndex:], '\'')
|
||||
if closingQuoteIndex == -1 {
|
||||
return "", 0, fmt.Errorf("%w", ErrSingleQuoteUnterminated)
|
||||
}
|
||||
buffer.WriteString(input[startIndex : startIndex+closingQuoteIndex])
|
||||
const singleQuoteRuneLength = 1
|
||||
startIndex += closingQuoteIndex + singleQuoteRuneLength
|
||||
return splitWord(input, startIndex, buffer)
|
||||
}
|
||||
|
||||
func handleEscaped(input string, startIndex int, buffer *bytes.Buffer) (
|
||||
word string, newStartIndex int, err error,
|
||||
) {
|
||||
if input[startIndex:] == "" {
|
||||
return "", 0, fmt.Errorf("%w", ErrEscapeUnterminated)
|
||||
}
|
||||
character, runeLength := utf8.DecodeRuneInString(input[startIndex:])
|
||||
if character != '\n' { // backslash-escaped newline is ignored
|
||||
buffer.WriteString(input[startIndex : startIndex+runeLength])
|
||||
}
|
||||
startIndex += runeLength
|
||||
return splitWord(input, startIndex, buffer)
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Split(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
command string
|
||||
words []string
|
||||
errWrapped error
|
||||
errMessage string
|
||||
}{
|
||||
"empty": {
|
||||
command: "",
|
||||
errWrapped: ErrCommandEmpty,
|
||||
errMessage: "command is empty",
|
||||
},
|
||||
"concrete_sh_command": {
|
||||
command: `/bin/sh -c "echo 123"`,
|
||||
words: []string{"/bin/sh", "-c", "echo 123"},
|
||||
},
|
||||
"single_word": {
|
||||
command: "word1",
|
||||
words: []string{"word1"},
|
||||
},
|
||||
"two_words_single_space": {
|
||||
command: "word1 word2",
|
||||
words: []string{"word1", "word2"},
|
||||
},
|
||||
"two_words_multiple_space": {
|
||||
command: "word1 word2",
|
||||
words: []string{"word1", "word2"},
|
||||
},
|
||||
"two_words_no_expansion": {
|
||||
command: "word1* word2?",
|
||||
words: []string{"word1*", "word2?"},
|
||||
},
|
||||
"escaped_single quote": {
|
||||
command: "ain\\'t good",
|
||||
words: []string{"ain't", "good"},
|
||||
},
|
||||
"escaped_single_quote_all_single_quoted": {
|
||||
command: "'ain'\\''t good'",
|
||||
words: []string{"ain't good"},
|
||||
},
|
||||
"empty_single_quoted": {
|
||||
command: "word1 '' word2",
|
||||
words: []string{"word1", "", "word2"},
|
||||
},
|
||||
"escaped_newline": {
|
||||
command: "word1\\\nword2",
|
||||
words: []string{"word1word2"},
|
||||
},
|
||||
"quoted_newline": {
|
||||
command: "text \"with\na\" quoted newline",
|
||||
words: []string{"text", "with\na", "quoted", "newline"},
|
||||
},
|
||||
"quoted_escaped_newline": {
|
||||
command: "\"word1\\d\\\\\\\" word2\\\nword3 word4\"",
|
||||
words: []string{"word1\\d\\\" word2word3 word4"},
|
||||
},
|
||||
"escaped_separated_newline": {
|
||||
command: "word1 \\\n word2",
|
||||
words: []string{"word1", "word2"},
|
||||
},
|
||||
"double_quotes_no_spacing": {
|
||||
command: "word1\"word2\"word3",
|
||||
words: []string{"word1word2word3"},
|
||||
},
|
||||
"unterminated_single_quote": {
|
||||
command: "'abc'\\''def",
|
||||
errWrapped: ErrSingleQuoteUnterminated,
|
||||
errMessage: `splitting word in "'abc'\\''def": unterminated single-quoted string`,
|
||||
},
|
||||
"unterminated_double_quote": {
|
||||
command: "\"abc'def",
|
||||
errWrapped: ErrDoubleQuoteUnterminated,
|
||||
errMessage: `splitting word in "\"abc'def": unterminated double-quoted string`,
|
||||
},
|
||||
"unterminated_escape": {
|
||||
command: "abc\\",
|
||||
errWrapped: ErrEscapeUnterminated,
|
||||
errMessage: `splitting word in "abc\\": unterminated backslash-escape`,
|
||||
},
|
||||
"unterminated_escape_only": {
|
||||
command: " \\",
|
||||
errWrapped: ErrEscapeUnterminated,
|
||||
errMessage: `unterminated backslash-escape: " \\"`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
words, err := Split(testCase.command)
|
||||
|
||||
assert.Equal(t, testCase.words, words)
|
||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||
if testCase.errWrapped != nil {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -29,14 +29,6 @@ type PortForwarding struct {
|
||||
// to write to a file. It cannot be nil for the
|
||||
// internal state
|
||||
Filepath *string `json:"status_file_path"`
|
||||
// UpCommand is the command to use when the port forwarding is up.
|
||||
// It can be the empty string to indicate not to run a command.
|
||||
// It cannot be nil in the internal state.
|
||||
UpCommand *string `json:"up_command"`
|
||||
// DownCommand is the command to use after the port forwarding goes down.
|
||||
// It can be the empty string to indicate to NOT run a command.
|
||||
// It cannot be nil in the internal state.
|
||||
DownCommand *string `json:"down_command"`
|
||||
// ListeningPort is the port traffic would be redirected to from the
|
||||
// forwarded port. The redirection is disabled if it is set to 0, which
|
||||
// is its default as well.
|
||||
@@ -92,8 +84,6 @@ func (p *PortForwarding) Copy() (copied PortForwarding) {
|
||||
Enabled: gosettings.CopyPointer(p.Enabled),
|
||||
Provider: gosettings.CopyPointer(p.Provider),
|
||||
Filepath: gosettings.CopyPointer(p.Filepath),
|
||||
UpCommand: gosettings.CopyPointer(p.UpCommand),
|
||||
DownCommand: gosettings.CopyPointer(p.DownCommand),
|
||||
ListeningPort: gosettings.CopyPointer(p.ListeningPort),
|
||||
Username: p.Username,
|
||||
Password: p.Password,
|
||||
@@ -104,8 +94,6 @@ func (p *PortForwarding) OverrideWith(other PortForwarding) {
|
||||
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
|
||||
p.Provider = gosettings.OverrideWithPointer(p.Provider, other.Provider)
|
||||
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
|
||||
p.UpCommand = gosettings.OverrideWithPointer(p.UpCommand, other.UpCommand)
|
||||
p.DownCommand = gosettings.OverrideWithPointer(p.DownCommand, other.DownCommand)
|
||||
p.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort)
|
||||
p.Username = gosettings.OverrideWithComparable(p.Username, other.Username)
|
||||
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
|
||||
@@ -115,8 +103,6 @@ func (p *PortForwarding) setDefaults() {
|
||||
p.Enabled = gosettings.DefaultPointer(p.Enabled, false)
|
||||
p.Provider = gosettings.DefaultPointer(p.Provider, "")
|
||||
p.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port")
|
||||
p.UpCommand = gosettings.DefaultPointer(p.UpCommand, "")
|
||||
p.DownCommand = gosettings.DefaultPointer(p.DownCommand, "")
|
||||
p.ListeningPort = gosettings.DefaultPointer(p.ListeningPort, 0)
|
||||
}
|
||||
|
||||
@@ -149,13 +135,6 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
|
||||
}
|
||||
node.Appendf("Forwarded port file path: %s", filepath)
|
||||
|
||||
if *p.UpCommand != "" {
|
||||
node.Appendf("Forwarded port up command: %s", *p.UpCommand)
|
||||
}
|
||||
if *p.DownCommand != "" {
|
||||
node.Appendf("Forwarded port down command: %s", *p.DownCommand)
|
||||
}
|
||||
|
||||
if p.Username != "" {
|
||||
credentialsNode := node.Appendf("Credentials:")
|
||||
credentialsNode.Appendf("Username: %s", p.Username)
|
||||
@@ -184,12 +163,6 @@ func (p *PortForwarding) read(r *reader.Reader) (err error) {
|
||||
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE",
|
||||
))
|
||||
|
||||
p.UpCommand = r.Get("VPN_PORT_FORWARDING_UP_COMMAND",
|
||||
reader.ForceLowercase(false))
|
||||
|
||||
p.DownCommand = r.Get("VPN_PORT_FORWARDING_DOWN_COMMAND",
|
||||
reader.ForceLowercase(false))
|
||||
|
||||
p.ListeningPort, err = r.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -22,7 +22,7 @@ type chainRule struct {
|
||||
packets uint64
|
||||
bytes uint64
|
||||
target string // "ACCEPT", "DROP", "REJECT" or "REDIRECT"
|
||||
protocol string // "icmp", "tcp", "udp" or "" for all protocols.
|
||||
protocol string // "tcp", "udp" or "" for all protocols.
|
||||
inputInterface string // input interface, for example "tun0" or "*""
|
||||
outputInterface string // output interface, for example "eth0" or "*""
|
||||
source netip.Prefix // source IP CIDR, for example 0.0.0.0/0. Must be valid.
|
||||
@@ -324,8 +324,6 @@ var ErrProtocolUnknown = errors.New("unknown protocol")
|
||||
func parseProtocol(s string) (protocol string, err error) {
|
||||
switch s {
|
||||
case "0":
|
||||
case "1":
|
||||
protocol = "icmp"
|
||||
case "6":
|
||||
protocol = "tcp"
|
||||
case "17":
|
||||
|
||||
@@ -56,8 +56,7 @@ num pkts bytes target prot opt in out source destinati
|
||||
num pkts bytes target prot opt in out source destination
|
||||
1 0 0 ACCEPT 17 -- tun0 * 0.0.0.0/0 0.0.0.0/0 udp dpt:55405
|
||||
2 0 0 ACCEPT 6 -- tun0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:55405
|
||||
3 0 0 ACCEPT 1 -- tun0 * 0.0.0.0/0 0.0.0.0/0
|
||||
4 0 0 DROP 0 -- tun0 * 1.2.3.4 0.0.0.0/0
|
||||
3 0 0 DROP 0 -- tun0 * 1.2.3.4 0.0.0.0/0
|
||||
`,
|
||||
table: chain{
|
||||
name: "INPUT",
|
||||
@@ -93,17 +92,6 @@ num pkts bytes target prot opt in out source destinati
|
||||
lineNumber: 3,
|
||||
packets: 0,
|
||||
bytes: 0,
|
||||
target: "ACCEPT",
|
||||
protocol: "icmp",
|
||||
inputInterface: "tun0",
|
||||
outputInterface: "*",
|
||||
source: netip.MustParsePrefix("0.0.0.0/0"),
|
||||
destination: netip.MustParsePrefix("0.0.0.0/0"),
|
||||
},
|
||||
{
|
||||
lineNumber: 4,
|
||||
packets: 0,
|
||||
bytes: 0,
|
||||
target: "DROP",
|
||||
protocol: "",
|
||||
inputInterface: "tun0",
|
||||
|
||||
@@ -92,7 +92,7 @@ func testIptablesPath(ctx context.Context, path string,
|
||||
// Set policy as the existing policy so no mutation is done.
|
||||
// This is an extra check for some buggy kernels where setting the policy
|
||||
// does not work.
|
||||
cmd = exec.CommandContext(ctx, path, "-nL", "INPUT")
|
||||
cmd = exec.CommandContext(ctx, path, "-L", "INPUT")
|
||||
output, err = runner.Run(cmd)
|
||||
if err != nil {
|
||||
unsupportedMessage = fmt.Sprintf("%s (%s)", output, err)
|
||||
|
||||
@@ -24,7 +24,7 @@ func newDeleteTestRuleMatcher(path string) *cmdMatcher {
|
||||
|
||||
func newListInputRulesMatcher(path string) *cmdMatcher {
|
||||
return newCmdMatcher(path,
|
||||
"^-nL$", "^INPUT$")
|
||||
"^-L$", "^INPUT$")
|
||||
}
|
||||
|
||||
func newSetPolicyMatcher(path, inputPolicy string) *cmdMatcher { //nolint:unparam
|
||||
|
||||
@@ -2,7 +2,6 @@ package natpmp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -24,15 +23,14 @@ func Test_Client_ExternalAddress(t *testing.T) {
|
||||
durationSinceStartOfEpoch time.Duration
|
||||
externalIPv4Address netip.Addr
|
||||
err error
|
||||
errMessageRegex string
|
||||
errMessage string
|
||||
}{
|
||||
"failure": {
|
||||
ctx: canceledCtx,
|
||||
gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
||||
initialConnDuration: initialConnectionDuration,
|
||||
err: net.ErrClosed,
|
||||
errMessageRegex: "executing remote procedure call: setting connection deadline: " +
|
||||
"set udp 127.0.0.1:[1-9][0-9]{1,4}: use of closed network connection",
|
||||
err: context.Canceled,
|
||||
errMessage: "executing remote procedure call: reading from udp connection: context canceled",
|
||||
},
|
||||
"success": {
|
||||
ctx: context.Background(),
|
||||
@@ -62,7 +60,7 @@ func Test_Client_ExternalAddress(t *testing.T) {
|
||||
durationSinceStartOfEpoch, externalIPv4Address, err := client.ExternalAddress(testCase.ctx, testCase.gateway)
|
||||
assert.ErrorIs(t, err, testCase.err)
|
||||
if testCase.err != nil {
|
||||
assert.Regexp(t, testCase.errMessageRegex, err.Error())
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
}
|
||||
assert.Equal(t, testCase.durationSinceStartOfEpoch, durationSinceStartOfEpoch)
|
||||
assert.Equal(t, testCase.externalIPv4Address, externalIPv4Address)
|
||||
|
||||
@@ -45,10 +45,8 @@ func (c *Client) rpc(ctx context.Context, gateway netip.Addr,
|
||||
cancel()
|
||||
<-endGoroutineDone
|
||||
}()
|
||||
ctxListeningReady := make(chan struct{})
|
||||
go func() {
|
||||
defer close(endGoroutineDone)
|
||||
close(ctxListeningReady)
|
||||
// Context is canceled either by the parent context or
|
||||
// when this function returns.
|
||||
<-ctx.Done()
|
||||
@@ -62,7 +60,6 @@ func (c *Client) rpc(ctx context.Context, gateway netip.Addr,
|
||||
}
|
||||
err = fmt.Errorf("%w; closing connection: %w", err, closeErr)
|
||||
}()
|
||||
<-ctxListeningReady // really to make unit testing reliable
|
||||
|
||||
const maxResponseSize = 16
|
||||
response = make([]byte, maxResponseSize)
|
||||
|
||||
@@ -3,7 +3,6 @@ package portforward
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
@@ -30,8 +29,3 @@ type Logger interface {
|
||||
Warn(s string)
|
||||
Error(s string)
|
||||
}
|
||||
|
||||
type Cmder interface {
|
||||
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
|
||||
waitError <-chan error, startErr error)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ type Loop struct {
|
||||
client *http.Client
|
||||
portAllower PortAllower
|
||||
logger Logger
|
||||
cmder Cmder
|
||||
// Fixed parameters
|
||||
uid, gid int
|
||||
// Internal channels and locks
|
||||
@@ -35,7 +34,7 @@ type Loop struct {
|
||||
|
||||
func NewLoop(settings settings.PortForwarding, routing Routing,
|
||||
client *http.Client, portAllower PortAllower,
|
||||
logger Logger, cmder Cmder, uid, gid int,
|
||||
logger Logger, uid, gid int,
|
||||
) *Loop {
|
||||
return &Loop{
|
||||
settings: Settings{
|
||||
@@ -43,8 +42,6 @@ func NewLoop(settings settings.PortForwarding, routing Routing,
|
||||
Service: service.Settings{
|
||||
Enabled: settings.Enabled,
|
||||
Filepath: *settings.Filepath,
|
||||
UpCommand: *settings.UpCommand,
|
||||
DownCommand: *settings.DownCommand,
|
||||
ListeningPort: *settings.ListeningPort,
|
||||
},
|
||||
},
|
||||
@@ -52,7 +49,6 @@ func NewLoop(settings settings.PortForwarding, routing Routing,
|
||||
client: client,
|
||||
portAllower: portAllower,
|
||||
logger: logger,
|
||||
cmder: cmder,
|
||||
uid: uid,
|
||||
gid: gid,
|
||||
}
|
||||
@@ -119,7 +115,7 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
|
||||
*serviceSettings.Enabled = *serviceSettings.Enabled && *l.settings.VPNIsUp
|
||||
|
||||
l.service = service.New(serviceSettings, l.routing, l.client,
|
||||
l.portAllower, l.logger, l.cmder, l.uid, l.gid)
|
||||
l.portAllower, l.logger, l.uid, l.gid)
|
||||
|
||||
var err error
|
||||
serviceRunError, err = l.service.Start(runCtx)
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/command"
|
||||
)
|
||||
|
||||
func runCommand(ctx context.Context, cmder Cmder, logger Logger,
|
||||
commandTemplate string, ports []uint16,
|
||||
) (err error) {
|
||||
portStrings := make([]string, len(ports))
|
||||
for i, port := range ports {
|
||||
portStrings[i] = fmt.Sprint(int(port))
|
||||
}
|
||||
portsString := strings.Join(portStrings, ",")
|
||||
commandString := strings.ReplaceAll(commandTemplate, "{{PORTS}}", portsString)
|
||||
args, err := command.Split(commandString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing command: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, args[0], args[1:]...) // #nosec G204
|
||||
stdout, stderr, waitError, err := cmder.Start(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
streamCtx, streamCancel := context.WithCancel(context.Background())
|
||||
streamDone := make(chan struct{})
|
||||
go streamLines(streamCtx, streamDone, logger, stdout, stderr)
|
||||
|
||||
err = <-waitError
|
||||
streamCancel()
|
||||
<-streamDone
|
||||
return err
|
||||
}
|
||||
|
||||
func streamLines(ctx context.Context, done chan<- struct{},
|
||||
logger Logger, stdout, stderr <-chan string,
|
||||
) {
|
||||
defer close(done)
|
||||
|
||||
var line string
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case line = <-stdout:
|
||||
logger.Info(line)
|
||||
case line = <-stderr:
|
||||
logger.Error(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
//go:build linux
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
"github.com/qdm12/gluetun/internal/command"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_Service_runCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
ctx := context.Background()
|
||||
cmder := command.New()
|
||||
const commandTemplate = `/bin/sh -c "echo {{PORTS}}"`
|
||||
ports := []uint16{1234, 5678}
|
||||
logger := NewMockLogger(ctrl)
|
||||
logger.EXPECT().Info("1234,5678")
|
||||
|
||||
err := runCommand(ctx, cmder, logger, commandTemplate, ports)
|
||||
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"os/exec"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||
)
|
||||
@@ -33,8 +32,3 @@ type PortForwarder interface {
|
||||
ports []uint16, err error)
|
||||
KeepPortForward(ctx context.Context, objects utils.PortForwardObjects) (err error)
|
||||
}
|
||||
|
||||
type Cmder interface {
|
||||
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
|
||||
waitError <-chan error, startErr error)
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
package service
|
||||
|
||||
//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . Logger
|
||||
@@ -1,82 +0,0 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/gluetun/internal/portforward/service (interfaces: Logger)
|
||||
|
||||
// Package service is a generated GoMock package.
|
||||
package service
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockLogger is a mock of Logger interface.
|
||||
type MockLogger struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockLoggerMockRecorder
|
||||
}
|
||||
|
||||
// MockLoggerMockRecorder is the mock recorder for MockLogger.
|
||||
type MockLoggerMockRecorder struct {
|
||||
mock *MockLogger
|
||||
}
|
||||
|
||||
// NewMockLogger creates a new mock instance.
|
||||
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
|
||||
mock := &MockLogger{ctrl: ctrl}
|
||||
mock.recorder = &MockLoggerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Debug mocks base method.
|
||||
func (m *MockLogger) Debug(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Debug", arg0)
|
||||
}
|
||||
|
||||
// Debug indicates an expected call of Debug.
|
||||
func (mr *MockLoggerMockRecorder) Debug(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), arg0)
|
||||
}
|
||||
|
||||
// Error mocks base method.
|
||||
func (m *MockLogger) Error(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Error", arg0)
|
||||
}
|
||||
|
||||
// Error indicates an expected call of Error.
|
||||
func (mr *MockLoggerMockRecorder) Error(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), arg0)
|
||||
}
|
||||
|
||||
// Info mocks base method.
|
||||
func (m *MockLogger) Info(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Info", arg0)
|
||||
}
|
||||
|
||||
// Info indicates an expected call of Info.
|
||||
func (mr *MockLoggerMockRecorder) Info(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), arg0)
|
||||
}
|
||||
|
||||
// Warn mocks base method.
|
||||
func (m *MockLogger) Warn(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Warn", arg0)
|
||||
}
|
||||
|
||||
// Warn indicates an expected call of Warn.
|
||||
func (mr *MockLoggerMockRecorder) Warn(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogger)(nil).Warn), arg0)
|
||||
}
|
||||
@@ -19,7 +19,6 @@ type Service struct {
|
||||
client *http.Client
|
||||
portAllower PortAllower
|
||||
logger Logger
|
||||
cmder Cmder
|
||||
// Internal channels and locks
|
||||
startStopMutex sync.Mutex
|
||||
keepPortCancel context.CancelFunc
|
||||
@@ -27,7 +26,7 @@ type Service struct {
|
||||
}
|
||||
|
||||
func New(settings Settings, routing Routing, client *http.Client,
|
||||
portAllower PortAllower, logger Logger, cmder Cmder, puid, pgid int,
|
||||
portAllower PortAllower, logger Logger, puid, pgid int,
|
||||
) *Service {
|
||||
return &Service{
|
||||
// Fixed parameters
|
||||
@@ -39,7 +38,6 @@ func New(settings Settings, routing Routing, client *http.Client,
|
||||
client: client,
|
||||
portAllower: portAllower,
|
||||
logger: logger,
|
||||
cmder: cmder,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,6 @@ type Settings struct {
|
||||
Enabled *bool
|
||||
PortForwarder PortForwarder
|
||||
Filepath string
|
||||
UpCommand string
|
||||
DownCommand string
|
||||
Interface string // needed for PIA, PrivateVPN and ProtonVPN, tun0 for example
|
||||
ServerName string // needed for PIA
|
||||
CanPortForward bool // needed for PIA
|
||||
@@ -26,8 +24,6 @@ func (s Settings) Copy() (copied Settings) {
|
||||
copied.Enabled = gosettings.CopyPointer(s.Enabled)
|
||||
copied.PortForwarder = s.PortForwarder
|
||||
copied.Filepath = s.Filepath
|
||||
copied.UpCommand = s.UpCommand
|
||||
copied.DownCommand = s.DownCommand
|
||||
copied.Interface = s.Interface
|
||||
copied.ServerName = s.ServerName
|
||||
copied.CanPortForward = s.CanPortForward
|
||||
@@ -41,8 +37,6 @@ func (s *Settings) OverrideWith(update Settings) {
|
||||
s.Enabled = gosettings.OverrideWithPointer(s.Enabled, update.Enabled)
|
||||
s.PortForwarder = gosettings.OverrideWithComparable(s.PortForwarder, update.PortForwarder)
|
||||
s.Filepath = gosettings.OverrideWithComparable(s.Filepath, update.Filepath)
|
||||
s.UpCommand = gosettings.OverrideWithComparable(s.UpCommand, update.UpCommand)
|
||||
s.DownCommand = gosettings.OverrideWithComparable(s.DownCommand, update.DownCommand)
|
||||
s.Interface = gosettings.OverrideWithComparable(s.Interface, update.Interface)
|
||||
s.ServerName = gosettings.OverrideWithComparable(s.ServerName, update.ServerName)
|
||||
s.CanPortForward = gosettings.OverrideWithComparable(s.CanPortForward, update.CanPortForward)
|
||||
|
||||
@@ -73,14 +73,6 @@ func (s *Service) Start(ctx context.Context) (runError <-chan error, err error)
|
||||
s.ports = ports
|
||||
s.portMutex.Unlock()
|
||||
|
||||
if s.settings.UpCommand != "" {
|
||||
err = runCommand(ctx, s.cmder, s.logger, s.settings.UpCommand, ports)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("running up command: %w", err)
|
||||
s.logger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
keepPortCtx, keepPortCancel := context.WithCancel(context.Background())
|
||||
s.keepPortCancel = keepPortCancel
|
||||
runErrorCh := make(chan error)
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *Service) Stop() (err error) {
|
||||
@@ -31,17 +30,6 @@ func (s *Service) cleanup() (err error) {
|
||||
s.portMutex.Lock()
|
||||
defer s.portMutex.Unlock()
|
||||
|
||||
if s.settings.DownCommand != "" {
|
||||
const downTimeout = 60 * time.Second
|
||||
ctx, cancel := context.WithTimeout(context.Background(), downTimeout)
|
||||
defer cancel()
|
||||
err = runCommand(ctx, s.cmder, s.logger, s.settings.DownCommand, s.ports)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("running down command: %w", err)
|
||||
s.logger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
for _, port := range s.ports {
|
||||
err = s.portAllower.RemoveAllowedPort(context.Background(), port)
|
||||
if err != nil {
|
||||
|
||||
@@ -14,17 +14,13 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
|
||||
providerSettings := utils.OpenVPNProviderSettings{
|
||||
AuthUserPass: true,
|
||||
Ciphers: []string{
|
||||
openvpn.AES256gcm,
|
||||
openvpn.AES256cbc,
|
||||
},
|
||||
Auth: openvpn.SHA256,
|
||||
VerifyX509Type: "name",
|
||||
TLSCipher: "TLS-DHE-RSA-WITH-AES-256-CBC-SHA:TLS-DHE-DSS-WITH-AES-256-CBC-SHA:TLS-RSA-WITH-AES-256-CBC-SHA",
|
||||
CAs: []string{"MIIErzCCA5egAwIBAgIJAMYKzSS8uPKDMA0GCSqGSIb3DQEBDQUAMIGVMQswCQYDVQQGEwJVUzELMAkGA1UECBMCRkwxFDASBgNVBAcTC1dpbnRlciBQYXJrMREwDwYDVQQKEwhJUFZhbmlzaDEVMBMGA1UECxMMSVBWYW5pc2ggVlBOMRQwEgYDVQQDEwtJUFZhbmlzaCBDQTEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEBpcHZhbmlzaC5jb20wIBcNMjIwNTA5MjAyMDQ1WhgPMjA4MjA0MjQyMDIwNDVaMIGVMQswCQYDVQQGEwJVUzELMAkGA1UECBMCRkwxFDASBgNVBAcTC1dpbnRlciBQYXJrMREwDwYDVQQKEwhJUFZhbmlzaDEVMBMGA1UECxMMSVBWYW5pc2ggVlBOMRQwEgYDVQQDEwtJUFZhbmlzaCBDQTEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEBpcHZhbmlzaC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC30MFY2v8go65jdOYM/nHu9hlHQMbEttdTxPIDMFuNS0UUxuHGUeJdVCtkeaDOKH3jHsGBczu1amYwphVv6A1qox1YTrzRCbec7CaHL926VcOQQcDAPTmL+JPHhlpR21Xa+woHFGDW90LgASLAPtupXgc6LXfFwb3vVpDnkyPUp4J0DRo2+lq3UtbHaONbGx8jyzYu/kWSiLUc7X69OedoSwlmsGACQteki2o/b0uKTf84Ei+QEjGUquGJU+LETmo2IP55I+KuyZE6+zIiiegm25jgPDkrqlw2UrJiLCjUg4VhTdjF9/AUmT5tJbhZUGGx1/l0bGr+44ea7PmB3DELAgMBAAGjgf0wgfowDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUS/0UJYkd58Fwg9f2nxEcJU4Z7q4wgcoGA1UdIwSBwjCBv4AUS/0UJYkd58Fwg9f2nxEcJU4Z7q6hgZukgZgwgZUxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJGTDEUMBIGA1UEBxMLV2ludGVyIFBhcmsxETAPBgNVBAoTCElQVmFuaXNoMRUwEwYDVQQLEwxJUFZhbmlzaCBWUE4xFDASBgNVBAMTC0lQVmFuaXNoIENBMSMwIQYJKoZIhvcNAQkBFhRzdXBwb3J0QGlwdmFuaXNoLmNvbYIJAMYKzSS8uPKDMA0GCSqGSIb3DQEBDQUAA4IBAQCc9JV7IR8BfBrF/BQTXg0SZMZyyMAxR2jfW9qMHKSeJuZVVjfHiqoynEgBCNbn71wZWv3OF/Thu9BJ4GiYJ2Bc9nIa90D1NGYgiOVYLGXfUUqy5FgfrsWh0Go5oYm9l7W9pWfIifwsaZynkY0rTIHn32FF0H3+wZrGrEUzVL6qi+KD8iR3cBbLT+xUzulMTBp4JYaQnxpV4fZNS0ZsNrWKFWz4Iz1SSBcsnvUhfWs1aKx4yOJQx33Pc+KwpUI+meTlMjoh+AoTriooKU2MbOqLQl32y3pR0MP3fX4HDVFRylxdckEc+VryGNHQLUJiIBKBCORih/YiRhtEhpoBxmkw"}, //nolint:lll
|
||||
CAs: []string{"MIIErTCCA5WgAwIBAgIJAMYKzSS8uPKDMA0GCSqGSIb3DQEBDQUAMIGVMQswCQYDVQQGEwJVUzELMAkGA1UECBMCRkwxFDASBgNVBAcTC1dpbnRlciBQYXJrMREwDwYDVQQKEwhJUFZhbmlzaDEVMBMGA1UECxMMSVBWYW5pc2ggVlBOMRQwEgYDVQQDEwtJUFZhbmlzaCBDQTEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEBpcHZhbmlzaC5jb20wHhcNMTIwMTExMTkzMjIwWhcNMjgxMTAyMTkzMjIwWjCBlTELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkZMMRQwEgYDVQQHEwtXaW50ZXIgUGFyazERMA8GA1UEChMISVBWYW5pc2gxFTATBgNVBAsTDElQVmFuaXNoIFZQTjEUMBIGA1UEAxMLSVBWYW5pc2ggQ0ExIzAhBgkqhkiG9w0BCQEWFHN1cHBvcnRAaXB2YW5pc2guY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt9DBWNr/IKOuY3TmDP5x7vYZR0DGxLbXU8TyAzBbjUtFFMbhxlHiXVQrZHmgzih94x7BgXM7tWpmMKYVb+gNaqMdWE680Qm3nOwmhy/dulXDkEHAwD05i/iTx4ZaUdtV2vsKBxRg1vdC4AEiwD7bqV4HOi13xcG971aQ55Mj1KeCdA0aNvpat1LWx2jjWxsfI8s2Lv5Fkoi1HO1+vTnnaEsJZrBgAkLXpItqP29Lik3/OBIvkBIxlKrhiVPixE5qNiD+eSPirsmROvsyIonoJtuY4Dw5K6pcNlKyYiwo1IOFYU3YxffwFJk+bSW4WVBhsdf5dGxq/uOHmuz5gdwxCwIDAQABo4H9MIH6MAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFEv9FCWJHefBcIPX9p8RHCVOGe6uMIHKBgNVHSMEgcIwgb+AFEv9FCWJHefBcIPX9p8RHCVOGe6uoYGbpIGYMIGVMQswCQYDVQQGEwJVUzELMAkGA1UECBMCRkwxFDASBgNVBAcTC1dpbnRlciBQYXJrMREwDwYDVQQKEwhJUFZhbmlzaDEVMBMGA1UECxMMSVBWYW5pc2ggVlBOMRQwEgYDVQQDEwtJUFZhbmlzaCBDQTEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEBpcHZhbmlzaC5jb22CCQDGCs0kvLjygzANBgkqhkiG9w0BAQ0FAAOCAQEAI2dkh/43ksV2fdYpVGhYaFZPVqCJoToCez0IvOmLeLGzow+EOSrY508oyjYeNP4VJEjApqo0NrMbKl8g/8bpLBcotOCF1c1HZ+y9v7648uumh01SMjsbBeHOuQcLb+7gX6c0pEmxWv8qj5JiW3/1L1bktnjW5Yp5oFkFSMXjOnIoYKHyKLjN2jtwH6XowUNYpg4qVtKU0CXPdOznWcd9/zSfa393HwJPeeVLbKYaFMC4IEbIUmKYtWyoJ9pJ58smU3pWsHZUg9Zc0LZZNjkNlBdQSLmUHAJ33Bd7pJS0JQeiWviC+4UTmzEWRKa7pDGnYRYNu2cUo0/voStphv8EVA=="}, //nolint:lll
|
||||
MssFix: 1320,
|
||||
ExtraLines: []string{
|
||||
"comp-lzo", // Explicitly disable compression
|
||||
},
|
||||
}
|
||||
return utils.OpenVPNConfig(providerSettings, connection, settings, ipv6Supported)
|
||||
}
|
||||
|
||||
86
internal/socks5/constants.go
Normal file
86
internal/socks5/constants.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package socks5
|
||||
|
||||
import "fmt"
|
||||
|
||||
// See https://datatracker.ietf.org/doc/html/rfc1928#section-3
|
||||
type authMethod byte
|
||||
|
||||
const (
|
||||
authNotRequired authMethod = 0
|
||||
authGssapi authMethod = 1
|
||||
authUsernamePassword authMethod = 2
|
||||
authNotAcceptable authMethod = 255
|
||||
)
|
||||
|
||||
func (a authMethod) String() string {
|
||||
switch a {
|
||||
case authNotRequired:
|
||||
return "no authentication required"
|
||||
case authGssapi:
|
||||
return "GSSAPI"
|
||||
case authUsernamePassword:
|
||||
return "username/password"
|
||||
case authNotAcceptable:
|
||||
return "no acceptable methods"
|
||||
default:
|
||||
return fmt.Sprintf("unknown method (%d)", a)
|
||||
}
|
||||
}
|
||||
|
||||
// Subnegotiation version
|
||||
// See https://datatracker.ietf.org/doc/html/rfc1929#section-2
|
||||
const (
|
||||
authUsernamePasswordSubNegotiation1 byte = 1
|
||||
)
|
||||
|
||||
// SOCKS versions.
|
||||
const (
|
||||
socks5Version byte = 5
|
||||
)
|
||||
|
||||
// See https://datatracker.ietf.org/doc/html/rfc1928#section-4
|
||||
type cmdType byte
|
||||
|
||||
const (
|
||||
connect cmdType = 1
|
||||
bind cmdType = 2
|
||||
udpAssociate cmdType = 3
|
||||
)
|
||||
|
||||
func (c cmdType) String() string {
|
||||
switch c {
|
||||
case connect:
|
||||
return "connect"
|
||||
case bind:
|
||||
return "bind"
|
||||
case udpAssociate:
|
||||
return "UDP associate"
|
||||
default:
|
||||
return fmt.Sprintf("unknown command (%d)", c)
|
||||
}
|
||||
}
|
||||
|
||||
// See https://datatracker.ietf.org/doc/html/rfc1928#section-4 and
|
||||
// https://datatracker.ietf.org/doc/html/rfc1928#section-5
|
||||
type addrType byte
|
||||
|
||||
const (
|
||||
ipv4 addrType = 1
|
||||
domainName addrType = 3
|
||||
ipv6 addrType = 4
|
||||
)
|
||||
|
||||
// See https://datatracker.ietf.org/doc/html/rfc1928#section-6
|
||||
type replyCode byte
|
||||
|
||||
const (
|
||||
succeeded replyCode = iota
|
||||
generalServerFailure
|
||||
connectionNotAllowedByRuleset
|
||||
networkUnreachable
|
||||
hostUnreachable
|
||||
connectionRefused
|
||||
ttlExpired
|
||||
commandNotSupported
|
||||
addressTypeNotSupported
|
||||
)
|
||||
6
internal/socks5/interfaces.go
Normal file
6
internal/socks5/interfaces.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package socks5
|
||||
|
||||
type Logger interface {
|
||||
Infof(format string, a ...interface{})
|
||||
Warnf(format string, a ...interface{})
|
||||
}
|
||||
103
internal/socks5/response.go
Normal file
103
internal/socks5/response.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package socks5
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
// See https://datatracker.ietf.org/doc/html/rfc1928#section-6
|
||||
func (c *socksConn) encodeFailedResponse(writer io.Writer, socksVersion byte, reply replyCode) {
|
||||
_, err := writer.Write([]byte{
|
||||
socksVersion,
|
||||
byte(reply),
|
||||
0, // RSV byte
|
||||
// TODO do we need to set the bind addr type to 0??
|
||||
})
|
||||
if err != nil {
|
||||
c.logger.Warnf("failed writing failed response: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// See https://datatracker.ietf.org/doc/html/rfc1928#section-6
|
||||
func (c *socksConn) encodeSuccessResponse(writer io.Writer, socksVersion byte,
|
||||
reply replyCode, bindAddrType addrType, bindAddress string,
|
||||
bindPort uint16) (err error) {
|
||||
bindData, err := encodeBindData(bindAddrType, bindAddress, bindPort)
|
||||
if err != nil { // TODO encode with below block if this changes
|
||||
return err
|
||||
}
|
||||
|
||||
const initialPacketLength = 3
|
||||
capacity := initialPacketLength + len(bindData)
|
||||
packet := make([]byte, initialPacketLength, capacity)
|
||||
packet[0] = socksVersion
|
||||
packet[1] = byte(reply)
|
||||
packet[2] = 0 // RSV byte
|
||||
packet = append(packet, bindData...)
|
||||
|
||||
_, err = writer.Write(packet)
|
||||
if err != nil {
|
||||
c.logger.Warnf("failed writing success response: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
ErrIPVersionUnexpected = errors.New("ip version is unexpected")
|
||||
ErrDomainNameTooLong = errors.New("domain name is too long")
|
||||
)
|
||||
|
||||
func encodeBindData(addrType addrType, address string, port uint16) (
|
||||
data []byte, err error) {
|
||||
capacity := bindDataLength(addrType, address)
|
||||
data = make([]byte, 0, capacity)
|
||||
|
||||
data = append(data, byte(addrType))
|
||||
switch addrType {
|
||||
case ipv4, ipv6:
|
||||
ip, err := netip.ParseAddr(address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing IP address: %w", err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case addrType == ipv4 && !ip.Is4():
|
||||
return nil, fmt.Errorf("%w: expected IPv4 for %s", ErrIPVersionUnexpected, ip)
|
||||
case addrType == ipv6 && !ip.Is6():
|
||||
return nil, fmt.Errorf("%w: expected IPv6 for %s", ErrIPVersionUnexpected, ip)
|
||||
}
|
||||
data = append(data, ip.AsSlice()...)
|
||||
case domainName:
|
||||
const maxDomainNameLength = 255
|
||||
if len(address) > maxDomainNameLength {
|
||||
return nil, fmt.Errorf("%w: %s", ErrDomainNameTooLong, address)
|
||||
}
|
||||
data = append(data, byte(len(address)))
|
||||
data = append(data, []byte(address)...)
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported address type %d", addrType))
|
||||
}
|
||||
data = binary.BigEndian.AppendUint16(data, port)
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func bindDataLength(addrType addrType, address string) (maxLength int) {
|
||||
maxLength++ // address type
|
||||
switch addrType {
|
||||
case ipv4:
|
||||
maxLength += net.IPv4len
|
||||
case domainName:
|
||||
maxLength++ // domain name length
|
||||
maxLength += len([]byte(address))
|
||||
case ipv6:
|
||||
maxLength += net.IPv6len
|
||||
default:
|
||||
panic("unsupported address type: " + fmt.Sprint(addrType))
|
||||
}
|
||||
maxLength += 2 // port
|
||||
return maxLength
|
||||
}
|
||||
105
internal/socks5/server.go
Normal file
105
internal/socks5/server.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package socks5
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
username string
|
||||
password string
|
||||
address string
|
||||
logger Logger
|
||||
|
||||
// internal fields
|
||||
listener net.Listener
|
||||
listening atomic.Bool
|
||||
socksConnCtx context.Context //nolint:containedctx
|
||||
socksConnCancel context.CancelFunc
|
||||
done <-chan struct{}
|
||||
stopping atomic.Bool
|
||||
}
|
||||
|
||||
func New(settings Settings) *Server {
|
||||
return &Server{
|
||||
username: settings.Username,
|
||||
password: settings.Password,
|
||||
address: settings.Address,
|
||||
logger: settings.Logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Start(_ context.Context) (runErr <-chan error, err error) {
|
||||
s.listener, err = net.Listen("tcp", s.address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listening on %s: %w", s.address, err)
|
||||
}
|
||||
s.listening.Store(true)
|
||||
|
||||
s.socksConnCtx, s.socksConnCancel = context.WithCancel(context.Background())
|
||||
|
||||
ready := make(chan struct{})
|
||||
runErrCh := make(chan error)
|
||||
runErr = runErrCh
|
||||
done := make(chan struct{})
|
||||
s.done = done
|
||||
go s.runServer(ready, runErrCh, done)
|
||||
<-ready
|
||||
return runErr, nil
|
||||
}
|
||||
|
||||
func (s *Server) runServer(ready chan<- struct{},
|
||||
runErrCh chan<- error, done chan<- struct{}) {
|
||||
close(ready)
|
||||
defer close(done)
|
||||
wg := new(sync.WaitGroup)
|
||||
defer wg.Wait()
|
||||
|
||||
dialer := &net.Dialer{}
|
||||
for {
|
||||
connection, err := s.listener.Accept()
|
||||
if err != nil {
|
||||
if !s.stopping.Load() {
|
||||
_ = s.Stop()
|
||||
runErrCh <- fmt.Errorf("accepting connection: %w", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(ctx context.Context, connection net.Conn,
|
||||
dialer *net.Dialer, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
socksConn := &socksConn{
|
||||
dialer: dialer,
|
||||
username: s.username,
|
||||
password: s.password,
|
||||
clientConn: connection,
|
||||
logger: s.logger,
|
||||
}
|
||||
err := socksConn.run(ctx)
|
||||
if err != nil {
|
||||
s.logger.Infof("running socks connection: %s", err)
|
||||
}
|
||||
}(s.socksConnCtx, connection, dialer, wg)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Stop() (err error) {
|
||||
s.stopping.Store(true)
|
||||
s.listening.Store(false)
|
||||
err = s.listener.Close()
|
||||
s.socksConnCancel() // stop ongoing socks connections
|
||||
<-s.done // wait for run goroutine to finish
|
||||
s.stopping.Store(false)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Server) listeningAddress() net.Addr {
|
||||
if s.listening.Load() {
|
||||
return s.listener.Addr()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
8
internal/socks5/settings.go
Normal file
8
internal/socks5/settings.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package socks5
|
||||
|
||||
type Settings struct {
|
||||
Username string
|
||||
Password string
|
||||
Address string
|
||||
Logger Logger
|
||||
}
|
||||
283
internal/socks5/socks5.go
Normal file
283
internal/socks5/socks5.go
Normal file
@@ -0,0 +1,283 @@
|
||||
package socks5
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type socksConn struct {
|
||||
// Injected fields
|
||||
dialer *net.Dialer
|
||||
username string
|
||||
password string
|
||||
clientConn net.Conn
|
||||
logger Logger
|
||||
}
|
||||
|
||||
func (c *socksConn) closeClientConn(ctxErr error) {
|
||||
err := c.clientConn.Close()
|
||||
if err != nil && ctxErr == nil {
|
||||
c.logger.Warnf("closing client connection: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *socksConn) run(ctx context.Context) error {
|
||||
authMethod := authNotRequired
|
||||
if c.username != "" || c.password != "" {
|
||||
authMethod = authUsernamePassword
|
||||
}
|
||||
|
||||
err := verifyFirstNegotiation(c.clientConn, authMethod)
|
||||
if err != nil {
|
||||
replyMethod := authMethod
|
||||
if errors.Is(err, ErrNoMethodIdentifiers) || errors.Is(err, ErrNoValidMethodIdentifier) {
|
||||
replyMethod = authNotAcceptable
|
||||
}
|
||||
_, writeErr := c.clientConn.Write([]byte{socks5Version, byte(replyMethod)})
|
||||
if writeErr != nil {
|
||||
c.logger.Warnf("failed writing first negotiation reply: %s", writeErr)
|
||||
}
|
||||
c.closeClientConn(ctx.Err())
|
||||
return fmt.Errorf("verifying first negotiation: %w", err)
|
||||
}
|
||||
|
||||
_, err = c.clientConn.Write([]byte{socks5Version, byte(authMethod)})
|
||||
if err != nil {
|
||||
c.closeClientConn(ctx.Err())
|
||||
return fmt.Errorf("writing first negotiation reply: %w", err)
|
||||
}
|
||||
|
||||
switch authMethod {
|
||||
case authNotRequired, authNotAcceptable:
|
||||
case authGssapi:
|
||||
panic("not implemented")
|
||||
// TODO
|
||||
case authUsernamePassword:
|
||||
// See https://datatracker.ietf.org/doc/html/rfc1929#section-2
|
||||
err = usernamePasswordSubnegotiate(c.clientConn, c.username, c.password)
|
||||
if err != nil {
|
||||
// If the server returns a `failure' (STATUS value other than X'00') status,
|
||||
// it MUST close the connection.
|
||||
c.closeClientConn(ctx.Err())
|
||||
return fmt.Errorf("subnegotiating username and password: %w", err)
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("unimplemented auth method %d", authMethod))
|
||||
}
|
||||
|
||||
err = c.handleRequest(ctx)
|
||||
c.closeClientConn(ctx.Err())
|
||||
if err != nil {
|
||||
return fmt.Errorf("handling request: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
ErrCommandNotSupported = errors.New("command not supported")
|
||||
)
|
||||
|
||||
func (c *socksConn) handleRequest(ctx context.Context) error {
|
||||
const socksVersion = socks5Version
|
||||
request, err := decodeRequest(c.clientConn, socksVersion)
|
||||
if err != nil {
|
||||
c.encodeFailedResponse(c.clientConn, socksVersion, generalServerFailure)
|
||||
return err
|
||||
}
|
||||
if request.command != connect {
|
||||
c.encodeFailedResponse(c.clientConn, socksVersion, commandNotSupported)
|
||||
return fmt.Errorf("%w: %s", ErrCommandNotSupported, request.command)
|
||||
}
|
||||
|
||||
destinationAddress := net.JoinHostPort(request.destination, fmt.Sprint(request.port))
|
||||
destinationConn, err := c.dialer.DialContext(ctx, "tcp", destinationAddress)
|
||||
if err != nil {
|
||||
c.encodeFailedResponse(c.clientConn, socksVersion, generalServerFailure)
|
||||
return err
|
||||
}
|
||||
defer destinationConn.Close()
|
||||
|
||||
destinationServerAddress := destinationConn.LocalAddr().String()
|
||||
destinationAddr, destinationPortStr, err := net.SplitHostPort(destinationServerAddress)
|
||||
fmt.Println("===", destinationServerAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destinationPort, err := strconv.Atoi(destinationPortStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var bindAddrType addrType
|
||||
if ip := net.ParseIP(destinationAddr); ip != nil {
|
||||
if ip.To4() != nil {
|
||||
bindAddrType = ipv4
|
||||
} else {
|
||||
bindAddrType = ipv6
|
||||
}
|
||||
} else {
|
||||
bindAddrType = domainName
|
||||
}
|
||||
|
||||
err = c.encodeSuccessResponse(c.clientConn, socksVersion, succeeded, bindAddrType,
|
||||
destinationAddr, uint16(destinationPort))
|
||||
if err != nil {
|
||||
c.encodeFailedResponse(c.clientConn, socksVersion, generalServerFailure)
|
||||
return fmt.Errorf("writing successful %s response: %w", request.command, err)
|
||||
}
|
||||
|
||||
errc := make(chan error)
|
||||
go func() {
|
||||
_, err := io.Copy(c.clientConn, destinationConn)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("from backend to client: %w", err)
|
||||
}
|
||||
errc <- err
|
||||
}()
|
||||
go func() {
|
||||
_, err := io.Copy(destinationConn, c.clientConn)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("from client to backend: %w", err)
|
||||
}
|
||||
errc <- err
|
||||
}()
|
||||
select {
|
||||
case err := <-errc:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
_ = destinationConn.Close()
|
||||
_ = c.clientConn.Close()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ErrVersionNotSupported = errors.New("version not supported")
|
||||
ErrNoMethodIdentifiers = errors.New("no method identifiers")
|
||||
)
|
||||
|
||||
// See https://datatracker.ietf.org/doc/html/rfc1928#section-3
|
||||
func verifyFirstNegotiation(reader io.Reader, requiredMethod authMethod) error {
|
||||
const headerLength = 2 // version + nMethods bytes
|
||||
header := make([]byte, headerLength)
|
||||
_, err := io.ReadFull(reader, header[:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading header: %w", err)
|
||||
}
|
||||
|
||||
if header[0] != socks5Version {
|
||||
return fmt.Errorf("%w: %d", ErrVersionNotSupported, header[0])
|
||||
}
|
||||
|
||||
nMethods := header[1]
|
||||
if nMethods == 0 {
|
||||
return fmt.Errorf("%w", ErrNoMethodIdentifiers)
|
||||
}
|
||||
|
||||
methodIdentifiers := make([]byte, nMethods)
|
||||
_, err = io.ReadFull(reader, methodIdentifiers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading method identifiers: %w", err)
|
||||
}
|
||||
for _, methodIdentifier := range methodIdentifiers {
|
||||
if methodIdentifier == byte(requiredMethod) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return makeNoAcceptableMethodError(requiredMethod, methodIdentifiers)
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNoValidMethodIdentifier = errors.New("no valid method identifier")
|
||||
)
|
||||
|
||||
func makeNoAcceptableMethodError(requiredAuthMethod authMethod, methodIdentifiers []byte) error {
|
||||
methodNames := make([]string, len(methodIdentifiers))
|
||||
for i, methodIdentifier := range methodIdentifiers {
|
||||
methodNames[i] = fmt.Sprintf("%q", authMethod(methodIdentifier))
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w: none of %s matches %s",
|
||||
ErrNoValidMethodIdentifier, strings.Join(methodNames, ", "),
|
||||
requiredAuthMethod)
|
||||
}
|
||||
|
||||
// See https://datatracker.ietf.org/doc/html/rfc1928#section-4
|
||||
type request struct {
|
||||
command cmdType
|
||||
destination string
|
||||
port uint16
|
||||
addressType addrType
|
||||
}
|
||||
|
||||
var (
|
||||
ErrRequestSocksVersionMismatch = errors.New("request SOCKS version mismatch")
|
||||
ErrAddressTypeNotSupported = errors.New("address type not supported")
|
||||
)
|
||||
|
||||
func decodeRequest(reader io.Reader, expectedVersion byte) (req request, err error) {
|
||||
const headerLength = 4
|
||||
header := [headerLength]byte{}
|
||||
_, err = io.ReadFull(reader, header[:])
|
||||
if err != nil {
|
||||
return request{}, fmt.Errorf("reading header: %w", err)
|
||||
}
|
||||
|
||||
version := header[0]
|
||||
if header[0] != expectedVersion {
|
||||
return request{}, fmt.Errorf("%w: expected %d and got %d",
|
||||
ErrRequestSocksVersionMismatch, expectedVersion, version)
|
||||
}
|
||||
|
||||
req.command = cmdType(header[1])
|
||||
// header[2] is RSV byte
|
||||
req.addressType = addrType(header[3])
|
||||
|
||||
switch req.addressType {
|
||||
case ipv4:
|
||||
var ip [4]byte
|
||||
_, err = io.ReadFull(reader, ip[:])
|
||||
if err != nil {
|
||||
return request{}, fmt.Errorf("reading IPv4 address: %w", err)
|
||||
}
|
||||
req.destination = netip.AddrFrom4(ip).String()
|
||||
case ipv6:
|
||||
var ip [16]byte
|
||||
_, err = io.ReadFull(reader, ip[:])
|
||||
if err != nil {
|
||||
return request{}, fmt.Errorf("reading IPv6 address: %w", err)
|
||||
}
|
||||
req.destination = netip.AddrFrom16(ip).String()
|
||||
case domainName:
|
||||
var header [1]byte
|
||||
_, err = io.ReadFull(reader, header[:])
|
||||
if err != nil {
|
||||
return request{}, fmt.Errorf("reading domain name header: %w", err)
|
||||
}
|
||||
domainName := make([]byte, header[0])
|
||||
_, err = io.ReadFull(reader, domainName)
|
||||
if err != nil {
|
||||
return request{}, fmt.Errorf("reading domain name bytes: %w", err)
|
||||
}
|
||||
req.destination = string(domainName)
|
||||
default:
|
||||
return request{}, fmt.Errorf("%w: %d", ErrAddressTypeNotSupported, req.addressType)
|
||||
}
|
||||
|
||||
var portBytes [2]byte
|
||||
_, err = io.ReadFull(reader, portBytes[:])
|
||||
if err != nil {
|
||||
return request{}, fmt.Errorf("reading port: %w", err)
|
||||
}
|
||||
req.port = binary.BigEndian.Uint16(portBytes[:])
|
||||
|
||||
return req, nil
|
||||
}
|
||||
175
internal/socks5/socks5_test.go
Normal file
175
internal/socks5/socks5_test.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package socks5
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/log"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
server := New(Settings{
|
||||
Username: "test",
|
||||
Password: "test",
|
||||
Address: ":8000",
|
||||
Logger: log.New(),
|
||||
})
|
||||
|
||||
runErr, startErr := server.Start(context.Background())
|
||||
require.NoError(t, startErr)
|
||||
|
||||
select {
|
||||
case err := <-runErr:
|
||||
require.NoError(t, err)
|
||||
default:
|
||||
}
|
||||
|
||||
t.Log("SlEEPING")
|
||||
time.Sleep(15 * time.Second)
|
||||
t.Log("Done sleeping")
|
||||
|
||||
err := server.Stop()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func backendServer(listener net.Listener) {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
conn.Write([]byte("Test"))
|
||||
conn.Close()
|
||||
listener.Close()
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
// backend server which we'll use SOCKS5 to connect to
|
||||
listener, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
backendServerPort := listener.Addr().(*net.TCPAddr).Port
|
||||
go backendServer(listener)
|
||||
|
||||
// SOCKS5 server
|
||||
server := New(Settings{
|
||||
Address: ":0",
|
||||
})
|
||||
_, err = server.Start(context.Background())
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
err = server.Stop()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
socks5Port := server.listeningAddress().(*net.TCPAddr).Port
|
||||
|
||||
addr := fmt.Sprintf("localhost:%d", socks5Port)
|
||||
socksDialer, err := proxy.SOCKS5("tcp", addr, nil, proxy.Direct)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr = fmt.Sprintf("localhost:%d", backendServerPort)
|
||||
conn, err := socksDialer.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 4)
|
||||
_, err = io.ReadFull(conn, buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(buf) != "Test" {
|
||||
t.Fatalf("got: %q want: Test", buf)
|
||||
}
|
||||
|
||||
err = conn.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPassword(t *testing.T) {
|
||||
// backend server which we'll use SOCKS5 to connect to
|
||||
ln, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
backendServerPort := ln.Addr().(*net.TCPAddr).Port
|
||||
go backendServer(ln)
|
||||
|
||||
auth := &proxy.Auth{User: "foo", Password: "bar"}
|
||||
|
||||
server := Server{
|
||||
logger: log.New(),
|
||||
username: auth.User,
|
||||
password: auth.Password,
|
||||
address: ":0",
|
||||
}
|
||||
_, err = server.Start(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
err = server.Stop()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
addr := fmt.Sprintf("localhost:%d", server.listeningAddress().(*net.TCPAddr).Port)
|
||||
|
||||
if d, err := proxy.SOCKS5("tcp", addr, nil, proxy.Direct); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
if _, err := d.Dial("tcp", addr); err == nil {
|
||||
t.Fatal("expected no-auth dial error")
|
||||
}
|
||||
}
|
||||
|
||||
badPwd := &proxy.Auth{User: "foo", Password: "not right"}
|
||||
if d, err := proxy.SOCKS5("tcp", addr, badPwd, proxy.Direct); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
if _, err := d.Dial("tcp", addr); err == nil {
|
||||
t.Fatal("expected bad password dial error")
|
||||
}
|
||||
}
|
||||
|
||||
badUsr := &proxy.Auth{User: "not right", Password: "bar"}
|
||||
if d, err := proxy.SOCKS5("tcp", addr, badUsr, proxy.Direct); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
if _, err := d.Dial("tcp", addr); err == nil {
|
||||
t.Fatal("expected bad username dial error")
|
||||
}
|
||||
}
|
||||
|
||||
socksDialer, err := proxy.SOCKS5("tcp", addr, auth, proxy.Direct)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr = fmt.Sprintf("localhost:%d", backendServerPort)
|
||||
conn, err := socksDialer.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 4)
|
||||
if _, err := io.ReadFull(conn, buf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(buf) != "Test" {
|
||||
t.Fatalf("got: %q want: Test", buf)
|
||||
}
|
||||
|
||||
if err := conn.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
69
internal/socks5/usernamepassword.go
Normal file
69
internal/socks5/usernamepassword.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package socks5
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrSubnegotiationVersionNotSupported = errors.New("subnegotiation version not supported")
|
||||
ErrUsernameNotValid = errors.New("username not valid")
|
||||
ErrPasswordNotValid = errors.New("password not valid")
|
||||
)
|
||||
|
||||
// See https://datatracker.ietf.org/doc/html/rfc1929#section-2
|
||||
func usernamePasswordSubnegotiate(conn io.ReadWriter, username, password string) (err error) {
|
||||
status := byte(1)
|
||||
const defaultVersion = byte(1)
|
||||
|
||||
const headerLength = 2
|
||||
var header [headerLength]byte
|
||||
_, err = io.ReadFull(conn, header[:])
|
||||
if err != nil {
|
||||
_, _ = conn.Write([]byte{defaultVersion, status})
|
||||
return fmt.Errorf("reading header: %w", err)
|
||||
}
|
||||
|
||||
if header[0] != authUsernamePasswordSubNegotiation1 {
|
||||
_, _ = conn.Write([]byte{defaultVersion, status})
|
||||
return fmt.Errorf("%w: %d", ErrSubnegotiationVersionNotSupported, header[0])
|
||||
}
|
||||
version := header[0]
|
||||
|
||||
usernameBytes := make([]byte, header[1])
|
||||
_, err = io.ReadFull(conn, usernameBytes)
|
||||
if err != nil {
|
||||
_, _ = conn.Write([]byte{version, status})
|
||||
return fmt.Errorf("reading username bytes: %w", err)
|
||||
} else if username != string(usernameBytes) {
|
||||
_, _ = conn.Write([]byte{version, status})
|
||||
return fmt.Errorf("%w: %s", ErrUsernameNotValid, string(usernameBytes))
|
||||
}
|
||||
|
||||
const passwordHeaderLength = 1
|
||||
passwordHeader := make([]byte, passwordHeaderLength)
|
||||
_, err = io.ReadFull(conn, passwordHeader[:])
|
||||
if err != nil {
|
||||
_, _ = conn.Write([]byte{version, status})
|
||||
return fmt.Errorf("reading password length: %w", err)
|
||||
}
|
||||
|
||||
passwordBytes := make([]byte, passwordHeader[0])
|
||||
_, err = io.ReadFull(conn, passwordBytes)
|
||||
if err != nil {
|
||||
_, _ = conn.Write([]byte{version, status})
|
||||
return fmt.Errorf("reading password bytes: %w", err)
|
||||
} else if password != string(passwordBytes) {
|
||||
_, _ = conn.Write([]byte{version, status})
|
||||
return fmt.Errorf("%w: %s", ErrPasswordNotValid, string(passwordBytes))
|
||||
}
|
||||
|
||||
status = 0
|
||||
_, err = conn.Write([]byte{version, status})
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing success status: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package wireguard
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/netlink"
|
||||
)
|
||||
@@ -17,10 +16,6 @@ func (w *Wireguard) addRule(rulePriority int, firewallMark uint32,
|
||||
rule.Table = int(firewallMark)
|
||||
rule.Family = family
|
||||
if err := w.netlink.RuleAdd(rule); err != nil {
|
||||
if strings.HasSuffix(err.Error(), "file exists") {
|
||||
w.logger.Info("if you are using Kubernetes, this may fix the error below: " +
|
||||
"https://github.com/qdm12/gluetun-wiki/blob/main/setup/advanced/kubernetes.md#adding-ipv6-rule--file-exists")
|
||||
}
|
||||
return nil, fmt.Errorf("adding %s: %w", rule, err)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user