mirror of
https://github.com/fosrl/newt.git
synced 2026-04-10 11:56:37 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd4782265a | ||
|
|
5c329be1f3 | ||
|
|
732e788c66 | ||
|
|
aa42b3623d | ||
|
|
4f42560e26 | ||
|
|
c2187de482 | ||
|
|
5ced7d6909 | ||
|
|
4e6e79ad21 | ||
|
|
abe6e2e400 | ||
|
|
f432a17c16 | ||
|
|
6f96169ff1 | ||
|
|
575942c4be | ||
|
|
16864fc1d7 | ||
|
|
f925c681d2 | ||
|
|
e01b0ae9c7 | ||
|
|
212bdf765a | ||
|
|
b045a0f5d4 | ||
|
|
d7741df514 | ||
|
|
8e188933a2 | ||
|
|
a13c7c6e65 | ||
|
|
bc44ca1aba | ||
|
|
a76089db98 | ||
|
|
627ec2fdbc |
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -1 +0,0 @@
|
|||||||
* @oschwartz10612 @miloschwartz
|
|
||||||
40
.github/workflows/cicd.yml
vendored
40
.github/workflows/cicd.yml
vendored
@@ -235,17 +235,17 @@ jobs:
|
|||||||
# uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
# uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||||
|
|
||||||
#- name: Set up Docker Buildx
|
#- name: Set up Docker Buildx
|
||||||
# uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
# uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||||
|
|
||||||
- name: Log in to GHCR
|
- name: Log in to GHCR
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -259,12 +259,12 @@ jobs:
|
|||||||
echo "DOCKERHUB_IMAGE=${DOCKERHUB_IMAGE,,}" >> "$GITHUB_ENV"
|
echo "DOCKERHUB_IMAGE=${DOCKERHUB_IMAGE,,}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
|
|
||||||
# Build ONLY amd64 and push arch-specific tag suffixes used later for manifest creation.
|
# Build ONLY amd64 and push arch-specific tag suffixes used later for manifest creation.
|
||||||
- name: Build and push (amd64 -> *:amd64-TAG)
|
- name: Build and push (amd64 -> *:amd64-TAG)
|
||||||
id: build_amd
|
id: build_amd
|
||||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
@@ -363,14 +363,14 @@ jobs:
|
|||||||
echo "Checked out $(git rev-parse --short HEAD) for tag ${TAG}"
|
echo "Checked out $(git rev-parse --short HEAD) for tag ${TAG}"
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||||
|
|
||||||
- name: Log in to GHCR
|
- name: Log in to GHCR
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -384,12 +384,12 @@ jobs:
|
|||||||
echo "DOCKERHUB_IMAGE=${DOCKERHUB_IMAGE,,}" >> "$GITHUB_ENV"
|
echo "DOCKERHUB_IMAGE=${DOCKERHUB_IMAGE,,}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
|
|
||||||
# Build ONLY arm64 and push arch-specific tag suffixes used later for manifest creation.
|
# Build ONLY arm64 and push arch-specific tag suffixes used later for manifest creation.
|
||||||
- name: Build and push (arm64 -> *:arm64-TAG)
|
- name: Build and push (arm64 -> *:arm64-TAG)
|
||||||
id: build_arm
|
id: build_arm
|
||||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
@@ -478,14 +478,14 @@ jobs:
|
|||||||
echo "Checked out $(git rev-parse --short HEAD) for tag ${TAG}"
|
echo "Checked out $(git rev-parse --short HEAD) for tag ${TAG}"
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||||
|
|
||||||
- name: Log in to GHCR
|
- name: Log in to GHCR
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -502,11 +502,11 @@ jobs:
|
|||||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
|
|
||||||
- name: Build and push (arm/v7 -> *:armv7-TAG)
|
- name: Build and push (arm/v7 -> *:armv7-TAG)
|
||||||
id: build_armv7
|
id: build_armv7
|
||||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
@@ -551,14 +551,14 @@ jobs:
|
|||||||
#PUBLISH_MINOR: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_minor || vars.PUBLISH_MINOR }}
|
#PUBLISH_MINOR: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_minor || vars.PUBLISH_MINOR }}
|
||||||
steps:
|
steps:
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||||
|
|
||||||
- name: Log in to GHCR
|
- name: Log in to GHCR
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -572,7 +572,7 @@ jobs:
|
|||||||
echo "DOCKERHUB_IMAGE=${DOCKERHUB_IMAGE,,}" >> "$GITHUB_ENV"
|
echo "DOCKERHUB_IMAGE=${DOCKERHUB_IMAGE,,}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Set up Docker Buildx (needed for imagetools)
|
- name: Set up Docker Buildx (needed for imagetools)
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
|
|
||||||
- name: Create & push multi-arch index (GHCR :TAG) via imagetools
|
- name: Create & push multi-arch index (GHCR :TAG) via imagetools
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -656,14 +656,14 @@ jobs:
|
|||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||||
|
|
||||||
- name: Log in to GHCR
|
- name: Log in to GHCR
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -687,7 +687,7 @@ jobs:
|
|||||||
sudo apt-get install -y jq
|
sudo apt-get install -y jq
|
||||||
|
|
||||||
- name: Set up Docker Buildx (needed for imagetools)
|
- name: Set up Docker Buildx (needed for imagetools)
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
|
|
||||||
- name: Resolve multi-arch digest refs (by TAG)
|
- name: Resolve multi-arch digest refs (by TAG)
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -759,7 +759,7 @@ jobs:
|
|||||||
cosign public-key --key env://COSIGN_PRIVATE_KEY >/dev/null
|
cosign public-key --key env://COSIGN_PRIVATE_KEY >/dev/null
|
||||||
|
|
||||||
- name: Generate SBOM (SPDX JSON) from GHCR digest
|
- name: Generate SBOM (SPDX JSON) from GHCR digest
|
||||||
uses: aquasecurity/trivy-action@97e0b3872f55f89b95b2f65b3dbab56962816478 # v0.34.2
|
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
|
||||||
with:
|
with:
|
||||||
image-ref: ${{ env.GHCR_REF }}
|
image-ref: ${{ env.GHCR_REF }}
|
||||||
format: spdx-json
|
format: spdx-json
|
||||||
|
|||||||
2
.github/workflows/stale-bot.yml
vendored
2
.github/workflows/stale-bot.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||||
with:
|
with:
|
||||||
days-before-stale: 14
|
days-before-stale: 14
|
||||||
days-before-close: 14
|
days-before-close: 14
|
||||||
|
|||||||
37
blueprint.yaml
Normal file
37
blueprint.yaml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
resources:
|
||||||
|
resource-nice-id:
|
||||||
|
name: this is my resource
|
||||||
|
protocol: http
|
||||||
|
full-domain: level1.test3.example.com
|
||||||
|
host-header: example.com
|
||||||
|
tls-server-name: example.com
|
||||||
|
auth:
|
||||||
|
pincode: 123456
|
||||||
|
password: sadfasdfadsf
|
||||||
|
sso-enabled: true
|
||||||
|
sso-roles:
|
||||||
|
- Member
|
||||||
|
sso-users:
|
||||||
|
- owen@pangolin.net
|
||||||
|
whitelist-users:
|
||||||
|
- owen@pangolin.net
|
||||||
|
targets:
|
||||||
|
# - site: glossy-plains-viscacha-rat
|
||||||
|
- hostname: localhost
|
||||||
|
method: http
|
||||||
|
port: 8000
|
||||||
|
healthcheck:
|
||||||
|
port: 8000
|
||||||
|
hostname: localhost
|
||||||
|
# - site: glossy-plains-viscacha-rat
|
||||||
|
- hostname: localhost
|
||||||
|
method: http
|
||||||
|
port: 8001
|
||||||
|
resource-nice-id2:
|
||||||
|
name: this is other resource
|
||||||
|
protocol: tcp
|
||||||
|
proxy-port: 3000
|
||||||
|
targets:
|
||||||
|
# - site: glossy-plains-viscacha-rat
|
||||||
|
- hostname: localhost
|
||||||
|
port: 3000
|
||||||
@@ -40,17 +40,13 @@ type WgConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Target struct {
|
type Target struct {
|
||||||
SourcePrefix string `json:"sourcePrefix"`
|
SourcePrefix string `json:"sourcePrefix"`
|
||||||
SourcePrefixes []string `json:"sourcePrefixes"`
|
SourcePrefixes []string `json:"sourcePrefixes"`
|
||||||
DestPrefix string `json:"destPrefix"`
|
DestPrefix string `json:"destPrefix"`
|
||||||
RewriteTo string `json:"rewriteTo,omitempty"`
|
RewriteTo string `json:"rewriteTo,omitempty"`
|
||||||
DisableIcmp bool `json:"disableIcmp,omitempty"`
|
DisableIcmp bool `json:"disableIcmp,omitempty"`
|
||||||
PortRange []PortRange `json:"portRange,omitempty"`
|
PortRange []PortRange `json:"portRange,omitempty"`
|
||||||
ResourceId int `json:"resourceId,omitempty"`
|
ResourceId int `json:"resourceId,omitempty"`
|
||||||
Protocol string `json:"protocol,omitempty"` // for now practicably either http or https
|
|
||||||
HTTPTargets []netstack2.HTTPTarget `json:"httpTargets,omitempty"` // for http protocol, list of downstream services to load balance across
|
|
||||||
TLSCert string `json:"tlsCert,omitempty"` // PEM-encoded certificate for incoming HTTPS termination
|
|
||||||
TLSKey string `json:"tlsKey,omitempty"` // PEM-encoded private key for incoming HTTPS termination
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PortRange struct {
|
type PortRange struct {
|
||||||
@@ -78,18 +74,18 @@ type PeerReading struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type WireGuardService struct {
|
type WireGuardService struct {
|
||||||
interfaceName string
|
interfaceName string
|
||||||
mtu int
|
mtu int
|
||||||
client *websocket.Client
|
client *websocket.Client
|
||||||
config WgConfig
|
config WgConfig
|
||||||
key wgtypes.Key
|
key wgtypes.Key
|
||||||
newtId string
|
newtId string
|
||||||
lastReadings map[string]PeerReading
|
lastReadings map[string]PeerReading
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
Port uint16
|
Port uint16
|
||||||
host string
|
host string
|
||||||
serverPubKey string
|
serverPubKey string
|
||||||
token string
|
token string
|
||||||
stopGetConfig func()
|
stopGetConfig func()
|
||||||
pendingConfigChainId string
|
pendingConfigChainId string
|
||||||
// Netstack fields
|
// Netstack fields
|
||||||
@@ -701,18 +697,7 @@ func (s *WireGuardService) syncTargets(desiredTargets []Target) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
s.tnet.AddProxySubnetRule(netstack2.SubnetRule{
|
s.tnet.AddProxySubnetRule(sourcePrefix, destPrefix, target.RewriteTo, portRanges, target.DisableIcmp, target.ResourceId)
|
||||||
SourcePrefix: sourcePrefix,
|
|
||||||
DestPrefix: destPrefix,
|
|
||||||
RewriteTo: target.RewriteTo,
|
|
||||||
PortRanges: portRanges,
|
|
||||||
DisableIcmp: target.DisableIcmp,
|
|
||||||
ResourceId: target.ResourceId,
|
|
||||||
Protocol: target.Protocol,
|
|
||||||
HTTPTargets: target.HTTPTargets,
|
|
||||||
TLSCert: target.TLSCert,
|
|
||||||
TLSKey: target.TLSKey,
|
|
||||||
})
|
|
||||||
logger.Info("Added target %s -> %s during sync", target.SourcePrefix, target.DestPrefix)
|
logger.Info("Added target %s -> %s during sync", target.SourcePrefix, target.DestPrefix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -970,18 +955,7 @@ func (s *WireGuardService) ensureTargets(targets []Target) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid CIDR %s: %v", sp, err)
|
return fmt.Errorf("invalid CIDR %s: %v", sp, err)
|
||||||
}
|
}
|
||||||
s.tnet.AddProxySubnetRule(netstack2.SubnetRule{
|
s.tnet.AddProxySubnetRule(sourcePrefix, destPrefix, target.RewriteTo, portRanges, target.DisableIcmp, target.ResourceId)
|
||||||
SourcePrefix: sourcePrefix,
|
|
||||||
DestPrefix: destPrefix,
|
|
||||||
RewriteTo: target.RewriteTo,
|
|
||||||
PortRanges: portRanges,
|
|
||||||
DisableIcmp: target.DisableIcmp,
|
|
||||||
ResourceId: target.ResourceId,
|
|
||||||
Protocol: target.Protocol,
|
|
||||||
HTTPTargets: target.HTTPTargets,
|
|
||||||
TLSCert: target.TLSCert,
|
|
||||||
TLSKey: target.TLSKey,
|
|
||||||
})
|
|
||||||
logger.Info("Added target subnet from %s to %s rewrite to %s with port ranges: %v", sp, target.DestPrefix, target.RewriteTo, target.PortRange)
|
logger.Info("Added target subnet from %s to %s rewrite to %s with port ranges: %v", sp, target.DestPrefix, target.RewriteTo, target.PortRange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1374,18 +1348,7 @@ func (s *WireGuardService) handleAddTarget(msg websocket.WSMessage) {
|
|||||||
logger.Info("Invalid CIDR %s: %v", sp, err)
|
logger.Info("Invalid CIDR %s: %v", sp, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.tnet.AddProxySubnetRule(netstack2.SubnetRule{
|
s.tnet.AddProxySubnetRule(sourcePrefix, destPrefix, target.RewriteTo, portRanges, target.DisableIcmp, target.ResourceId)
|
||||||
SourcePrefix: sourcePrefix,
|
|
||||||
DestPrefix: destPrefix,
|
|
||||||
RewriteTo: target.RewriteTo,
|
|
||||||
PortRanges: portRanges,
|
|
||||||
DisableIcmp: target.DisableIcmp,
|
|
||||||
ResourceId: target.ResourceId,
|
|
||||||
Protocol: target.Protocol,
|
|
||||||
HTTPTargets: target.HTTPTargets,
|
|
||||||
TLSCert: target.TLSCert,
|
|
||||||
TLSKey: target.TLSKey,
|
|
||||||
})
|
|
||||||
logger.Info("Added target subnet from %s to %s rewrite to %s with port ranges: %v", sp, target.DestPrefix, target.RewriteTo, target.PortRange)
|
logger.Info("Added target subnet from %s to %s rewrite to %s with port ranges: %v", sp, target.DestPrefix, target.RewriteTo, target.PortRange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1503,18 +1466,7 @@ func (s *WireGuardService) handleUpdateTarget(msg websocket.WSMessage) {
|
|||||||
logger.Info("Invalid CIDR %s: %v", sp, err)
|
logger.Info("Invalid CIDR %s: %v", sp, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.tnet.AddProxySubnetRule(netstack2.SubnetRule{
|
s.tnet.AddProxySubnetRule(sourcePrefix, destPrefix, target.RewriteTo, portRanges, target.DisableIcmp, target.ResourceId)
|
||||||
SourcePrefix: sourcePrefix,
|
|
||||||
DestPrefix: destPrefix,
|
|
||||||
RewriteTo: target.RewriteTo,
|
|
||||||
PortRanges: portRanges,
|
|
||||||
DisableIcmp: target.DisableIcmp,
|
|
||||||
ResourceId: target.ResourceId,
|
|
||||||
Protocol: target.Protocol,
|
|
||||||
HTTPTargets: target.HTTPTargets,
|
|
||||||
TLSCert: target.TLSCert,
|
|
||||||
TLSKey: target.TLSKey,
|
|
||||||
})
|
|
||||||
logger.Info("Added target subnet from %s to %s rewrite to %s with port ranges: %v", sp, target.DestPrefix, target.RewriteTo, target.PortRange)
|
logger.Info("Added target subnet from %s to %s rewrite to %s with port ranges: %v", sp, target.DestPrefix, target.RewriteTo, target.PortRange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
inherit version;
|
inherit version;
|
||||||
src = pkgs.nix-gitignore.gitignoreSource [ ] ./.;
|
src = pkgs.nix-gitignore.gitignoreSource [ ] ./.;
|
||||||
|
|
||||||
vendorHash = "sha256-kmQM8Yy5TuOiNpMpUme/2gfE+vrhUK+0AphN+p71wGs=";
|
vendorHash = "sha256-YIcuj1S+ZWAzXZOMZbppTvsDcW1W1Sy8ynfMkzLMQpM=";
|
||||||
|
|
||||||
nativeInstallCheckInputs = [ pkgs.versionCheckHook ];
|
nativeInstallCheckInputs = [ pkgs.versionCheckHook ];
|
||||||
|
|
||||||
|
|||||||
4
go.mod
4
go.mod
@@ -4,7 +4,7 @@ go 1.25.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/docker/docker v28.5.2+incompatible
|
github.com/docker/docker v28.5.2+incompatible
|
||||||
github.com/gaissmai/bart v0.26.0
|
github.com/gaissmai/bart v0.26.1
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/vishvananda/netlink v1.3.1
|
github.com/vishvananda/netlink v1.3.1
|
||||||
@@ -24,7 +24,7 @@ require (
|
|||||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3
|
golang.zx2c4.com/wireguard/windows v0.5.3
|
||||||
google.golang.org/grpc v1.79.1
|
google.golang.org/grpc v1.79.3
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c
|
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c
|
||||||
software.sslmate.com/src/go-pkcs12 v0.7.0
|
software.sslmate.com/src/go-pkcs12 v0.7.0
|
||||||
|
|||||||
8
go.sum
8
go.sum
@@ -26,8 +26,8 @@ github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw
|
|||||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/gaissmai/bart v0.26.0 h1:xOZ57E9hJLBiQaSyeZa9wgWhGuzfGACgqp4BE77OkO0=
|
github.com/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo=
|
||||||
github.com/gaissmai/bart v0.26.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
@@ -159,8 +159,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:
|
|||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -137,31 +137,14 @@ func (h *TCPHandler) InstallTCPHandler() error {
|
|||||||
|
|
||||||
// handleTCPConn handles a TCP connection by proxying it to the actual target
|
// handleTCPConn handles a TCP connection by proxying it to the actual target
|
||||||
func (h *TCPHandler) handleTCPConn(netstackConn *gonet.TCPConn, id stack.TransportEndpointID) {
|
func (h *TCPHandler) handleTCPConn(netstackConn *gonet.TCPConn, id stack.TransportEndpointID) {
|
||||||
// Extract source and target address from the connection ID first so they
|
defer netstackConn.Close()
|
||||||
// are available for HTTP routing before any defer is set up.
|
|
||||||
|
// Extract source and target address from the connection ID
|
||||||
srcIP := id.RemoteAddress.String()
|
srcIP := id.RemoteAddress.String()
|
||||||
srcPort := id.RemotePort
|
srcPort := id.RemotePort
|
||||||
dstIP := id.LocalAddress.String()
|
dstIP := id.LocalAddress.String()
|
||||||
dstPort := id.LocalPort
|
dstPort := id.LocalPort
|
||||||
|
|
||||||
// For HTTP/HTTPS ports, look up the matching subnet rule. If the rule has
|
|
||||||
// Protocol configured, hand the connection off to the HTTP handler which
|
|
||||||
// takes full ownership of the lifecycle (the defer close must not be
|
|
||||||
// installed before this point).
|
|
||||||
if (dstPort == 80 || dstPort == 443) && h.proxyHandler != nil && h.proxyHandler.httpHandler != nil {
|
|
||||||
srcAddr, _ := netip.ParseAddr(srcIP)
|
|
||||||
dstAddr, _ := netip.ParseAddr(dstIP)
|
|
||||||
rule := h.proxyHandler.subnetLookup.Match(srcAddr, dstAddr, dstPort, tcp.ProtocolNumber)
|
|
||||||
if rule != nil && rule.Protocol != "" {
|
|
||||||
logger.Info("TCP Forwarder: Routing %s:%d -> %s:%d to HTTP handler (%s)",
|
|
||||||
srcIP, srcPort, dstIP, dstPort, rule.Protocol)
|
|
||||||
h.proxyHandler.httpHandler.HandleConn(netstackConn, rule)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defer netstackConn.Close()
|
|
||||||
|
|
||||||
logger.Info("TCP Forwarder: Handling connection %s:%d -> %s:%d", srcIP, srcPort, dstIP, dstPort)
|
logger.Info("TCP Forwarder: Handling connection %s:%d -> %s:%d", srcIP, srcPort, dstIP, dstPort)
|
||||||
|
|
||||||
// Check if there's a destination rewrite for this connection (e.g., localhost targets)
|
// Check if there's a destination rewrite for this connection (e.g., localhost targets)
|
||||||
|
|||||||
@@ -1,318 +0,0 @@
|
|||||||
/* SPDX-License-Identifier: MIT
|
|
||||||
*
|
|
||||||
* Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package netstack2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httputil"
|
|
||||||
"net/url"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/fosrl/newt/logger"
|
|
||||||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// HTTPTarget
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// HTTPTarget describes a single downstream HTTP or HTTPS service that the
|
|
||||||
// proxy should forward requests to.
|
|
||||||
type HTTPTarget struct {
|
|
||||||
DestAddr string `json:"destAddr"` // IP address or hostname of the downstream service
|
|
||||||
DestPort uint16 `json:"destPort"` // TCP port of the downstream service
|
|
||||||
UseHTTPS bool `json:"useHttps"` // When true the outbound leg uses HTTPS
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// HTTPHandler
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// HTTPHandler intercepts TCP connections from the netstack forwarder on ports
|
|
||||||
// 80 and 443 and services them as HTTP or HTTPS, reverse-proxying each request
|
|
||||||
// to downstream targets specified by the matching SubnetRule.
|
|
||||||
//
|
|
||||||
// HTTP and raw TCP are fully separate: a connection is only routed here when
|
|
||||||
// its SubnetRule has Protocol set ("http" or "https"). All other connections
|
|
||||||
// on those ports fall through to the normal raw-TCP path.
|
|
||||||
//
|
|
||||||
// Incoming TLS termination (Protocol == "https") is performed per-connection
|
|
||||||
// using the certificate and key stored in the rule, so different subnet rules
|
|
||||||
// can present different certificates without sharing any state.
|
|
||||||
//
|
|
||||||
// Outbound connections to downstream targets honour HTTPTarget.UseHTTPS
|
|
||||||
// independently of the incoming protocol.
|
|
||||||
type HTTPHandler struct {
|
|
||||||
stack *stack.Stack
|
|
||||||
proxyHandler *ProxyHandler
|
|
||||||
|
|
||||||
listener *chanListener
|
|
||||||
server *http.Server
|
|
||||||
|
|
||||||
// proxyCache holds pre-built *httputil.ReverseProxy values keyed by the
|
|
||||||
// canonical target URL string ("scheme://host:port"). Building a proxy is
|
|
||||||
// cheap, but reusing one preserves the underlying http.Transport connection
|
|
||||||
// pool, which matters for throughput.
|
|
||||||
proxyCache sync.Map // map[string]*httputil.ReverseProxy
|
|
||||||
|
|
||||||
// tlsCache holds pre-parsed *tls.Config values keyed by the concatenation
|
|
||||||
// of the PEM certificate and key. Parsing a keypair is relatively expensive
|
|
||||||
// and the same cert is likely reused across many connections.
|
|
||||||
tlsCache sync.Map // map[string]*tls.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// chanListener – net.Listener backed by a channel
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// chanListener implements net.Listener by receiving net.Conn values over a
|
|
||||||
// buffered channel. This lets the netstack TCP forwarder hand off connections
|
|
||||||
// directly to a running http.Server without any real OS socket.
|
|
||||||
type chanListener struct {
|
|
||||||
connCh chan net.Conn
|
|
||||||
closed chan struct{}
|
|
||||||
once sync.Once
|
|
||||||
}
|
|
||||||
|
|
||||||
func newChanListener() *chanListener {
|
|
||||||
return &chanListener{
|
|
||||||
connCh: make(chan net.Conn, 128),
|
|
||||||
closed: make(chan struct{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept blocks until a connection is available or the listener is closed.
|
|
||||||
func (l *chanListener) Accept() (net.Conn, error) {
|
|
||||||
select {
|
|
||||||
case conn, ok := <-l.connCh:
|
|
||||||
if !ok {
|
|
||||||
return nil, net.ErrClosed
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
case <-l.closed:
|
|
||||||
return nil, net.ErrClosed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close shuts down the listener; subsequent Accept calls return net.ErrClosed.
|
|
||||||
func (l *chanListener) Close() error {
|
|
||||||
l.once.Do(func() { close(l.closed) })
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Addr returns a placeholder address (the listener has no real OS socket).
|
|
||||||
func (l *chanListener) Addr() net.Addr {
|
|
||||||
return &net.TCPAddr{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// send delivers conn to the listener. Returns false if the listener is already
|
|
||||||
// closed, in which case the caller is responsible for closing conn.
|
|
||||||
func (l *chanListener) send(conn net.Conn) bool {
|
|
||||||
select {
|
|
||||||
case l.connCh <- conn:
|
|
||||||
return true
|
|
||||||
case <-l.closed:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// httpConnCtx – conn wrapper that carries a SubnetRule through the listener
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// httpConnCtx wraps a net.Conn so the matching SubnetRule can be passed
|
|
||||||
// through the chanListener into the http.Server's ConnContext callback,
|
|
||||||
// making it available to request handlers via the request context.
|
|
||||||
type httpConnCtx struct {
|
|
||||||
net.Conn
|
|
||||||
rule *SubnetRule
|
|
||||||
}
|
|
||||||
|
|
||||||
// connCtxKey is the unexported context key used to store a *SubnetRule on the
|
|
||||||
// per-connection context created by http.Server.ConnContext.
|
|
||||||
type connCtxKey struct{}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Constructor and lifecycle
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// NewHTTPHandler creates an HTTPHandler attached to the given stack and
|
|
||||||
// ProxyHandler. Call Start to begin serving connections.
|
|
||||||
func NewHTTPHandler(s *stack.Stack, ph *ProxyHandler) *HTTPHandler {
|
|
||||||
return &HTTPHandler{
|
|
||||||
stack: s,
|
|
||||||
proxyHandler: ph,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start launches the internal http.Server that services connections delivered
|
|
||||||
// via HandleConn. The server runs for the lifetime of the HTTPHandler; call
|
|
||||||
// Close to stop it.
|
|
||||||
func (h *HTTPHandler) Start() error {
|
|
||||||
h.listener = newChanListener()
|
|
||||||
|
|
||||||
h.server = &http.Server{
|
|
||||||
Handler: http.HandlerFunc(h.handleRequest),
|
|
||||||
// ConnContext runs once per accepted connection and attaches the
|
|
||||||
// SubnetRule carried by httpConnCtx to the connection's context so
|
|
||||||
// that handleRequest can retrieve it without any global state.
|
|
||||||
ConnContext: func(ctx context.Context, c net.Conn) context.Context {
|
|
||||||
if cc, ok := c.(*httpConnCtx); ok {
|
|
||||||
return context.WithValue(ctx, connCtxKey{}, cc.rule)
|
|
||||||
}
|
|
||||||
return ctx
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
if err := h.server.Serve(h.listener); err != nil && err != http.ErrServerClosed {
|
|
||||||
logger.Error("HTTP handler: server exited unexpectedly: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
logger.Info("HTTP handler: ready — routing determined per SubnetRule on ports 80/443")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleConn accepts a TCP connection from the netstack forwarder together
|
|
||||||
// with the SubnetRule that matched it. The HTTP handler takes full ownership
|
|
||||||
// of the connection's lifecycle; the caller must NOT close conn after this call.
|
|
||||||
//
|
|
||||||
// When rule.Protocol is "https", TLS termination is performed on conn using
|
|
||||||
// the certificate and key stored in rule.TLSCert and rule.TLSKey before the
|
|
||||||
// connection is passed to the HTTP server. The HTTP server itself is always
|
|
||||||
// plain-HTTP; TLS is fully unwrapped at this layer.
|
|
||||||
func (h *HTTPHandler) HandleConn(conn net.Conn, rule *SubnetRule) {
|
|
||||||
var effectiveConn net.Conn = conn
|
|
||||||
|
|
||||||
if rule.Protocol == "https" {
|
|
||||||
tlsCfg, err := h.getTLSConfig(rule)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("HTTP handler: cannot build TLS config for connection from %s: %v",
|
|
||||||
conn.RemoteAddr(), err)
|
|
||||||
conn.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// tls.Server wraps the raw conn; the TLS handshake is deferred until
|
|
||||||
// the first Read, which the http.Server will trigger naturally.
|
|
||||||
effectiveConn = tls.Server(conn, tlsCfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapped := &httpConnCtx{Conn: effectiveConn, rule: rule}
|
|
||||||
if !h.listener.send(wrapped) {
|
|
||||||
// Listener is already closed — clean up the orphaned connection.
|
|
||||||
effectiveConn.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close gracefully shuts down the HTTP server and the underlying channel
|
|
||||||
// listener, causing the goroutine started in Start to exit.
|
|
||||||
func (h *HTTPHandler) Close() error {
|
|
||||||
if h.server != nil {
|
|
||||||
if err := h.server.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if h.listener != nil {
|
|
||||||
h.listener.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Internal helpers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// getTLSConfig returns a *tls.Config for the cert/key pair in rule, using a
|
|
||||||
// cache to avoid re-parsing the same keypair on every connection.
|
|
||||||
// The cache key is the concatenation of the PEM cert and key strings, so
|
|
||||||
// different rules that happen to share the same material hit the same entry.
|
|
||||||
func (h *HTTPHandler) getTLSConfig(rule *SubnetRule) (*tls.Config, error) {
|
|
||||||
cacheKey := rule.TLSCert + "|" + rule.TLSKey
|
|
||||||
if v, ok := h.tlsCache.Load(cacheKey); ok {
|
|
||||||
return v.(*tls.Config), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cert, err := tls.X509KeyPair([]byte(rule.TLSCert), []byte(rule.TLSKey))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse TLS keypair: %w", err)
|
|
||||||
}
|
|
||||||
cfg := &tls.Config{
|
|
||||||
Certificates: []tls.Certificate{cert},
|
|
||||||
}
|
|
||||||
// LoadOrStore is safe under concurrent calls: if two goroutines race here
|
|
||||||
// both will produce a valid config; the loser's work is discarded.
|
|
||||||
actual, _ := h.tlsCache.LoadOrStore(cacheKey, cfg)
|
|
||||||
return actual.(*tls.Config), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getProxy returns a cached *httputil.ReverseProxy for the given target,
|
|
||||||
// creating one on first use. Reusing the proxy preserves its http.Transport
|
|
||||||
// connection pool, avoiding repeated TCP/TLS handshakes to the downstream.
|
|
||||||
func (h *HTTPHandler) getProxy(target HTTPTarget) *httputil.ReverseProxy {
|
|
||||||
scheme := "http"
|
|
||||||
if target.UseHTTPS {
|
|
||||||
scheme = "https"
|
|
||||||
}
|
|
||||||
cacheKey := fmt.Sprintf("%s://%s:%d", scheme, target.DestAddr, target.DestPort)
|
|
||||||
|
|
||||||
if v, ok := h.proxyCache.Load(cacheKey); ok {
|
|
||||||
return v.(*httputil.ReverseProxy)
|
|
||||||
}
|
|
||||||
|
|
||||||
targetURL := &url.URL{
|
|
||||||
Scheme: scheme,
|
|
||||||
Host: fmt.Sprintf("%s:%d", target.DestAddr, target.DestPort),
|
|
||||||
}
|
|
||||||
proxy := httputil.NewSingleHostReverseProxy(targetURL)
|
|
||||||
|
|
||||||
if target.UseHTTPS {
|
|
||||||
// Allow self-signed certificates on downstream HTTPS targets.
|
|
||||||
proxy.Transport = &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
InsecureSkipVerify: true, //nolint:gosec // downstream self-signed certs are a supported configuration
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
|
||||||
logger.Error("HTTP handler: upstream error (%s %s -> %s): %v",
|
|
||||||
r.Method, r.URL.RequestURI(), cacheKey, err)
|
|
||||||
http.Error(w, "Bad Gateway", http.StatusBadGateway)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual, _ := h.proxyCache.LoadOrStore(cacheKey, proxy)
|
|
||||||
return actual.(*httputil.ReverseProxy)
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleRequest is the http.Handler entry point. It retrieves the SubnetRule
|
|
||||||
// attached to the connection by ConnContext, selects the first configured
|
|
||||||
// downstream target, and forwards the request via the cached ReverseProxy.
|
|
||||||
//
|
|
||||||
// TODO: add host/path-based routing across multiple HTTPTargets once the
|
|
||||||
// configuration model evolves beyond a single target per rule.
|
|
||||||
func (h *HTTPHandler) handleRequest(w http.ResponseWriter, r *http.Request) {
|
|
||||||
rule, _ := r.Context().Value(connCtxKey{}).(*SubnetRule)
|
|
||||||
if rule == nil || len(rule.HTTPTargets) == 0 {
|
|
||||||
logger.Error("HTTP handler: no downstream targets for request %s %s", r.Method, r.URL.RequestURI())
|
|
||||||
http.Error(w, "no targets configured", http.StatusBadGateway)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
target := rule.HTTPTargets[0]
|
|
||||||
scheme := "http"
|
|
||||||
if target.UseHTTPS {
|
|
||||||
scheme = "https"
|
|
||||||
}
|
|
||||||
logger.Info("HTTP handler: %s %s -> %s://%s:%d",
|
|
||||||
r.Method, r.URL.RequestURI(), scheme, target.DestAddr, target.DestPort)
|
|
||||||
|
|
||||||
h.getProxy(target).ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
@@ -53,14 +53,6 @@ type SubnetRule struct {
|
|||||||
RewriteTo string // Optional rewrite address for DNAT - can be IP/CIDR or domain name
|
RewriteTo string // Optional rewrite address for DNAT - can be IP/CIDR or domain name
|
||||||
PortRanges []PortRange // empty slice means all ports allowed
|
PortRanges []PortRange // empty slice means all ports allowed
|
||||||
ResourceId int // Optional resource ID from the server for access logging
|
ResourceId int // Optional resource ID from the server for access logging
|
||||||
|
|
||||||
// HTTP proxy configuration (optional).
|
|
||||||
// When Protocol is non-empty the TCP connection is handled by HTTPHandler
|
|
||||||
// instead of the raw TCP forwarder.
|
|
||||||
Protocol string // "", "http", or "https" — controls the incoming (client-facing) protocol
|
|
||||||
HTTPTargets []HTTPTarget // downstream services to proxy requests to
|
|
||||||
TLSCert string // PEM-encoded certificate for incoming HTTPS termination
|
|
||||||
TLSKey string // PEM-encoded private key for incoming HTTPS termination
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllRules returns a copy of all subnet rules
|
// GetAllRules returns a copy of all subnet rules
|
||||||
@@ -122,7 +114,6 @@ type ProxyHandler struct {
|
|||||||
tcpHandler *TCPHandler
|
tcpHandler *TCPHandler
|
||||||
udpHandler *UDPHandler
|
udpHandler *UDPHandler
|
||||||
icmpHandler *ICMPHandler
|
icmpHandler *ICMPHandler
|
||||||
httpHandler *HTTPHandler
|
|
||||||
subnetLookup *SubnetLookup
|
subnetLookup *SubnetLookup
|
||||||
natTable map[connKey]*natState
|
natTable map[connKey]*natState
|
||||||
reverseNatTable map[reverseConnKey]*natState // Reverse lookup map for O(1) reply packet NAT
|
reverseNatTable map[reverseConnKey]*natState // Reverse lookup map for O(1) reply packet NAT
|
||||||
@@ -173,21 +164,12 @@ func NewProxyHandler(options ProxyHandlerOptions) (*ProxyHandler, error) {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize TCP handler if enabled. The HTTP handler piggybacks on the
|
// Initialize TCP handler if enabled
|
||||||
// TCP forwarder — TCPHandler.handleTCPConn checks the subnet rule for
|
|
||||||
// ports 80/443 and routes matching connections to the HTTP handler, so
|
|
||||||
// the HTTP handler is always initialised alongside TCP.
|
|
||||||
if options.EnableTCP {
|
if options.EnableTCP {
|
||||||
handler.tcpHandler = NewTCPHandler(handler.proxyStack, handler)
|
handler.tcpHandler = NewTCPHandler(handler.proxyStack, handler)
|
||||||
if err := handler.tcpHandler.InstallTCPHandler(); err != nil {
|
if err := handler.tcpHandler.InstallTCPHandler(); err != nil {
|
||||||
return nil, fmt.Errorf("failed to install TCP handler: %v", err)
|
return nil, fmt.Errorf("failed to install TCP handler: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.httpHandler = NewHTTPHandler(handler.proxyStack, handler)
|
|
||||||
if err := handler.httpHandler.Start(); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to start HTTP handler: %v", err)
|
|
||||||
}
|
|
||||||
logger.Debug("ProxyHandler: HTTP handler enabled")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize UDP handler if enabled
|
// Initialize UDP handler if enabled
|
||||||
@@ -226,14 +208,16 @@ func NewProxyHandler(options ProxyHandlerOptions) (*ProxyHandler, error) {
|
|||||||
return handler, nil
|
return handler, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddSubnetRule adds a subnet rule to the proxy handler.
|
// AddSubnetRule adds a subnet with optional port restrictions to the proxy handler
|
||||||
// HTTP proxy behaviour is configured via rule.Protocol, rule.HTTPTargets,
|
// sourcePrefix: The IP prefix of the peer sending the data
|
||||||
// rule.TLSCert, and rule.TLSKey; leave Protocol empty for raw TCP/UDP.
|
// destPrefix: The IP prefix of the destination
|
||||||
func (p *ProxyHandler) AddSubnetRule(rule SubnetRule) {
|
// rewriteTo: Optional address to rewrite destination to - can be IP/CIDR or domain name
|
||||||
|
// If portRanges is nil or empty, all ports are allowed for this subnet
|
||||||
|
func (p *ProxyHandler) AddSubnetRule(sourcePrefix, destPrefix netip.Prefix, rewriteTo string, portRanges []PortRange, disableIcmp bool, resourceId int) {
|
||||||
if p == nil || !p.enabled {
|
if p == nil || !p.enabled {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
p.subnetLookup.AddSubnet(rule)
|
p.subnetLookup.AddSubnet(sourcePrefix, destPrefix, rewriteTo, portRanges, disableIcmp, resourceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveSubnetRule removes a subnet from the proxy handler
|
// RemoveSubnetRule removes a subnet from the proxy handler
|
||||||
@@ -810,11 +794,6 @@ func (p *ProxyHandler) Close() error {
|
|||||||
p.accessLogger.Close()
|
p.accessLogger.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shut down HTTP handler
|
|
||||||
if p.httpHandler != nil {
|
|
||||||
p.httpHandler.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close ICMP replies channel
|
// Close ICMP replies channel
|
||||||
if p.icmpReplies != nil {
|
if p.icmpReplies != nil {
|
||||||
close(p.icmpReplies)
|
close(p.icmpReplies)
|
||||||
|
|||||||
@@ -44,18 +44,24 @@ func prefixEqual(a, b netip.Prefix) bool {
|
|||||||
return a.Masked() == b.Masked()
|
return a.Masked() == b.Masked()
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddSubnet adds a subnet rule to the lookup table.
|
// AddSubnet adds a subnet rule with source and destination prefixes and optional port restrictions
|
||||||
// If rule.PortRanges is nil or empty, all ports are allowed.
|
// If portRanges is nil or empty, all ports are allowed for this subnet
|
||||||
// rule.RewriteTo can be either an IP/CIDR (e.g., "192.168.1.1/32") or a domain name (e.g., "example.com").
|
// rewriteTo can be either an IP/CIDR (e.g., "192.168.1.1/32") or a domain name (e.g., "example.com")
|
||||||
// HTTP proxy behaviour is driven by rule.Protocol, rule.HTTPTargets, rule.TLSCert, and rule.TLSKey.
|
func (sl *SubnetLookup) AddSubnet(sourcePrefix, destPrefix netip.Prefix, rewriteTo string, portRanges []PortRange, disableIcmp bool, resourceId int) {
|
||||||
func (sl *SubnetLookup) AddSubnet(rule SubnetRule) {
|
|
||||||
sl.mu.Lock()
|
sl.mu.Lock()
|
||||||
defer sl.mu.Unlock()
|
defer sl.mu.Unlock()
|
||||||
|
|
||||||
rulePtr := &rule
|
rule := &SubnetRule{
|
||||||
|
SourcePrefix: sourcePrefix,
|
||||||
|
DestPrefix: destPrefix,
|
||||||
|
DisableIcmp: disableIcmp,
|
||||||
|
RewriteTo: rewriteTo,
|
||||||
|
PortRanges: portRanges,
|
||||||
|
ResourceId: resourceId,
|
||||||
|
}
|
||||||
|
|
||||||
// Canonicalize source prefix to handle host bits correctly
|
// Canonicalize source prefix to handle host bits correctly
|
||||||
canonicalSourcePrefix := rule.SourcePrefix.Masked()
|
canonicalSourcePrefix := sourcePrefix.Masked()
|
||||||
|
|
||||||
// Get or create destination trie for this source prefix
|
// Get or create destination trie for this source prefix
|
||||||
destTriePtr, exists := sl.sourceTrie.Get(canonicalSourcePrefix)
|
destTriePtr, exists := sl.sourceTrie.Get(canonicalSourcePrefix)
|
||||||
@@ -70,12 +76,12 @@ func (sl *SubnetLookup) AddSubnet(rule SubnetRule) {
|
|||||||
|
|
||||||
// Canonicalize destination prefix to handle host bits correctly
|
// Canonicalize destination prefix to handle host bits correctly
|
||||||
// BART masks prefixes internally, so we need to match that behavior in our bookkeeping
|
// BART masks prefixes internally, so we need to match that behavior in our bookkeeping
|
||||||
canonicalDestPrefix := rule.DestPrefix.Masked()
|
canonicalDestPrefix := destPrefix.Masked()
|
||||||
|
|
||||||
// Add rule to destination trie
|
// Add rule to destination trie
|
||||||
// Original behavior: overwrite if same (sourcePrefix, destPrefix) exists
|
// Original behavior: overwrite if same (sourcePrefix, destPrefix) exists
|
||||||
// Store as single-element slice to match original overwrite behavior
|
// Store as single-element slice to match original overwrite behavior
|
||||||
destTriePtr.trie.Insert(canonicalDestPrefix, []*SubnetRule{rulePtr})
|
destTriePtr.trie.Insert(canonicalDestPrefix, []*SubnetRule{rule})
|
||||||
|
|
||||||
// Update destTriePtr.rules - remove old rule with same canonical prefix if exists, then add new one
|
// Update destTriePtr.rules - remove old rule with same canonical prefix if exists, then add new one
|
||||||
// Use canonical comparison to handle cases like 10.0.0.5/24 vs 10.0.0.0/24
|
// Use canonical comparison to handle cases like 10.0.0.5/24 vs 10.0.0.0/24
|
||||||
@@ -85,7 +91,7 @@ func (sl *SubnetLookup) AddSubnet(rule SubnetRule) {
|
|||||||
newRules = append(newRules, r)
|
newRules = append(newRules, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newRules = append(newRules, rulePtr)
|
newRules = append(newRules, rule)
|
||||||
destTriePtr.rules = newRules
|
destTriePtr.rules = newRules
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -351,13 +351,13 @@ func (net *Net) ListenUDP(laddr *net.UDPAddr) (*gonet.UDPConn, error) {
|
|||||||
return net.DialUDP(laddr, nil)
|
return net.DialUDP(laddr, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddProxySubnetRule adds a subnet rule to the proxy handler.
|
// AddProxySubnetRule adds a subnet rule to the proxy handler
|
||||||
// HTTP proxy behaviour is configured via rule.Protocol, rule.HTTPTargets,
|
// If portRanges is nil or empty, all ports are allowed for this subnet
|
||||||
// rule.TLSCert, and rule.TLSKey; leave Protocol empty for raw TCP/UDP.
|
// rewriteTo can be either an IP/CIDR (e.g., "192.168.1.1/32") or a domain name (e.g., "example.com")
|
||||||
func (net *Net) AddProxySubnetRule(rule SubnetRule) {
|
func (net *Net) AddProxySubnetRule(sourcePrefix, destPrefix netip.Prefix, rewriteTo string, portRanges []PortRange, disableIcmp bool, resourceId int) {
|
||||||
tun := (*netTun)(net)
|
tun := (*netTun)(net)
|
||||||
if tun.proxyHandler != nil {
|
if tun.proxyHandler != nil {
|
||||||
tun.proxyHandler.AddSubnetRule(rule)
|
tun.proxyHandler.AddSubnetRule(sourcePrefix, destPrefix, rewriteTo, portRanges, disableIcmp, resourceId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,11 +71,6 @@ func (c *Client) loadConfig() error {
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(bytes.TrimSpace(data)) == 0 {
|
|
||||||
logger.Info("Config file at %s is empty, will initialize it with provided values", configPath)
|
|
||||||
c.configNeedsSave = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var config Config
|
var config Config
|
||||||
if err := json.Unmarshal(data, &config); err != nil {
|
if err := json.Unmarshal(data, &config); err != nil {
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
package websocket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLoadConfig_EmptyFileMarksConfigForSave(t *testing.T) {
|
|
||||||
t.Setenv("CONFIG_FILE", "")
|
|
||||||
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
configPath := filepath.Join(tmpDir, "config.json")
|
|
||||||
if err := os.WriteFile(configPath, []byte(""), 0o644); err != nil {
|
|
||||||
t.Fatalf("failed to create empty config file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &Client{
|
|
||||||
config: &Config{
|
|
||||||
Endpoint: "https://example.com",
|
|
||||||
ProvisioningKey: "spk-test",
|
|
||||||
},
|
|
||||||
clientType: "newt",
|
|
||||||
configFilePath: configPath,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.loadConfig(); err != nil {
|
|
||||||
t.Fatalf("loadConfig returned error for empty file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !client.configNeedsSave {
|
|
||||||
t.Fatal("expected empty config file to mark configNeedsSave")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user