Skip to content

Commit 3aabea5

Browse files
committed
conflict
2 parents bba8920 + 6d2162b commit 3aabea5

File tree

9 files changed

+181
-55
lines changed

9 files changed

+181
-55
lines changed

.github/.jira_sync_config.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# See https://github.com/canonical/gh-jira-sync-bot for config
2+
settings:
3+
jira_project_key: "ISD"
4+
5+
status_mapping:
6+
opened: Untriaged
7+
closed: done
8+
not_planned: rejected
9+
10+
add_gh_comment: true
11+
12+
epic_key: ISD-3981
13+
14+
label_mapping:
15+
bug: Bug
16+
enhancement: Story

.github/workflows/integration-tests.yaml

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,56 +7,98 @@ on:
77
jobs:
88
integration-test:
99
name: Run Integration Tests
10-
runs-on: [ self-hosted, linux, x64, large ]
10+
runs-on: [ self-hosted, linux, x64, jammy, large ]
1111

1212
steps:
1313
- uses: actions/checkout@v2
1414

15-
- name: Build Aproxy Snap
15+
- name: Build aproxy Snap
1616
id: snapcraft-build
1717
uses: snapcore/action-build@v1
18+
with:
19+
snapcraft-args: --build-for amd64
1820

19-
- name: Upload Aproxy Snap
20-
uses: actions/upload-artifact@v3
21+
- name: Upload aproxy Snap
22+
uses: actions/upload-artifact@v4
2123
with:
2224
name: snap
2325
path: aproxy*.snap
2426

25-
- name: Install Aproxy Snap
27+
- name: Install aproxy Snap
2628
run: |
2729
sudo snap install --dangerous aproxy_*_amd64.snap
2830
29-
- name: Configure Aproxy
31+
- name: Show aproxy Configuration
32+
run: |
33+
sudo snap get aproxy
34+
35+
- name: Configure aproxy
3036
run: |
31-
sudo snap set aproxy proxy=squid.internal:3128 listen=:23403
3237
sudo nft -f - << EOF
3338
define default-ip = $(ip route get $(ip route show 0.0.0.0/0 | grep -oP 'via \K\S+') | grep -oP 'src \K\S+')
3439
define private-ips = { 10.0.0.0/8, 127.0.0.1/8, 172.16.0.0/12, 192.168.0.0/16 }
40+
define aproxy-port = $(sudo snap get aproxy listen | cut -d ":" -f 2)
3541
table ip aproxy
3642
flush table ip aproxy
3743
table ip aproxy {
3844
chain prerouting {
3945
type nat hook prerouting priority dstnat; policy accept;
40-
ip daddr != \$private-ips tcp dport { 80, 443 } counter dnat to \$default-ip:23403
46+
ip daddr != \$private-ips tcp dport { 80, 443, 11371, 4242 } counter dnat to \$default-ip:\$aproxy-port
4147
}
4248
4349
chain output {
4450
type nat hook output priority -100; policy accept;
45-
ip daddr != \$private-ips tcp dport { 80, 443 } counter dnat to \$default-ip:23403
51+
ip daddr != \$private-ips tcp dport { 80, 443, 11371, 4242 } counter dnat to \$default-ip:\$aproxy-port
4652
}
4753
}
4854
EOF
4955
56+
- name: Start tcpdump
57+
run: |
58+
sudo tcpdump -i any -s 65535 -w capture.pcap &
59+
echo $! > tcpdump.pid
60+
5061
- name: Test HTTP
5162
run: |
52-
curl --noproxy "*" http://example.com -svS -o /dev/null
63+
timeout 60 curl --noproxy "*" http://example.com -svS -o /dev/null
5364
5465
- name: Test HTTPS
5566
run: |
56-
curl --noproxy "*" https://example.com -svS -o /dev/null
67+
timeout 60 curl --noproxy "*" https://example.com -svS -o /dev/null
68+
69+
- name: Test HKP
70+
run: |
71+
timeout 60 gpg -vvv --keyserver hkp://keyserver.ubuntu.com --recv-keys E1DE584A8CCA52DC29550F18ABAC58F075A17EFA
72+
73+
- name: Test TCP4
74+
run: |
75+
sudo apt install -y socat
76+
timeout 60 socat /dev/null TCP4:tcpbin.com:4242
5777
5878
- name: Test Access Logs
5979
run: |
60-
sudo snap logs aproxy.aproxy
6180
sudo snap logs aproxy.aproxy | grep -Fq "example.com:80"
6281
sudo snap logs aproxy.aproxy | grep -Fq "example.com:443"
82+
sudo snap logs aproxy.aproxy | grep -Fq "keyserver.ubuntu.com:11371"
83+
sudo snap logs aproxy.aproxy | grep -Eq "[0-9.]+:4242"
84+
85+
- name: Show Access Logs
86+
if: failure()
87+
run: |
88+
sudo snap logs aproxy.aproxy -n=all
89+
90+
- name: Stop tcpdump
91+
if: failure()
92+
run: |
93+
PID=$(cat tcpdump.pid)
94+
if [ -n "$PID" ]; then
95+
sudo kill -2 "$PID" || true
96+
fi
97+
sleep 1
98+
99+
- name: Upload tcpdump capture
100+
if: failure()
101+
uses: actions/upload-artifact@v4
102+
with:
103+
name: tcpdump
104+
path: capture.pcap

.github/workflows/publish.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Publish
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
7+
jobs:
8+
tests:
9+
uses: ./.github/workflows/tests.yaml
10+
11+
integration-tests:
12+
uses: ./.github/workflows/integration-tests.yaml
13+
14+
publish:
15+
name: Publish Aproxy
16+
runs-on: ubuntu-latest
17+
needs: [ tests, integration-tests ]
18+
19+
steps:
20+
- uses: actions/checkout@v2
21+
22+
- name: Build Aproxy Snap
23+
id: snapcraft-build
24+
uses: snapcore/action-build@v1
25+
26+
- name: Publish Aproxy
27+
env:
28+
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
29+
run: |
30+
for snap in aproxy*.snap
31+
do
32+
snapcraft upload $snap --release edge
33+
done

CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* @canonical/is-charms

aproxy.go

Lines changed: 56 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"bufio"
5+
"bytes"
56
"context"
67
"encoding/binary"
78
"errors"
@@ -18,12 +19,11 @@ import (
1819
"strings"
1920
"sync"
2021
"sync/atomic"
21-
"syscall"
2222

2323
"golang.org/x/crypto/cryptobyte"
2424
)
2525

26-
var version = "0.2.2"
26+
var version = "0.2.4"
2727

2828
// PrereadConn is a wrapper around net.Conn that supports pre-reading from the underlying connection.
2929
// Any Read before the EndPreread can be undone and read again by calling the EndPreread function.
@@ -84,27 +84,24 @@ func PrereadSNI(conn *PrereadConn) (_ string, err error) {
8484
err = fmt.Errorf("failed to preread TLS client hello: %w", err)
8585
}
8686
}()
87-
typeVersionLen := make([]byte, 5)
88-
n, err := conn.Read(typeVersionLen)
89-
if n != 5 {
90-
return "", errors.New("too short")
91-
}
87+
recordHeader := make([]byte, 5)
88+
n, err := io.ReadFull(conn, recordHeader)
9289
if err != nil {
93-
return "", err
90+
return "", fmt.Errorf("failed to read TLS record layer header: %w", err)
91+
}
92+
if n != 5 {
93+
return "", fmt.Errorf("failed to read TLS record layer header: too short, less than 5 bytes (%d)", n)
9494
}
95-
if typeVersionLen[0] != 22 {
95+
if recordHeader[0] != 22 {
9696
return "", errors.New("not a TCP handshake")
9797
}
98-
msgLen := binary.BigEndian.Uint16(typeVersionLen[3:])
98+
msgLen := binary.BigEndian.Uint16(recordHeader[3:])
9999
buf := make([]byte, msgLen+5)
100-
n, err = conn.Read(buf[5:])
100+
n, err = io.ReadFull(conn, buf[5:])
101101
if n != int(msgLen) {
102-
return "", errors.New("too short")
103-
}
104-
if err != nil {
105-
return "", err
102+
return "", fmt.Errorf("client hello too short (%d < %d), err: %w", n, msgLen, err)
106103
}
107-
copy(buf[:5], typeVersionLen)
104+
copy(buf[:5], recordHeader)
108105
return extractSNI(buf)
109106
}
110107

@@ -224,6 +221,7 @@ func DialProxy(proxy string) (net.Conn, error) {
224221
}
225222

226223
// DialProxyConnect dials the TCP connection and finishes the HTTP CONNECT handshake with the proxy.
224+
// dst: HOST:PORT or IP:PORT
227225
func DialProxyConnect(proxy string, dst string) (net.Conn, error) {
228226
conn, err := DialProxy(proxy)
229227
if err != nil {
@@ -247,12 +245,12 @@ func DialProxyConnect(proxy string, dst string) (net.Conn, error) {
247245
return nil, fmt.Errorf("failed to send connect request to http proxy: %w", err)
248246
}
249247
response, err := http.ReadResponse(bufio.NewReaderSize(conn, 0), &request)
250-
if response.StatusCode != 200 {
251-
return nil, fmt.Errorf("proxy return %d response for connect request", response.StatusCode)
252-
}
253248
if err != nil {
254249
return nil, fmt.Errorf("failed to receive http connect response from proxy: %w", err)
255250
}
251+
if response.StatusCode != 200 {
252+
return nil, fmt.Errorf("proxy return %d response for connect request", response.StatusCode)
253+
}
256254
return conn, nil
257255
}
258256

@@ -268,11 +266,7 @@ func GetOriginalDst(conn *net.TCPConn) (*net.TCPAddr, error) {
268266
if err != nil {
269267
return nil, fmt.Errorf("failed to convert connection to file: %w", err)
270268
}
271-
return GetsockoptIPv4OriginalDst(
272-
int(file.Fd()),
273-
syscall.SOL_IP,
274-
80, // SO_ORIGINAL_DST
275-
)
269+
return GetsockoptIPv4OriginalDst(file.Fd())
276270
}
277271

278272
// RelayTCP relays data between the incoming TCP connection and the proxy connection.
@@ -304,10 +298,34 @@ func RelayHTTP(conn io.ReadWriter, proxyConn io.ReadWriteCloser, logger *slog.Lo
304298
}
305299
req.URL.Host = req.Host
306300
req.URL.Scheme = "http"
301+
if req.UserAgent() == "" {
302+
req.Header.Set("User-Agent", "")
303+
}
307304
req.Header.Set("Connection", "close")
308-
if err := req.WriteProxy(proxyConn); err != nil {
309-
logger.Error("failed to send HTTP request to proxy", "error", err)
310-
return
305+
if req.Proto == "HTTP/1.0" {
306+
// no matter what the request protocol is, Go enforces a minimum version of HTTP/1.1
307+
// this causes problems for HTTP/1.0 only clients like GPG (HKP)
308+
// manually modify and send the HTTP/1.0 request to the proxy server
309+
buf := bytes.NewBuffer(nil)
310+
err := req.WriteProxy(buf)
311+
if err != nil {
312+
logger.Error("failed to serialize HTTP/1.0 request", "error", err)
313+
return
314+
}
315+
reqStr := buf.String()
316+
crlfIndex := strings.Index(reqStr, "\r\n")
317+
protoSpaceIndex := strings.LastIndex(reqStr[:crlfIndex], " ")
318+
reqStr = reqStr[:protoSpaceIndex+1] + "HTTP/1.0" + reqStr[crlfIndex:]
319+
_, err = proxyConn.Write([]byte(reqStr))
320+
if err != nil {
321+
logger.Error("failed to send HTTP request to proxy", "error", err)
322+
return
323+
}
324+
} else {
325+
if err := req.WriteProxy(proxyConn); err != nil {
326+
logger.Error("failed to send HTTP request to proxy", "error", err)
327+
return
328+
}
311329
}
312330
resp, err := http.ReadResponse(bufio.NewReader(proxyConn), req)
313331
if err != nil {
@@ -349,7 +367,7 @@ func HandleConn(conn net.Conn, proxy string) {
349367
logger.Info("relay TLS connection to proxy")
350368
RelayTCP(consigned, proxyConn, logger)
351369
}
352-
case 80:
370+
case 80, 11371:
353371
host, err := PrereadHttpHost(consigned)
354372
if err != nil {
355373
logger.Error("failed to preread HTTP host from connection", "error", err)
@@ -367,13 +385,19 @@ func HandleConn(conn net.Conn, proxy string) {
367385
logger.Info("relay HTTP connection to proxy")
368386
RelayHTTP(consigned, proxyConn, logger)
369387
default:
370-
logger.Error(fmt.Sprintf("unknown destination port: %d", dst.Port))
371-
return
388+
logger = logger.With("host", fmt.Sprintf("%s:%d", dst.IP.String(), dst.Port))
389+
proxyConn, err := DialProxyConnect(proxy, fmt.Sprintf("%s:%d", dst.IP.String(), dst.Port))
390+
if err != nil {
391+
logger.Error("failed to connect to tcp proxy", "error", err)
392+
return
393+
}
394+
logger.Info("relay TCP connection to proxy")
395+
RelayTCP(consigned, proxyConn, logger)
372396
}
373397
}
374398

375399
func main() {
376-
proxyFlag := flag.String("proxy", "", "upstream HTTP proxy address in the 'host:port' format")
400+
proxyFlag := flag.String("proxy", "", "upstream proxy address in the 'host:port' format")
377401
listenFlag := flag.String("listen", ":8443", "the address and port on which the server will listen")
378402
flag.Parse()
379403
listenAddr := *listenFlag
@@ -387,7 +411,7 @@ func main() {
387411
slog.Info(fmt.Sprintf("start listening on %s", listenAddr))
388412
proxy := *proxyFlag
389413
if proxy == "" {
390-
log.Fatalf("no upstearm proxy specified")
414+
log.Fatalf("no upstream proxy specified")
391415
}
392416
slog.Info(fmt.Sprintf("start forwarding to proxy %s", proxy))
393417
go func() {

go.mod

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
module aproxy
22

3-
go 1.21
3+
go 1.23.0
44

5-
require golang.org/x/crypto v0.17.0
5+
toolchain go1.24.1
6+
7+
require golang.org/x/crypto v0.35.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
2-
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
1+
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
2+
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=

snap/snapcraft.yaml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: aproxy
2-
version: 0.2.2
2+
version: 0.2.4
33
summary: Transparent proxy for HTTP and HTTPS/TLS connections.
44
description: |
55
Aproxy is a transparent proxy for HTTP and HTTPS/TLS connections. By
@@ -17,6 +17,10 @@ architectures:
1717
build-for: amd64
1818
- build-on: amd64
1919
build-for: arm64
20+
- build-on: amd64
21+
build-for: s390x
22+
- build-on: amd64
23+
build-for: ppc64el
2024

2125
apps:
2226
aproxy:
@@ -35,7 +39,11 @@ parts:
3539
- go
3640
override-build: |
3741
snapcraftctl build
38-
export GOARCH=$SNAPCRAFT_TARGET_ARCH
42+
if [ $SNAPCRAFT_TARGET_ARCH == ppc64el ]; then
43+
export GOARCH=ppc64le
44+
else
45+
export GOARCH=$SNAPCRAFT_TARGET_ARCH
46+
fi
3947
export CGO_ENABLED=0
4048
go mod download
4149
go build -ldflags="-w -s"

0 commit comments

Comments
 (0)